Unverified Commit 8d5bda39 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

fix: Updates the Dashboard Page URL (#473)

* Modifying DashboardPage component, tests passing

* Cleanups and updating Dashboard resource links

* Flattens folder structure on the component folders

* Extract helpers for building URI and Dashboard Detail Page URL, implement on search results component

* Adding types and cleaning up

* Changing URL schema to use the URI directly on the URL

* Cleanup and basic test for url helper
parent 234a53de
...@@ -42,9 +42,8 @@ import { NO_TIMESTAMP_TEXT } from 'components/constants'; ...@@ -42,9 +42,8 @@ import { NO_TIMESTAMP_TEXT } from 'components/constants';
import './styles.scss'; import './styles.scss';
export interface RouteProps { const STATUS_SUCCESS = 'success';
uri: string; const STATUS_DANGER = 'danger';
}
interface DashboardPageState { interface DashboardPageState {
uri: string; uri: string;
...@@ -56,42 +55,48 @@ export interface StateFromProps { ...@@ -56,42 +55,48 @@ export interface StateFromProps {
dashboard: DashboardMetadata; dashboard: DashboardMetadata;
} }
export interface MatchProps {
uri: string;
}
export interface DispatchFromProps { export interface DispatchFromProps {
getDashboard: (payload: { uri: string, searchIndex?: string, source?: string }) => GetDashboardRequest; getDashboard: (payload: { uri: string, searchIndex?: string, source?: string }) => GetDashboardRequest;
} }
export type DashboardPageProps = RouteComponentProps<RouteProps> & StateFromProps & DispatchFromProps; export type DashboardPageProps = RouteComponentProps<MatchProps> & StateFromProps & DispatchFromProps;
export class DashboardPage extends React.Component<DashboardPageProps, DashboardPageState> { export class DashboardPage extends React.Component<DashboardPageProps, DashboardPageState> {
constructor(props) { constructor(props) {
super(props); super(props);
const { uri } = this.props.match.params;
const { uri } = qs.parse(this.props.location.search);
this.state = { uri }; this.state = { uri };
} }
componentDidMount() { componentDidMount() {
this.loadDashboard(this.state.uri);
}
loadDashboard(uri: string) {
const { index, source } = getLoggingParams(this.props.location.search); const { index, source } = getLoggingParams(this.props.location.search);
const { uri } = this.props.match.params;
this.props.getDashboard({ source, uri, searchIndex: index }); this.props.getDashboard({ source, uri, searchIndex: index });
this.setState({ uri });
} }
componentDidUpdate() { componentDidUpdate() {
const { uri } = qs.parse(this.props.location.search); const { uri } = this.props.match.params;
if (this.state.uri !== uri) { if (this.state.uri !== uri) {
const { index, source } = getLoggingParams(this.props.location.search);
this.setState({ uri }); this.setState({ uri });
this.loadDashboard(uri); this.props.getDashboard({ source, uri, searchIndex: index });
} }
}; };
mapStatusToStyle = (status: string): string => { mapStatusToStyle = (status: string): string => {
if (status === LAST_RUN_SUCCEEDED) { if (status === LAST_RUN_SUCCEEDED) {
return 'success'; return STATUS_SUCCESS;
} }
return 'danger'; return STATUS_DANGER;
}; };
renderTabs() { renderTabs() {
......
...@@ -10,27 +10,27 @@ import LoadingSpinner from '../common/LoadingSpinner'; ...@@ -10,27 +10,27 @@ import LoadingSpinner from '../common/LoadingSpinner';
import { TableDetail, TableDetailProps, MatchProps } from './'; import { TableDetail, TableDetailProps, MatchProps } from './';
describe('TableDetail', () => { const setup = (propOverrides?: Partial<TableDetailProps>, location?: Partial<History.Location>) => {
const routerProps = getMockRouterProps<MatchProps>({
const setup = (propOverrides?: Partial<TableDetailProps>, location?: Partial<History.Location>) => { "cluster":"gold",
const routerProps = getMockRouterProps<MatchProps>({ "database":"hive",
"cluster":"gold", "schema":"base",
"database":"hive", "table":"rides"
"schema":"base", }, location);
"table":"rides" const props = {
}, location); isLoading: false,
const props = { statusCode: 200,
isLoading: false, tableData: tableMetadata,
statusCode: 200, getTableData: jest.fn(),
tableData: tableMetadata, ...routerProps,
getTableData: jest.fn(), ...propOverrides,
...routerProps,
...propOverrides,
};
const wrapper = mount<TableDetail>(<TableDetail {...props} />);
return { props, wrapper };
}; };
const wrapper = mount<TableDetail>(<TableDetail {...props} />);
return { props, wrapper };
};
describe('TableDetail', () => {
describe('render', () => { describe('render', () => {
......
...@@ -88,7 +88,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone ...@@ -88,7 +88,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
this.didComponentMount = true; this.didComponentMount = true;
} }
componentDidUpdate(prevProps) { componentDidUpdate() {
const newKey = this.getTableKey(); const newKey = this.getTableKey();
if (this.key !== newKey) { if (this.key !== newKey) {
...@@ -111,6 +111,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone ...@@ -111,6 +111,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
DO NOT CHANGE DO NOT CHANGE
*/ */
const params = this.props.match.params; const params = this.props.match.params;
return `${params.database}://${params.cluster}.${params.schema}/${params.table}`; return `${params.database}://${params.cluster}.${params.schema}/${params.table}`;
} }
......
...@@ -3,7 +3,7 @@ import * as Avatar from 'react-avatar'; ...@@ -3,7 +3,7 @@ import * as Avatar from 'react-avatar';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import AvatarLabel, { AvatarLabelProps } from '../'; import AvatarLabel, { AvatarLabelProps } from '.';
describe('AvatarLabel', () => { describe('AvatarLabel', () => {
const setup = (propOverrides?: Partial<AvatarLabelProps>) => { const setup = (propOverrides?: Partial<AvatarLabelProps>) => {
......
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import BadgeList from '../' import BadgeList from '.'
import Flag from 'components/common/Flag'; import Flag from 'components/common/Flag';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import * as ConfigUtils from 'config/config-utils'; import * as ConfigUtils from 'config/config-utils';
......
...@@ -6,7 +6,7 @@ import globalState from 'fixtures/globalState'; ...@@ -6,7 +6,7 @@ import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { BookmarkIcon, BookmarkIconProps, mapDispatchToProps, mapStateToProps } from "../"; import { BookmarkIcon, BookmarkIconProps, mapDispatchToProps, mapStateToProps } from ".";
describe('BookmarkIcon', () => { describe('BookmarkIcon', () => {
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Breadcrumb, BreadcrumbProps, mapDispatchToProps } from '../'; import { Breadcrumb, BreadcrumbProps, mapDispatchToProps } from '.';
describe('Breadcrumb', () => { describe('Breadcrumb', () => {
const setup = (propOverrides?: Partial<BreadcrumbProps>) => { const setup = (propOverrides?: Partial<BreadcrumbProps>) => {
......
...@@ -3,7 +3,7 @@ import * as ReactMarkdown from 'react-markdown'; ...@@ -3,7 +3,7 @@ import * as ReactMarkdown from 'react-markdown';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import EditableText, { EditableTextProps } from '../'; import EditableText, { EditableTextProps } from '.';
import { import {
CANCEL_BUTTON_TEXT, CANCEL_BUTTON_TEXT,
REFRESH_BUTTON_TEXT, REFRESH_BUTTON_TEXT,
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import EntityCardSection, { EntityCardSectionProps } from '../'; import EntityCardSection, { EntityCardSectionProps } from '.';
describe('EntityCardSection', () => { describe('EntityCardSection', () => {
let props: EntityCardSectionProps; let props: EntityCardSectionProps;
......
...@@ -2,8 +2,8 @@ import * as React from 'react'; ...@@ -2,8 +2,8 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import EntityCard, { EntityCardProps } from '../'; import EntityCard, { EntityCardProps } from '.';
import EntityCardSection from '../EntityCardSection'; import EntityCardSection from './EntityCardSection';
describe('EntityCard', () => { describe('EntityCard', () => {
let props: EntityCardProps; let props: EntityCardProps;
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import Flag, { CaseType, FlagProps, convertText } from '../'; import Flag, { CaseType, FlagProps, convertText } from '.';
describe('Flag', () => { describe('Flag', () => {
let props: FlagProps; let props: FlagProps;
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import FlashMessage, { FlashMessageProps } from '../'; import FlashMessage, { FlashMessageProps } from '.';
describe('FlashMessage', () => { describe('FlashMessage', () => {
const setup = (propOverrides?: Partial<FlashMessageProps>) => { const setup = (propOverrides?: Partial<FlashMessageProps>) => {
......
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import CheckBoxItem, { CheckBoxItemProps } from '../'; import CheckBoxItem, { CheckBoxItemProps } from '.';
describe('CheckBoxItem', () => { describe('CheckBoxItem', () => {
const expectedChild = (<span>I am a child</span>); const expectedChild = (<span>I am a child</span>);
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import LoadingSpinner from '../'; import LoadingSpinner from '.';
describe('LoadingSpinner', () => { describe('LoadingSpinner', () => {
let subject; let subject;
......
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Preloader, PreloaderProps, mapDispatchToProps } from '../'; import { Preloader, PreloaderProps, mapDispatchToProps } from '.';
describe('Preloader', () => { describe('Preloader', () => {
const setup = (propOverrides?: Partial<PreloaderProps>) => { const setup = (propOverrides?: Partial<PreloaderProps>) => {
......
...@@ -45,8 +45,10 @@ describe('DashboardListItem', () => { ...@@ -45,8 +45,10 @@ describe('DashboardListItem', () => {
describe('getLink', () => { describe('getLink', () => {
it('getLink returns correct string', () => { it('getLink returns correct string', () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
const { dashboard, logging } = props; const expectedURL = "/dashboard/mode_dashboard%3A%2F%2Fcluster.group%2Fname?index=0&source=src";
expect(wrapper.instance().getLink()).toEqual(`/dashboard?uri=${dashboard.uri}&index=${logging.index}&source=${logging.source}`); const actual = wrapper.instance().getLink();
expect(actual).toEqual(expectedURL);
}); });
}); });
......
...@@ -7,11 +7,11 @@ import { LoggingParams } from '../types'; ...@@ -7,11 +7,11 @@ import { LoggingParams } from '../types';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import { getSourceDisplayName, getSourceIconClass } from 'config/config-utils'; import { getSourceDisplayName, getSourceIconClass } from 'config/config-utils';
import { buildDashboardURL } from 'utils/navigationUtils';
import { formatDate } from 'utils/dateUtils';
import { ResourceType, DashboardResource } from 'interfaces'; import { ResourceType, DashboardResource } from 'interfaces';
import { formatDate } from 'utils/dateUtils';
import * as Constants from './constants'; import * as Constants from './constants';
import { NO_TIMESTAMP_TEXT } from 'components/constants'; import { NO_TIMESTAMP_TEXT } from 'components/constants';
...@@ -21,9 +21,11 @@ export interface DashboardListItemProps { ...@@ -21,9 +21,11 @@ export interface DashboardListItemProps {
} }
class DashboardListItem extends React.Component<DashboardListItemProps, {}> { class DashboardListItem extends React.Component<DashboardListItemProps, {}> {
getLink = () => { getLink = () => {
const { dashboard, logging } = this.props; const { dashboard, logging } = this.props;
return `/dashboard?uri=${dashboard.uri}&index=${logging.index}&source=${logging.source}`;
return `${buildDashboardURL(dashboard.uri)}?index=${logging.index}&source=${logging.source}`;
}; };
generateResourceIconClass = (dashboardId: string, dashboardType: ResourceType): string => { generateResourceIconClass = (dashboardId: string, dashboardType: ResourceType): string => {
......
...@@ -4,7 +4,7 @@ import { shallow } from 'enzyme'; ...@@ -4,7 +4,7 @@ import { shallow } from 'enzyme';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import TableListItem, { TableListItemProps } from '../'; import TableListItem, { TableListItemProps } from '.';
import { ResourceType, Badge, TagType } from 'interfaces'; import { ResourceType, Badge, TagType } from 'interfaces';
import * as ConfigUtils from 'config/config-utils'; import * as ConfigUtils from 'config/config-utils';
......
...@@ -6,7 +6,7 @@ import * as Avatar from 'react-avatar'; ...@@ -6,7 +6,7 @@ import * as Avatar from 'react-avatar';
import Flag from 'components/common/Flag'; import Flag from 'components/common/Flag';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import UserListItem, { UserListItemProps } from '../'; import UserListItem, { UserListItemProps } from '.';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
describe('UserListItem', () => { describe('UserListItem', () => {
......
...@@ -2,10 +2,10 @@ import * as React from 'react'; ...@@ -2,10 +2,10 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import DashboardListItem from '../DashboardListItem'; import DashboardListItem from './DashboardListItem';
import TableListItem from '../TableListItem'; import TableListItem from './TableListItem';
import UserListItem from '../UserListItem'; import UserListItem from './UserListItem';
import ResourceListItem, { ListItemProps } from '../'; import ResourceListItem, { ListItemProps } from '.';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
describe('ResourceListItem', () => { describe('ResourceListItem', () => {
......
...@@ -5,6 +5,7 @@ import SearchItemList from './SearchItemList'; ...@@ -5,6 +5,7 @@ import SearchItemList from './SearchItemList';
import ResultItemList from './ResultItemList'; import ResultItemList from './ResultItemList';
import { getSourceDisplayName, getSourceIconClass, indexDashboardsEnabled, indexUsersEnabled } from 'config/config-utils'; import { getSourceDisplayName, getSourceIconClass, indexDashboardsEnabled, indexUsersEnabled } from 'config/config-utils';
import { buildDashboardURL } from 'utils/navigationUtils';
import { GlobalState } from 'ducks/rootReducer' import { GlobalState } from 'ducks/rootReducer'
import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'ducks/search/types'; import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'ducks/search/types';
...@@ -93,15 +94,19 @@ export class InlineSearchResults extends React.Component<InlineSearchResultsProp ...@@ -93,15 +94,19 @@ export class InlineSearchResults extends React.Component<InlineSearchResultsProp
getSuggestedResultHref = (resourceType: ResourceType, result: Resource, index: number): string => { getSuggestedResultHref = (resourceType: ResourceType, result: Resource, index: number): string => {
const logParams = `source=inline_search&index=${index}`; const logParams = `source=inline_search&index=${index}`;
switch (resourceType) { switch (resourceType) {
case ResourceType.dashboard: case ResourceType.dashboard:
const dashboard = result as DashboardResource; const dashboard = result as DashboardResource;
return `/dashboard?uri=${dashboard.uri}&${logParams}`;
return `${buildDashboardURL(dashboard.uri)}?${logParams}`;
case ResourceType.table: case ResourceType.table:
const table = result as TableResource; const table = result as TableResource;
return `/table_detail/${table.cluster}/${table.database}/${table.schema}/${table.name}?${logParams}`; return `/table_detail/${table.cluster}/${table.database}/${table.schema}/${table.name}?${logParams}`;
case ResourceType.user: case ResourceType.user:
const user = result as UserResource; const user = result as UserResource;
return `/user/${user.user_id}?${logParams}`; return `/user/${user.user_id}?${logParams}`;
default: default:
return ''; return '';
......
...@@ -187,8 +187,10 @@ describe('InlineSearchResults', () => { ...@@ -187,8 +187,10 @@ describe('InlineSearchResults', () => {
it('returns the correct href for ResourceType.dashboard', () => { it('returns the correct href for ResourceType.dashboard', () => {
const index = 0; const index = 0;
const givenDashboard = props.dashboards.results[index]; const givenDashboard = props.dashboards.results[index];
const expected = "/dashboard/product_dashboard%3A%2F%2Fcluster.group%2Fname?source=inline_search&index=0";
const output = wrapper.instance().getSuggestedResultHref(ResourceType.dashboard, givenDashboard, index); const output = wrapper.instance().getSuggestedResultHref(ResourceType.dashboard, givenDashboard, index);
expect(output).toEqual(`/dashboard?uri=${givenDashboard.uri}&source=inline_search&index=${index}`);
expect(output).toEqual(expected);
}); });
it('returns the correct href for ResourceType.table', () => { it('returns the correct href for ResourceType.table', () => {
const index = 0; const index = 0;
......
...@@ -3,7 +3,7 @@ import * as History from 'history'; ...@@ -3,7 +3,7 @@ import * as History from 'history';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { mapStateToProps, mapDispatchToProps, SearchBar, SearchBarProps } from '../'; import { mapStateToProps, mapDispatchToProps, SearchBar, SearchBarProps } from '.';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { getMockRouterProps } from 'fixtures/mockRouter'; import { getMockRouterProps } from 'fixtures/mockRouter';
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Tab, Tabs } from 'react-bootstrap'; import { Tab, Tabs } from 'react-bootstrap';
import TabsComponent, { TabsProps } from '../'; import TabsComponent, { TabsProps } from '.';
describe('Tabs', () => { describe('Tabs', () => {
let props: TabsProps; let props: TabsProps;
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import { TagsList, TagsListProps, mapDispatchToProps, mapStateToProps } from '../'; import { TagsList, TagsListProps, mapDispatchToProps, mapStateToProps } from '.';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
......
...@@ -7,7 +7,7 @@ export const dashboardSummary: DashboardResource = { ...@@ -7,7 +7,7 @@ export const dashboardSummary: DashboardResource = {
product: 'mode', product: 'mode',
type: ResourceType.dashboard, type: ResourceType.dashboard,
description: 'I am a dashboard', description: 'I am a dashboard',
uri: 'product_dashboard://cluster.group/name', uri: 'mode_dashboard://cluster.group/name',
url: 'product/name', url: 'product/name',
cluster: 'cluster', cluster: 'cluster',
last_successful_run_timestamp: 1585062593 last_successful_run_timestamp: 1585062593
......
...@@ -43,7 +43,7 @@ ReactDOM.render( ...@@ -43,7 +43,7 @@ ReactDOM.render(
<Switch> <Switch>
<Route path="/announcements" component={AnnouncementPage} /> <Route path="/announcements" component={AnnouncementPage} />
<Route path="/browse" component={BrowsePage} /> <Route path="/browse" component={BrowsePage} />
<Route path="/dashboard" component={DashboardPage} /> <Route path="/dashboard/:uri" component={DashboardPage} />
<Route path="/search" component={SearchPage} /> <Route path="/search" component={SearchPage} />
<Route path="/table_detail/:cluster/:database/:schema/:table" component={TableDetail} /> <Route path="/table_detail/:cluster/:database/:schema/:table" component={TableDetail} />
<Route path="/user/:userId" component={ProfilePage} /> <Route path="/user/:userId" component={ProfilePage} />
......
import * as DateUtils from 'utils/dateUtils';
import * as LogUtils from 'utils/logUtils';
import * as NavigationUtils from 'utils/navigationUtils';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import * as DateUtils from './dateUtils';
import * as LogUtils from './logUtils';
import * as NavigationUtils from './navigationUtils';
import { ResourceType } from 'interfaces/Resources'; import { ResourceType } from 'interfaces/Resources';
...@@ -122,6 +124,16 @@ describe('navigationUtils', () => { ...@@ -122,6 +124,16 @@ describe('navigationUtils', () => {
expect(url).toEqual(expectedUrl); expect(url).toEqual(expectedUrl);
}); });
}); });
describe('buildDashboardURL', () => {
it('encodes the passed URI for safe use on the URL bar', () => {
const testURI = 'product_dashboard://cluster.groupID/dashboardID';
const expected = '/dashboard/product_dashboard%3A%2F%2Fcluster.groupID%2FdashboardID';
const actual = NavigationUtils.buildDashboardURL(testURI);
expect(actual).toEqual(expected);
});
});
}); });
...@@ -206,4 +218,4 @@ describe('logUtils', () => { ...@@ -206,4 +218,4 @@ describe('logUtils', () => {
expect(replaceStateSpy).not.toHaveBeenCalled() expect(replaceStateSpy).not.toHaveBeenCalled()
}); });
}); });
}); });
\ No newline at end of file
...@@ -47,3 +47,12 @@ export const updateSearchUrl = (searchParams: SearchParams, replace: boolean = f ...@@ -47,3 +47,12 @@ export const updateSearchUrl = (searchParams: SearchParams, replace: boolean = f
BrowserHistory.push(newUrl); BrowserHistory.push(newUrl);
} }
}; };
/**
* Creates the dashboard detail URL from the URI
* @param URI String URI of the dashboard, it has this shape: uri = "<product>_dashboard://<cluster>.<groupID>/<dashboardID>"
* @return String Dashboard Detail page URL
*/
export const buildDashboardURL = (URI: string) => {
return `/dashboard/${encodeURIComponent(URI)}`;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment