Skip to content

Commit b0f16cd

Browse files
authored
[AN-156] Broad Methods Repository: "workflows" to "methods" (#5098)
1 parent ac94dfa commit b0f16cd

File tree

9 files changed

+105
-89
lines changed

9 files changed

+105
-89
lines changed

src/components/breadcrumbs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const breadcrumbElement = (child, href) => {
1717
export const commonPaths = {
1818
datasetList: () => [breadcrumbElement('Datasets', Nav.getLink('library-datasets'))],
1919

20-
workflowList: () => [breadcrumbElement('Workflows', Nav.getLink('workflows'))],
20+
workflowList: () => [breadcrumbElement('Methods', Nav.getLink('workflows'))],
2121

2222
workspaceList: () => [breadcrumbElement('Workspaces', Nav.getLink('workspaces'))],
2323

src/pages/workflows/WorkflowList.test.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jest.mock('src/libs/ajax');
1919
jest.mock('src/libs/notifications');
2020
jest.mock('src/libs/nav', () => ({
2121
...jest.requireActual('src/libs/nav'),
22-
getLink: jest.fn(() => '#workflows'),
22+
getLink: jest.fn(() => '#methods'),
2323
goToPath: jest.fn(),
2424
}));
2525

@@ -211,11 +211,11 @@ describe('workflows table', () => {
211211
});
212212

213213
// Assert
214-
expect(screen.getByPlaceholderText('SEARCH WORKFLOWS')).toBeInTheDocument();
215-
expect(screen.getByText('My Workflows (0)')).toBeInTheDocument();
216-
expect(screen.getByText('Public Workflows (0)')).toBeInTheDocument();
214+
expect(screen.getByPlaceholderText('SEARCH METHODS')).toBeInTheDocument();
215+
expect(screen.getByText('My Methods (0)')).toBeInTheDocument();
216+
expect(screen.getByText('Public Methods (0)')).toBeInTheDocument();
217217

218-
expect(screen.queryByText('Featured Workflows')).not.toBeInTheDocument();
218+
expect(screen.queryByText('Featured Methods')).not.toBeInTheDocument();
219219
});
220220

221221
it('renders the workflows table with method information', async () => {
@@ -235,7 +235,7 @@ describe('workflows table', () => {
235235

236236
const headers: HTMLElement[] = within(table).getAllByRole('columnheader');
237237
expect(headers).toHaveLength(4);
238-
expect(headers[0]).toHaveTextContent('Workflow');
238+
expect(headers[0]).toHaveTextContent('Method');
239239
expect(headers[1]).toHaveTextContent('Synopsis');
240240
expect(headers[2]).toHaveTextContent('Owners');
241241
expect(headers[3]).toHaveTextContent('Snapshots');
@@ -269,7 +269,7 @@ describe('workflows table', () => {
269269
expect(screen.queryByText('daruk method')).not.toBeInTheDocument();
270270
});
271271

272-
it('displays only my workflows in the my workflows tab', async () => {
272+
it('displays only my workflows in the my methods tab', async () => {
273273
// Arrange
274274
asMockedFn(Ajax).mockImplementation(
275275
() => mockAjax([darukMethod, revaliMethod, revaliPrivateMethod]) as AjaxContract
@@ -307,7 +307,7 @@ describe('workflows table', () => {
307307
expect(screen.queryByText('ganon private method')).not.toBeInTheDocument();
308308
});
309309

310-
it('displays only public workflows in the public workflows tab', async () => {
310+
it('displays only public workflows in the public methods tab', async () => {
311311
// Arrange
312312
asMockedFn(Ajax).mockImplementation(
313313
() => mockAjax([darukMethod, revaliMethod, ganonPrivateMethod]) as AjaxContract
@@ -343,8 +343,8 @@ describe('workflows table', () => {
343343
});
344344

345345
// Assert
346-
expect(screen.getByText(`My Workflows (${myMethodsCount})`)).toBeInTheDocument();
347-
expect(screen.getByText(`Public Workflows (${publicMethodsCount})`)).toBeInTheDocument();
346+
expect(screen.getByText(`My Methods (${myMethodsCount})`)).toBeInTheDocument();
347+
expect(screen.getByText(`Public Methods (${publicMethodsCount})`)).toBeInTheDocument();
348348
}
349349
);
350350

@@ -365,10 +365,10 @@ describe('workflows table', () => {
365365
// Assert
366366

367367
// currently selected tab - count based on filter
368-
expect(screen.getByText('My Workflows (1)')).toBeInTheDocument();
368+
expect(screen.getByText('My Methods (1)')).toBeInTheDocument();
369369

370370
// other tab - count not based on filter
371-
expect(screen.getByText('Public Workflows (3)')).toBeInTheDocument();
371+
expect(screen.getByText('Public Methods (3)')).toBeInTheDocument();
372372
});
373373

374374
it('filters workflows by namespace', async () => {
@@ -422,7 +422,7 @@ describe('workflows table', () => {
422422
// Arrange
423423
asMockedFn(Ajax).mockImplementation(() => mockAjax([darukMethod, revaliMethod]) as AjaxContract);
424424

425-
// set the user's email to ensure there are no my workflows
425+
// set the user's email to ensure there are no my methods
426426
jest.spyOn(userStore, 'get').mockImplementation(jest.fn().mockReturnValue(mockUserState('[email protected]')));
427427

428428
// should be called to switch tabs
@@ -435,8 +435,8 @@ describe('workflows table', () => {
435435
render(<WorkflowList queryParams={{ filter: 'test' }} />);
436436
});
437437

438-
await user.click(screen.getByText('Public Workflows (2)'));
439-
await user.click(screen.getByText('My Workflows (0)'));
438+
await user.click(screen.getByText('Public Methods (2)'));
439+
await user.click(screen.getByText('My Methods (0)'));
440440

441441
// Assert
442442
expect(navHistoryReplace).toHaveBeenCalledTimes(2);
@@ -457,7 +457,7 @@ describe('workflows table', () => {
457457
render(<WorkflowList />);
458458
});
459459

460-
fireEvent.change(screen.getByPlaceholderText('SEARCH WORKFLOWS'), { target: { value: 'mysearch' } });
460+
fireEvent.change(screen.getByPlaceholderText('SEARCH METHODS'), { target: { value: 'mysearch' } });
461461
await act(() => delay(300)); // debounced search
462462

463463
// Assert
@@ -478,7 +478,7 @@ describe('workflows table', () => {
478478
expect(screen.getByRole('searchbox')).toHaveProperty('value', 'testfilter');
479479
});
480480

481-
it('sorts by workflow ascending by default', async () => {
481+
it('sorts by method ascending by default', async () => {
482482
// Arrange
483483
asMockedFn(Ajax).mockImplementation(
484484
() => mockAjax([sortingMethod, darukMethod, revaliMethod, revaliMethod2]) as AjaxContract
@@ -493,7 +493,7 @@ describe('workflows table', () => {
493493
checkOrder('daruk method', 'revali method', 'revali method 2', 'sorting method');
494494
});
495495

496-
it('sorts by workflow descending', async () => {
496+
it('sorts by method descending', async () => {
497497
// Arrange
498498
asMockedFn(Ajax).mockImplementation(
499499
() => mockAjax([sortingMethod, darukMethod, revaliMethod, revaliMethod2]) as AjaxContract
@@ -506,7 +506,7 @@ describe('workflows table', () => {
506506
render(<WorkflowList queryParams={{ tab: 'public' }} />);
507507
});
508508

509-
await user.click(screen.getByText('Workflow'));
509+
await user.click(screen.getByText('Method'));
510510

511511
// Assert
512512
checkOrder('sorting method', 'revali method 2', 'revali method', 'daruk method');
@@ -721,14 +721,14 @@ describe('workflows table', () => {
721721
expect(screen.getByText('11 - 13 of 13')).toBeInTheDocument();
722722

723723
// Act
724-
await user.click(screen.getByText('Public Workflows (13)'));
724+
await user.click(screen.getByText('Public Methods (13)'));
725725

726726
// Assert
727727

728728
// Note: the total workflow count (13) is actually still from
729729
// the previous tab just in the test because Nav cannot be
730730
// mocked properly to actually switch tabs when the public
731-
// workflows tab is clicked
731+
// methods tab is clicked
732732
expect(screen.getByText('1 - 10 of 13')).toBeInTheDocument();
733733
});
734734

@@ -753,7 +753,7 @@ describe('workflows table', () => {
753753
expect(screen.getByText('11 - 13 of 13')).toBeInTheDocument();
754754

755755
// Act
756-
fireEvent.change(screen.getByPlaceholderText('SEARCH WORKFLOWS'), { target: { value: 'method' } });
756+
fireEvent.change(screen.getByPlaceholderText('SEARCH METHODS'), { target: { value: 'method' } });
757757
await act(() => delay(300)); // debounced search
758758

759759
// Assert
@@ -907,11 +907,11 @@ describe('workflows table', () => {
907907

908908
// tabs should not display method counts because their
909909
// true values are not known
910-
expect(screen.getByText('My Workflows')).toBeInTheDocument();
911-
expect(screen.getByText('Public Workflows')).toBeInTheDocument();
910+
expect(screen.getByText('My Methods')).toBeInTheDocument();
911+
expect(screen.getByText('Public Methods')).toBeInTheDocument();
912912

913913
expect(screen.getByText('Nothing to display')).toBeInTheDocument();
914-
expect(notify).toHaveBeenCalledWith('error', 'Error loading workflows', expect.anything());
914+
expect(notify).toHaveBeenCalledWith('error', 'Error loading methods', expect.anything());
915915
});
916916
});
917917

src/pages/workflows/WorkflowList.tsx

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@ import { withBusyState } from 'src/libs/utils';
2020
import { WorkflowModal } from 'src/pages/workflows/workflow/common/WorkflowModal';
2121
import { MethodDefinition } from 'src/pages/workflows/workflow-utils';
2222

23+
// Note: The first tab key in this array will determine the default tab selected
24+
// if the tab query parameter is not present or has an invalid value (and when
25+
// clicking on that tab, the tab query parameter will not be used in the URL)
26+
const tabKeys = ['mine', 'public'] as const;
27+
type TabKey = (typeof tabKeys)[number]; // 'mine' | 'public'
28+
29+
// Custom type guard
30+
const isTabKey = (val: any): val is TabKey => _.includes(val, tabKeys);
31+
32+
const defaultTabKey: TabKey = tabKeys[0];
33+
2334
/**
2435
* Represents a list of method definitions grouped into two
25-
* categories — My Workflows and Public Workflows — corresponding
36+
* categories — My Methods and Public Methods — corresponding
2637
* to the tabs above the workflows table.
2738
*/
28-
interface GroupedWorkflows {
29-
mine: MethodDefinition[];
30-
public: MethodDefinition[];
31-
}
39+
type GroupedWorkflows = Record<TabKey, MethodDefinition[]>;
3240

3341
// This is based on the sort type from the FlexTable component
3442
// When that component is converted to TypeScript, we should use its sort type
@@ -39,7 +47,7 @@ interface SortProperties {
3947
}
4048

4149
interface NewQueryParams {
42-
newTab?: string;
50+
newTab?: TabKey;
4351
newFilter?: string;
4452
}
4553

@@ -80,14 +88,16 @@ interface WorkflowListProps {
8088
// TODO: consider wrapping query updates in useEffect
8189
export const WorkflowList = (props: WorkflowListProps) => {
8290
const { queryParams = {} } = props;
83-
const { tab = 'mine', filter = '', ...query } = queryParams;
91+
const { tab: queryTab, filter = '', ...query } = queryParams;
92+
93+
const selectedTab: TabKey = isTabKey(queryTab) ? queryTab : defaultTabKey;
8494

8595
const signal: AbortSignal = useCancellation();
8696
const [busy, setBusy] = useState<boolean>(false);
8797

8898
// workflows is undefined while the method definitions are still loading;
8999
// it is null if there is an error while loading
90-
const [workflows, setWorkflows] = useState<GroupedWorkflows | null>();
100+
const [workflows, setWorkflows] = useState<GroupedWorkflows | null | undefined>();
91101

92102
// Valid direction values are 'asc' and 'desc' (based on expected
93103
// function signatures from the Sortable component used in this
@@ -99,9 +109,9 @@ export const WorkflowList = (props: WorkflowListProps) => {
99109
const [pageNumber, setPageNumber] = useState(1);
100110
const [itemsPerPage, setItemsPerPage] = useState(25);
101111

102-
const getTabQueryName = (newTab: string | undefined): string | undefined => (newTab === 'mine' ? undefined : newTab);
112+
const getTabQueryName = (newTab: TabKey): TabKey | undefined => (newTab === defaultTabKey ? undefined : newTab);
103113

104-
const getUpdatedQuery = ({ newTab = tab, newFilter = filter }: NewQueryParams): string => {
114+
const getUpdatedQuery = ({ newTab = selectedTab, newFilter = filter }: NewQueryParams): string => {
105115
// Note: setting undefined so that falsy values don't show up at all
106116
return qs.stringify(
107117
{ ...query, tab: getTabQueryName(newTab), filter: newFilter || undefined },
@@ -123,27 +133,30 @@ export const WorkflowList = (props: WorkflowListProps) => {
123133
setSort(newSort);
124134
};
125135

126-
const tabName: string = tab || 'mine';
127-
const tabs = { mine: 'My Workflows', public: 'Public Workflows' };
136+
const tabNames: Record<TabKey, string> = { mine: 'My Methods', public: 'Public Methods' };
128137

129-
const getTabDisplayNames = (workflows: GroupedWorkflows | null | undefined, currentTabName: string) => {
130-
const getCountString = (tabName: keyof GroupedWorkflows): string => {
138+
const getTabDisplayNames = (
139+
workflows: GroupedWorkflows | null | undefined,
140+
selectedTab: TabKey
141+
): Record<TabKey, string> => {
142+
const getCountString = (tab: TabKey): string => {
131143
if (workflows == null) {
132144
return '';
133145
}
134146

135-
// Only the current tab's workflow count reflects the search
147+
// Only the currently selected tab's workflow count reflects the search
136148
// filter (since the filter is cleared when switching tabs)
137-
if (tabName === currentTabName) {
149+
if (tab === selectedTab) {
138150
return ` (${sortedWorkflows.length})`;
139151
}
140-
return ` (${workflows[tabName].length})`;
152+
return ` (${workflows[tab].length})`;
141153
};
142154

143-
return {
144-
mine: `My Workflows${getCountString('mine')}`,
145-
public: `Public Workflows${getCountString('public')}`,
146-
};
155+
const tabDisplayNames: Record<TabKey, string> = { ...tabNames }; // (shallow) copy
156+
for (const tabKey of tabKeys) {
157+
tabDisplayNames[tabKey] += getCountString(tabKey);
158+
}
159+
return tabDisplayNames;
147160
};
148161

149162
useOnMount(() => {
@@ -160,7 +173,7 @@ export const WorkflowList = (props: WorkflowListProps) => {
160173
});
161174
} catch (error) {
162175
setWorkflows(null);
163-
notify('error', 'Error loading workflows', { detail: error instanceof Response ? await error.text() : error });
176+
notify('error', 'Error loading methods', { detail: error instanceof Response ? await error.text() : error });
164177
}
165178
});
166179

@@ -174,7 +187,7 @@ export const WorkflowList = (props: WorkflowListProps) => {
174187
snapshotId,
175188
});
176189

177-
// Gets the sort key of a method definition based on the currently
190+
// Get the sort key of a method definition based on the currently
178191
// selected sort field such that numeric fields are sorted numerically
179192
// and other fields are sorted as case-insensitive strings
180193
const getSortKey = ({ [sort.field]: sortValue }: MethodDefinition): number | string => {
@@ -187,25 +200,30 @@ export const WorkflowList = (props: WorkflowListProps) => {
187200
return _.lowerCase(sortValue.toString());
188201
};
189202

190-
const sortedWorkflows: MethodDefinition[] = _.flow<MethodDefinition[], MethodDefinition[], MethodDefinition[]>(
203+
const sortedWorkflows: MethodDefinition[] = _.flow<
204+
// filter input type: MethodDefinition[] | undefined (extra [] are because the inputs are viewed as a rest parameter)
205+
(MethodDefinition[] | undefined)[],
206+
MethodDefinition[], // filter output type / orderBy input type
207+
MethodDefinition[] // final result type
208+
>(
191209
_.filter(({ namespace, name }: MethodDefinition) => Utils.textMatch(filter, `${namespace}/${name}`)),
192210
_.orderBy([getSortKey], [sort.direction])
193-
)(workflows?.[tabName]);
211+
)(workflows?.[selectedTab]);
194212

195213
const firstPageIndex: number = (pageNumber - 1) * itemsPerPage;
196214
const lastPageIndex: number = firstPageIndex + itemsPerPage;
197215
const paginatedWorkflows: MethodDefinition[] = sortedWorkflows.slice(firstPageIndex, lastPageIndex);
198216

199217
return (
200218
<FooterWrapper>
201-
<TopBar title='Workflows' href=''>
219+
<TopBar title='Broad Methods Repository' href=''>
202220
{null /* no additional content to display in the top bar */}
203221
</TopBar>
204222
<TabBar
205-
aria-label='workflows menu'
206-
activeTab={tabName}
207-
tabNames={Object.keys(tabs)}
208-
displayNames={getTabDisplayNames(workflows, tabName)}
223+
aria-label='methods list menu'
224+
activeTab={selectedTab}
225+
tabNames={tabKeys}
226+
displayNames={getTabDisplayNames(workflows, selectedTab)}
209227
getHref={(currentTab) => `${Nav.getLink('workflows')}${getUpdatedQuery({ newTab: currentTab })}`}
210228
getOnClick={(currentTab) => (e) => {
211229
e.preventDefault();
@@ -218,8 +236,8 @@ export const WorkflowList = (props: WorkflowListProps) => {
218236
<div style={{ display: 'flex' }}>
219237
<DelayedSearchInput
220238
style={{ width: 500, display: 'flex', justifyContent: 'flex-start' }}
221-
placeholder='SEARCH WORKFLOWS'
222-
aria-label='Search workflows'
239+
placeholder='SEARCH METHODS'
240+
aria-label='Search methods'
223241
onChange={(val) => updateQuery({ newFilter: val })}
224242
value={filter}
225243
/>
@@ -238,7 +256,7 @@ export const WorkflowList = (props: WorkflowListProps) => {
238256
<AutoSizer>
239257
{({ width, height }) => (
240258
<FlexTable
241-
aria-label={tabs[tabName]}
259+
aria-label={tabNames[selectedTab]}
242260
width={width}
243261
height={height}
244262
sort={sort as any /* necessary until FlexTable is converted to TS */}
@@ -257,7 +275,7 @@ export const WorkflowList = (props: WorkflowListProps) => {
257275
// @ts-expect-error
258276
<Paginator
259277
filteredDataLength={sortedWorkflows.length}
260-
unfilteredDataLength={workflows![tabName].length}
278+
unfilteredDataLength={workflows![selectedTab].length}
261279
pageNumber={pageNumber}
262280
setPageNumber={setPageNumber}
263281
itemsPerPage={itemsPerPage}
@@ -295,7 +313,7 @@ const getColumns = (
295313
field: 'name',
296314
headerRenderer: () => (
297315
<WorkflowTableHeader sort={sort} field='name' onSort={onSort}>
298-
Workflow
316+
Method
299317
</WorkflowTableHeader>
300318
),
301319
cellRenderer: ({ rowIndex }) => {
@@ -359,8 +377,8 @@ const getColumns = (
359377
export const navPaths = [
360378
{
361379
name: 'workflows',
362-
path: '/workflows',
380+
path: '/methods',
363381
component: WorkflowList,
364-
title: 'Workflows',
382+
title: 'Broad Methods Repository',
365383
},
366384
];

0 commit comments

Comments
 (0)