[ARVADOS-WORKBENCH2] updated: 2.3.2.1-5-g943b2db5

Git user git at public.arvados.org
Mon Feb 21 21:25:57 UTC 2022


Summary of changes:
 cypress/integration/collection.spec.js             |   3 +-
 cypress/integration/project.spec.js                |   4 +-
 .../collection-panel-files.tsx                     |   4 +-
 .../collection-panel-files2.test.tsx               | 116 -----------------
 .../collection-panel-files2.tsx                    | 138 ---------------------
 src/components/data-explorer/data-explorer.tsx     |   4 +
 src/components/search-input/search-input.test.tsx  |  26 ++--
 src/components/search-input/search-input.tsx       |  10 +-
 .../run-process-panel/run-process-first-step.tsx   |   2 +-
 9 files changed, 38 insertions(+), 269 deletions(-)
 delete mode 100644 src/components/collection-panel-files/collection-panel-files2.test.tsx
 delete mode 100644 src/components/collection-panel-files/collection-panel-files2.tsx

  discards  5bd7e4cbdf5fa2f9ac4fc1503b852585b8d67a78 (commit)
  discards  775d6d6fe17c0dafac02d25f515bf8143ac2ef4b (commit)
  discards  5eedce18197d1e8299fd89714c61c509b2d68ea9 (commit)
  discards  4cfd1b381bd0bd82760ab7062185f8fa5e56ba9a (commit)
       via  943b2db5779fb570ac3be110eb7a5a9f3c89ca9e (commit)
       via  0fb9480d19d8ea1f8b524952d154edc1022cff9c (commit)
       via  94bf91c274d9aafd13b49ad6908e0d5a80612c9d (commit)
       via  d0e5901b7b11ac9c530b791778597868f70b04a7 (commit)
       via  f4a6396d59ca5f39c260b10dd9c63247fac47791 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (5bd7e4cbdf5fa2f9ac4fc1503b852585b8d67a78)
            \
             N -- N -- N (943b2db5779fb570ac3be110eb7a5a9f3c89ca9e)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 943b2db5779fb570ac3be110eb7a5a9f3c89ca9e
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Wed Feb 16 16:02:09 2022 +0100

    Merge branch '18594-Collection-Advanced-Menu-is-trying-to-fetch-User-record-with-collection-UUID' into main
    closes #18594
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index b3d6bbed..2ec0bb9e 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -199,7 +199,9 @@ describe('Project tests', function() {
         cy.createGroup(activeUser.token, {
             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
             group_class: 'project',
-        }).as('testProject1');
+        }).as('testProject1').then((testProject1) => {
+            cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, 'can_write');
+        });
 
         cy.getAll('@testProject1').then(function([testProject1]) {
             cy.loginAs(activeUser);
@@ -213,4 +215,48 @@ describe('Project tests', function() {
             cy.get('[data-cy=search-input] input').should('not.have.value', 'test123');
         });
     });
+
+    it('opens advanced popup for project with username', () => {
+        const projectName = `Test project ${Math.floor(Math.random() * 999999)}`;
+
+        cy.createGroup(adminUser.token, {
+            name: projectName,
+            group_class: 'project',
+        }).as('mainProject')
+
+        cy.getAll('@mainProject')
+            .then(function ([mainProject]) {
+                cy.loginAs(adminUser);
+                
+                cy.get('[data-cy=side-panel-tree]').contains('Groups').click();
+
+                cy.get('[data-cy=uuid]').eq(0).invoke('text').then(uuid => {
+                    cy.createLink(adminUser.token, {
+                        name: 'can_write',
+                        link_class: 'permission',
+                        head_uuid: mainProject.uuid,
+                        tail_uuid: uuid
+                    });
+
+                    cy.createLink(adminUser.token, {
+                        name: 'can_write',
+                        link_class: 'permission',
+                        head_uuid: mainProject.uuid,
+                        tail_uuid: activeUser.user.uuid
+                    });
+
+                    cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+                    cy.get('main').contains(projectName).rightclick();
+
+                    cy.get('[data-cy=context-menu]').contains('Advanced').click();
+
+                    cy.get('[role=tablist]').contains('METADATA').click();
+
+                    cy.get('td').contains(uuid).should('exist');
+
+                    cy.get('td').contains(activeUser.user.uuid).should('exist');
+                });
+        });
+    });
 });
\ No newline at end of file
diff --git a/src/store/advanced-tab/advanced-tab.tsx b/src/store/advanced-tab/advanced-tab.tsx
index 25d90195..61fd705a 100644
--- a/src/store/advanced-tab/advanced-tab.tsx
+++ b/src/store/advanced-tab/advanced-tab.tsx
@@ -280,8 +280,8 @@ const getDataForAdvancedTab = (uuid: string) =>
                 .addEqual('head_uuid', uuid)
                 .getFilters()
         });
-        const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid || '');
-        return { data, metadata, user };
+
+        return { data, metadata };
     };
 
 const initAdvancedTabDialog = (data: AdvancedTabDialogData) => dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data });
diff --git a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
index b631a74c..f493df33 100644
--- a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
+++ b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
@@ -61,7 +61,6 @@ export const AdvancedTabDialog = compose(
                 curlHeader,
                 curlExample,
                 uuid,
-                user
             } = this.props.data;
             return <Dialog
                 open={open}
@@ -78,7 +77,11 @@ export const AdvancedTabDialog = compose(
                 </Tabs>
                 <DialogContent className={classes.content}>
                     {value === 0 && <div>{dialogContentExample(apiResponse, classes)}</div>}
-                    {value === 1 && <div>{metadata !== '' && metadata.items.length > 0 ? <MetadataTab items={metadata.items} uuid={uuid} user={user} /> : dialogContentHeader('(No metadata links found)')}</div>}
+                    {value === 1 && <div>
+                        {metadata !== '' && metadata.items.length > 0 ?
+                            <MetadataTab items={metadata.items} uuid={uuid} />
+                            : dialogContentHeader('(No metadata links found)')}
+                    </div>}
                     {value === 2 && dialogContent(pythonHeader, pythonExample, classes)}
                     {value === 3 && <div>
                         {dialogContent(cliGetHeader, cliGetExample, classes)}
diff --git a/src/views-components/advanced-tab-dialog/metadataTab.tsx b/src/views-components/advanced-tab-dialog/metadataTab.tsx
index 9f08d1e3..1b950d24 100644
--- a/src/views-components/advanced-tab-dialog/metadataTab.tsx
+++ b/src/views-components/advanced-tab-dialog/metadataTab.tsx
@@ -4,7 +4,6 @@
 
 import React from "react";
 import { Table, TableHead, TableCell, TableRow, TableBody, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { UserResource, getUserDisplayName } from "models/user";
 
 type CssRules = 'cell';
 
@@ -25,7 +24,6 @@ interface MetadataTable {
 
 interface MetadataProps {
     items: MetadataTable[];
-    user: UserResource;
     uuid: string;
 }
 
@@ -47,7 +45,7 @@ export const MetadataTab = withStyles(styles)((props: MetadataProps & WithStyles
                     <TableCell className={props.classes.cell}>{it.uuid}</TableCell>
                     <TableCell className={props.classes.cell}>{it.linkClass}</TableCell>
                     <TableCell className={props.classes.cell}>{it.name}</TableCell>
-                    <TableCell className={props.classes.cell}>{props.user && `User: ${getUserDisplayName(props.user)}`}</TableCell>
+                    <TableCell className={props.classes.cell}>{it.tailUuid}</TableCell>
                     <TableCell className={props.classes.cell}>{it.headUuid === props.uuid ? 'this' : it.headUuid}</TableCell>
                     <TableCell className={props.classes.cell}>{JSON.stringify(it.properties)}</TableCell>
                 </TableRow>

commit 0fb9480d19d8ea1f8b524952d154edc1022cff9c
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Thu Dec 16 17:11:03 2021 +0100

    Merge branch '17579-Clear-table-filter-when-changing-the-project' into main
    closes #17579
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index f91dbb5b..57493bc2 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -992,7 +992,8 @@ describe('Collection panel tests', function () {
 
                         cy.get('[data-cy=form-submit-btn]').click();
 
-                        cy.get('button[aria-label=Remove]').click({ multiple: true });
+                        cy.get('button[aria-label=Remove]').should('exist');
+                        cy.get('button[aria-label=Remove]').click({ multiple: true, force: true });
 
                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
                     });
diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index 1c175952..b3d6bbed 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -194,4 +194,23 @@ describe('Project tests', function() {
             cy.contains(testRootProject.uuid).should('exist');
         });
     });
+
+    it('clears search input when changing project', () => {
+        cy.createGroup(activeUser.token, {
+            name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+            group_class: 'project',
+        }).as('testProject1');
+
+        cy.getAll('@testProject1').then(function([testProject1]) {
+            cy.loginAs(activeUser);
+
+            cy.get('[data-cy=side-panel-tree]').contains(testProject1.name).click();
+
+            cy.get('[data-cy=search-input] input').type('test123');
+
+            cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+            cy.get('[data-cy=search-input] input').should('not.have.value', 'test123');
+        });
+    });
 });
\ No newline at end of file
diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
index dd28d0fc..147066c9 100644
--- a/src/components/collection-panel-files/collection-panel-files.tsx
+++ b/src/components/collection-panel-files/collection-panel-files.tsx
@@ -454,7 +454,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                         </IconButton>
                     </Tooltip>
                     <div className={path.length > 1 ? classes.searchWrapper : classes.searchWrapperHidden}>
-                        <SearchInput label="Search" value={leftSearch} onSearch={setLeftSearch} />
+                        <SearchInput selfClearProp={leftKey} label="Search" value={leftSearch} onSearch={setLeftSearch} />
                     </div>
                     <div className={classes.dataWrapper}>
                         {
@@ -501,7 +501,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                 </div>
                 <div className={classes.rightPanel}>
                     <div className={classes.searchWrapper}>
-                        <SearchInput label="Search" value={rightSearch} onSearch={setRightSearch} />
+                        <SearchInput selfClearProp={rightKey} label="Search" value={rightSearch} onSearch={setRightSearch} />
                     </div>
                     {
                         isWritable &&
diff --git a/src/components/collection-panel-files/collection-panel-files2.test.tsx b/src/components/collection-panel-files/collection-panel-files2.test.tsx
deleted file mode 100644
index 4d8b8150..00000000
--- a/src/components/collection-panel-files/collection-panel-files2.test.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { configure, shallow, mount } from "enzyme";
-import { WithStyles } from "@material-ui/core";
-import Adapter from "enzyme-adapter-react-16";
-import { TreeItem, TreeItemStatus } from '../tree/tree';
-import { FileTreeData } from '../file-tree/file-tree-data';
-import { CollectionFileType } from "../../models/collection-file";
-import { CollectionPanelFilesComponent, CollectionPanelFilesProps, CssRules } from './collection-panel-files2';
-import { SearchInput } from '../search-input/search-input';
-
-configure({ adapter: new Adapter() });
-
-jest.mock('components/file-tree/file-tree', () => ({
-    FileTree: () => 'FileTree',
-}));
-
-describe('<CollectionPanelFiles />', () => {
-    let props: CollectionPanelFilesProps & WithStyles<CssRules>;
-
-    beforeEach(() => {
-        props = {
-            classes: {} as Record<CssRules, string>,
-            items: [],
-            isWritable: true,
-            isLoading: false,
-            tooManyFiles: false,
-            onUploadDataClick: jest.fn(),
-            onSearchChange: jest.fn(),
-            onItemMenuOpen: jest.fn(),
-            onOptionsMenuOpen: jest.fn(),
-            onSelectionToggle: jest.fn(),
-            onCollapseToggle: jest.fn(),
-            onFileClick: jest.fn(),
-            loadFilesFunc: jest.fn(),
-            currentItemUuid: '',
-        };
-    });
-
-    it('renders properly', () => {
-        // when
-        const wrapper = shallow(<CollectionPanelFilesComponent {...props} />);
-
-        // then
-        expect(wrapper).not.toBeUndefined();
-    });
-
-    it('filters out files', () => {
-        // given
-        const searchPhrase = 'test';
-        const items: Array<TreeItem<FileTreeData>> = [
-            {
-                data: {
-                    url: '',
-                    type: CollectionFileType.DIRECTORY,
-                    name: 'test',
-                },
-                id: '1',
-                open: true,
-                active: true,
-                status: TreeItemStatus.LOADED,
-            },
-            {
-                data: {
-                    url: '',
-                    type: CollectionFileType.FILE,
-                    name: 'test123',
-                },
-                id: '2',
-                open: true,
-                active: true,
-                status: TreeItemStatus.LOADED,
-            },
-            {
-                data: {
-                    url: '',
-                    type: CollectionFileType.FILE,
-                    name: 'another-file',
-                },
-                id: '3',
-                open: true,
-                active: true,
-                status: TreeItemStatus.LOADED,
-            }
-        ];
-
-        // setup
-        props.items = items;
-        const wrapper = mount(<CollectionPanelFilesComponent {...props} />);
-        wrapper.find(SearchInput).simulate('change', { target: { value: searchPhrase } });
-
-        // when
-        setTimeout(() => { // we have to use set timeout because of the debounce
-            expect(wrapper.find('FileTree').prop('items'))
-            .toEqual([
-                {
-                    data: { url: '', type: 'directory', name: 'test' },
-                    id: '1',
-                    open: true,
-                    active: true,
-                    status: 'loaded'
-                },
-                {
-                    data: { url: '', type: 'file', name: 'test123' },
-                    id: '2',
-                    open: true,
-                    active: true,
-                    status: 'loaded'
-                }
-            ]);
-        }, 0);
-    });
-});
\ No newline at end of file
diff --git a/src/components/collection-panel-files/collection-panel-files2.tsx b/src/components/collection-panel-files/collection-panel-files2.tsx
deleted file mode 100644
index 41182482..00000000
--- a/src/components/collection-panel-files/collection-panel-files2.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { TreeItem, TreeItemStatus } from 'components/tree/tree';
-import { FileTreeData } from 'components/file-tree/file-tree-data';
-import { FileTree } from 'components/file-tree/file-tree';
-import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, Card, Button, Tooltip, CircularProgress } from '@material-ui/core';
-import { CustomizeTableIcon } from 'components/icon/icon';
-import { DownloadIcon } from 'components/icon/icon';
-import { SearchInput } from '../search-input/search-input';
-
-export interface CollectionPanelFilesProps {
-    items: Array<TreeItem<FileTreeData>>;
-    isWritable: boolean;
-    isLoading: boolean;
-    tooManyFiles: boolean;
-    onUploadDataClick: () => void;
-    onSearchChange: (searchValue: string) => void;
-    onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>, isWritable: boolean) => void;
-    onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => void;
-    onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
-    onCollapseToggle: (id: string, status: TreeItemStatus) => void;
-    onFileClick: (id: string) => void;
-    loadFilesFunc: () => void;
-    currentItemUuid?: string;
-}
-
-export type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button' | 'centeredLabel' | 'cardHeaderContent' | 'cardHeaderContentTitle';
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-    root: {
-        paddingBottom: theme.spacing.unit,
-        height: '100%'
-    },
-    cardSubheader: {
-        paddingTop: 0,
-        paddingBottom: 0,
-        minHeight: 8 * theme.spacing.unit,
-    },
-    cardHeaderContent: {
-        display: 'flex',
-        paddingRight: 2 * theme.spacing.unit,
-        justifyContent: 'space-between',
-    },
-    cardHeaderContentTitle: {
-        paddingLeft: theme.spacing.unit,
-        paddingTop: 2 * theme.spacing.unit,
-        paddingRight: 2 * theme.spacing.unit,
-    },
-    nameHeader: {
-        marginLeft: '75px'
-    },
-    fileSizeHeader: {
-        marginRight: '65px'
-    },
-    uploadIcon: {
-        transform: 'rotate(180deg)'
-    },
-    button: {
-        marginRight: -theme.spacing.unit,
-        marginTop: '8px'
-    },
-    centeredLabel: {
-        fontSize: '0.875rem',
-        textAlign: 'center'
-    },
-});
-
-export const CollectionPanelFilesComponent = ({ onItemMenuOpen, onSearchChange, onOptionsMenuOpen, onUploadDataClick, classes,
-    isWritable, isLoading, tooManyFiles, loadFilesFunc, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) => {
-    const { useState, useEffect } = React;
-    const [searchValue, setSearchValue] = useState('');
-
-    useEffect(() => {
-        onSearchChange(searchValue);
-    }, [onSearchChange, searchValue]);
-
-    return (<Card data-cy='collection-files-panel' className={classes.root}>
-        <CardHeader
-            title={
-                <div className={classes.cardHeaderContent}>
-                    <span className={classes.cardHeaderContentTitle}>Files</span>
-                    <SearchInput
-                        value={searchValue}
-                        label='Search files'
-                        onSearch={setSearchValue} />
-                </div>
-            }
-            className={classes.cardSubheader}
-            classes={{ action: classes.button }}
-            action={<>
-                {isWritable &&
-                    <Button
-                        data-cy='upload-button'
-                        onClick={onUploadDataClick}
-                        variant='contained'
-                        color='primary'
-                        size='small'>
-                        <DownloadIcon className={classes.uploadIcon} />
-                    Upload data
-                </Button>}
-                {!tooManyFiles &&
-                    <Tooltip title="More options" disableFocusListener>
-                        <IconButton
-                            data-cy='collection-files-panel-options-btn'
-                            onClick={(ev) => onOptionsMenuOpen(ev, isWritable)}>
-                            <CustomizeTableIcon />
-                        </IconButton>
-                    </Tooltip>}
-            </>
-            } />
-        {tooManyFiles
-            ? <div className={classes.centeredLabel}>
-                File listing may take some time, please click to browse: <Button onClick={loadFilesFunc}><DownloadIcon />Show files</Button>
-            </div>
-            : <>
-                <Grid container justify="space-between">
-                    <Typography variant="caption" className={classes.nameHeader}>
-                        Name
-                    </Typography>
-                    <Typography variant="caption" className={classes.fileSizeHeader}>
-                        File size
-                    </Typography>
-                </Grid>
-                {isLoading
-                    ? <div className={classes.centeredLabel}><CircularProgress /></div>
-                    : <div style={{ height: 'calc(100% - 60px)' }}>
-                        <FileTree
-                            onMenuOpen={(ev, item) => onItemMenuOpen(ev, item, isWritable)}
-                            {...treeProps} /></div>}
-            </>
-        }
-    </Card>);
-};
-
-export const CollectionPanelFiles = withStyles(styles)(CollectionPanelFilesComponent);
diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index d272e870..78aae350 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -83,11 +83,13 @@ type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T
 
 export const DataExplorer = withStyles(styles)(
     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+
         componentDidMount() {
             if (this.props.onSetColumns) {
                 this.props.onSetColumns(this.props.columns);
             }
         }
+
         render() {
             const {
                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
@@ -96,6 +98,7 @@ export const DataExplorer = withStyles(styles)(
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
                 paperKey, fetchMode, currentItemUuid, title
             } = this.props;
+
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
                 {title && <div className={classes.title}>{title}</div>}
                 {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
@@ -104,6 +107,7 @@ export const DataExplorer = withStyles(styles)(
                             {!hideSearchInput && <SearchInput
                                 label={searchLabel}
                                 value={searchValue}
+                                selfClearProp={currentItemUuid}
                                 onSearch={onSearch} />}
                         </div>
                         {actions}
diff --git a/src/components/search-input/search-input.test.tsx b/src/components/search-input/search-input.test.tsx
index 90c52b76..c57d3608 100644
--- a/src/components/search-input/search-input.test.tsx
+++ b/src/components/search-input/search-input.test.tsx
@@ -21,20 +21,20 @@ describe("<SearchInput />", () => {
 
     describe("on submit", () => {
         it("calls onSearch with initial value passed via props", () => {
-            const searchInput = mount(<SearchInput value="initial value" onSearch={onSearch} />);
+            const searchInput = mount(<SearchInput selfClearProp="" value="initial value" onSearch={onSearch} />);
             searchInput.find("form").simulate("submit");
             expect(onSearch).toBeCalledWith("initial value");
         });
 
         it("calls onSearch with current value", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} />);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             searchInput.find("form").simulate("submit");
             expect(onSearch).toBeCalledWith("current value");
         });
 
         it("calls onSearch with new value passed via props", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} />);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             searchInput.setProps({value: "new value"});
             searchInput.find("form").simulate("submit");
@@ -42,7 +42,7 @@ describe("<SearchInput />", () => {
         });
 
         it("cancels timeout set on input value change", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000} />);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={1000} />);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             searchInput.find("form").simulate("submit");
             jest.runTimersToTime(1000);
@@ -54,7 +54,7 @@ describe("<SearchInput />", () => {
 
     describe("on input value change", () => {
         it("calls onSearch after default timeout", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} />);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} />);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             expect(onSearch).not.toBeCalled();
             jest.runTimersToTime(DEFAULT_SEARCH_DEBOUNCE);
@@ -62,7 +62,7 @@ describe("<SearchInput />", () => {
         });
 
         it("calls onSearch after the time specified in props has passed", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={2000}/>);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={2000}/>);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             jest.runTimersToTime(1000);
             expect(onSearch).not.toBeCalled();
@@ -71,7 +71,7 @@ describe("<SearchInput />", () => {
         });
 
         it("calls onSearch only once after no change happened during the specified time", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={1000}/>);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             jest.runTimersToTime(500);
             searchInput.find("input").simulate("change", { target: { value: "changed value" } });
@@ -80,7 +80,7 @@ describe("<SearchInput />", () => {
         });
 
         it("calls onSearch again after the specified time has passed since previous call", () => {
-            const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
+            const searchInput = mount(<SearchInput selfClearProp="" value="" onSearch={onSearch} debounce={1000}/>);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
             jest.runTimersToTime(500);
             searchInput.find("input").simulate("change", { target: { value: "intermediate value" } });
@@ -95,4 +95,14 @@ describe("<SearchInput />", () => {
 
     });
 
+    describe("on input target change", () => {
+        it("clears the input value on selfClearProp change", () => {
+            const searchInput = mount(<SearchInput selfClearProp="abc" value="123" onSearch={onSearch} debounce={1000}/>);
+            searchInput.setProps({ selfClearProp: 'aaa' });
+            jest.runTimersToTime(1000);
+            expect(onSearch).toBeCalledWith("");
+            expect(onSearch).toHaveBeenCalledTimes(1);
+        });
+    });
+
 });
diff --git a/src/components/search-input/search-input.tsx b/src/components/search-input/search-input.tsx
index 5d5a9a22..50338f40 100644
--- a/src/components/search-input/search-input.tsx
+++ b/src/components/search-input/search-input.tsx
@@ -35,6 +35,7 @@ const styles: StyleRulesCallback<CssRules> = theme => {
 interface SearchInputDataProps {
     value: string;
     label?: string;
+    selfClearProp: string;
 }
 
 interface SearchInputActionProps {
@@ -47,6 +48,7 @@ type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyl
 interface SearchInputState {
     value: string;
     label: string;
+    selfClearProp: string;
 }
 
 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
@@ -55,7 +57,8 @@ export const SearchInput = withStyles(styles)(
     class extends React.Component<SearchInputProps> {
         state: SearchInputState = {
             value: "",
-            label: ""
+            label: "",
+            selfClearProp: ""
         };
 
         timeout: number;
@@ -66,6 +69,7 @@ export const SearchInput = withStyles(styles)(
                     <InputLabel>{this.state.label}</InputLabel>
                     <Input
                         type="text"
+                        data-cy="search-input"
                         value={this.state.value}
                         onChange={this.handleChange}
                         endAdornment={
@@ -93,6 +97,10 @@ export const SearchInput = withStyles(styles)(
             if (nextProps.value !== this.props.value) {
                 this.setState({ value: nextProps.value });
             }
+            if (this.state.value !== '' && nextProps.selfClearProp && nextProps.selfClearProp !== this.state.selfClearProp) {
+                this.props.onSearch('');
+                this.setState({ selfClearProp: nextProps.selfClearProp });
+            }
         }
 
         componentWillUnmount() {
diff --git a/src/views/run-process-panel/run-process-first-step.tsx b/src/views/run-process-panel/run-process-first-step.tsx
index 906d3a37..ed6d5640 100644
--- a/src/views/run-process-panel/run-process-first-step.tsx
+++ b/src/views/run-process-panel/run-process-first-step.tsx
@@ -59,7 +59,7 @@ export const RunProcessFirstStep = withStyles(styles)(
         <Grid container spacing={16}>
             <Grid container item xs={6} className={classes.root}>
                 <Grid item xs={12} className={classes.searchGrid}>
-                    <SearchInput value='' onSearch={onSearch} />
+                    <SearchInput selfClearProp={JSON.stringify(selectedWorkflow)} value='' onSearch={onSearch} />
                 </Grid>
                 <Grid item xs={12}>
                     <List className={classes.list}>

commit 94bf91c274d9aafd13b49ad6908e0d5a80612c9d
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Jan 28 16:52:21 2022 -0500

    Merge branch '18628-detailed-resource-owner-renderer' into main. Closes #18628
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/all-processes-panel/all-processes-panel.tsx b/src/views/all-processes-panel/all-processes-panel.tsx
index f9fab44d..fc0e54a8 100644
--- a/src/views/all-processes-panel/all-processes-panel.tsx
+++ b/src/views/all-processes-panel/all-processes-panel.tsx
@@ -16,7 +16,7 @@ import { ALL_PROCESSES_PANEL_ID } from 'store/all-processes-panel/all-processes-
 import {
     ProcessStatus,
     ResourceName,
-    ResourceOwner,
+    ResourceOwnerWithName,
     ResourceType,
     ContainerRunTime,
     ResourceCreatedAtDate
@@ -87,7 +87,7 @@ export const allProcessesPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwner uuid={uuid} />
+        render: uuid => <ResourceOwnerWithName uuid={uuid} />
     },
     {
         name: AllProcessesPanelColumnNames.CREATED_AT,
diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx
index 404baeb9..1a694650 100644
--- a/src/views/favorite-panel/favorite-panel.tsx
+++ b/src/views/favorite-panel/favorite-panel.tsx
@@ -18,7 +18,7 @@ import {
     ResourceFileSize,
     ResourceLastModifiedDate,
     ResourceName,
-    ResourceOwner,
+    ResourceOwnerWithName,
     ResourceType
 } from 'views-components/data-explorer/renderers';
 import { FavoriteIcon } from 'components/icon/icon';
@@ -94,7 +94,7 @@ export const favoritePanelColumns: DataColumns<string> = [
         selected: false,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwner uuid={uuid} />
+        render: uuid => <ResourceOwnerWithName uuid={uuid} />
     },
     {
         name: FavoritePanelColumnNames.FILE_SIZE,
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index eaf48d14..d74b42e0 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -20,7 +20,7 @@ import {
     ResourceLastModifiedDate,
     ProcessStatus,
     ResourceType,
-    ResourceOwner
+    ResourceOwnerWithName
 } from 'views-components/data-explorer/renderers';
 import { ProjectIcon } from 'components/icon/icon';
 import { ResourceName } from 'views-components/data-explorer/renderers';
@@ -102,7 +102,7 @@ export const projectPanelColumns: DataColumns<string> = [
         selected: false,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwner uuid={uuid} />
+        render: uuid => <ResourceOwnerWithName uuid={uuid} />
     },
     {
         name: ProjectPanelColumnNames.FILE_SIZE,
diff --git a/src/views/public-favorites-panel/public-favorites-panel.tsx b/src/views/public-favorites-panel/public-favorites-panel.tsx
index ee09654a..7e67b7ef 100644
--- a/src/views/public-favorites-panel/public-favorites-panel.tsx
+++ b/src/views/public-favorites-panel/public-favorites-panel.tsx
@@ -18,7 +18,7 @@ import {
     ResourceLastModifiedDate,
     ResourceType,
     ResourceName,
-    ResourceOwner
+    ResourceOwnerWithName
 } from 'views-components/data-explorer/renderers';
 import { PublicFavoriteIcon } from 'components/icon/icon';
 import { Dispatch } from 'redux';
@@ -92,7 +92,7 @@ export const publicFavoritePanelColumns: DataColumns<string> = [
         selected: false,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwner uuid={uuid} />
+        render: uuid => <ResourceOwnerWithName uuid={uuid} />
     },
     {
         name: PublicFavoritePanelColumnNames.FILE_SIZE,
diff --git a/src/views/search-results-panel/search-results-panel-view.tsx b/src/views/search-results-panel/search-results-panel-view.tsx
index 6f9cd2c4..5d6b580b 100644
--- a/src/views/search-results-panel/search-results-panel-view.tsx
+++ b/src/views/search-results-panel/search-results-panel-view.tsx
@@ -15,7 +15,7 @@ import {
     ResourceFileSize,
     ResourceLastModifiedDate,
     ResourceName,
-    ResourceOwner,
+    ResourceOwnerWithName,
     ResourceStatus,
     ResourceType
 } from 'views-components/data-explorer/renderers';
@@ -85,7 +85,7 @@ export const searchResultsPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwner uuid={uuid} />
+        render: uuid => <ResourceOwnerWithName uuid={uuid} />
     },
     {
         name: SearchResultsPanelColumnNames.FILE_SIZE,

commit d0e5901b7b11ac9c530b791778597868f70b04a7
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Fri Feb 18 19:01:55 2022 -0300

    Merge branch '18560-wb2-vocabulary-picking'. Closes #18560
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/create-workflow.spec.js b/cypress/integration/create-workflow.spec.js
index 4da74757..b1ea5dbf 100644
--- a/cypress/integration/create-workflow.spec.js
+++ b/cypress/integration/create-workflow.spec.js
@@ -64,9 +64,6 @@ describe('Multi-file deletion tests', function () {
         cy.get('@testWorkflow').then(() => {
             cy.loginAs(adminUser);
 
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
-
             cy.get('[data-cy=side-panel-button]').click();
             cy.get('[data-cy=side-panel-run-process]').click();
 
diff --git a/src/models/vocabulary.test.ts b/src/models/vocabulary.test.ts
index 18e2f19f..761c785b 100644
--- a/src/models/vocabulary.test.ts
+++ b/src/models/vocabulary.test.ts
@@ -18,7 +18,8 @@ describe('Vocabulary', () => {
                     strict: false,
                     labels: [
                         {label: "Animal" },
-                        {label: "Creature"}
+                        {label: "Creature"},
+                        {label: "Beast"},
                     ],
                     values: {
                         IDVALANIMALS1: {
@@ -39,13 +40,13 @@ describe('Vocabulary', () => {
                     labels: [{label: "Sizes"}],
                     values: {
                         IDVALSIZES1: {
-                            labels: [{label: "Small"}]
+                            labels: [{label: "Small"}, {label: "S"}, {label: "Little"}]
                         },
                         IDVALSIZES2: {
-                            labels: [{label: "Medium"}]
+                            labels: [{label: "Medium"}, {label: "M"}]
                         },
                         IDVALSIZES3: {
-                            labels: [{label: "Large"}]
+                            labels: [{label: "Large"}, {label: "L"}]
                         },
                         IDVALSIZES4: {
                             labels: []
@@ -61,23 +62,70 @@ describe('Vocabulary', () => {
         // Alphabetically ordered by label
         expect(tagKeys).toEqual([
             {id: "IDKEYANIMALS", label: "Animal"},
+            {id: "IDKEYANIMALS", label: "Beast"},
             {id: "IDKEYANIMALS", label: "Creature"},
             {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"},
             {id: "IDKEYSIZES", label: "Sizes"},
         ]);
     });
 
+    it('returns the list of preferred tag keys', () => {
+        const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary);
+        // Alphabetically ordered by label
+        expect(preferredTagKeys).toEqual([
+            {id: "IDKEYANIMALS", label: "Animal", synonyms: []},
+            {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []},
+            {id: "IDKEYSIZES", label: "Sizes", synonyms: []},
+        ]);
+    });
+
+    it('returns the list of preferred tag keys with matching synonyms', () => {
+        const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary, 'creat');
+        // Alphabetically ordered by label
+        expect(preferredTagKeys).toEqual([
+            {id: "IDKEYANIMALS", label: "Animal", synonyms: ["Creature"]},
+            {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []},
+            {id: "IDKEYSIZES", label: "Sizes", synonyms: []},
+        ]);
+    });
+
     it('returns the tag values for a given key', () => {
         const tagValues = Vocabulary.getTagValues('IDKEYSIZES', vocabulary);
         // Alphabetically ordered by label
         expect(tagValues).toEqual([
             {id: "IDVALSIZES4", label: "IDVALSIZES4"},
+            {id: "IDVALSIZES3", label: "L"},
             {id: "IDVALSIZES3", label: "Large"},
+            {id: "IDVALSIZES1", label: "Little"},
+            {id: "IDVALSIZES2", label: "M"},
             {id: "IDVALSIZES2", label: "Medium"},
+            {id: "IDVALSIZES1", label: "S"},
             {id: "IDVALSIZES1", label: "Small"},
         ])
     });
 
+    it('returns the preferred tag values for a given key', () => {
+        const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary);
+        // Alphabetically ordered by label
+        expect(preferredTagValues).toEqual([
+            {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []},
+            {id: "IDVALSIZES3", label: "Large", synonyms: []},
+            {id: "IDVALSIZES2", label: "Medium", synonyms: []},
+            {id: "IDVALSIZES1", label: "Small", synonyms: []},
+        ])
+    });
+
+    it('returns the preferred tag values with matching synonyms for a given key', () => {
+        const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary, 'litt');
+        // Alphabetically ordered by label
+        expect(preferredTagValues).toEqual([
+            {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []},
+            {id: "IDVALSIZES3", label: "Large", synonyms: []},
+            {id: "IDVALSIZES2", label: "Medium", synonyms: []},
+            {id: "IDVALSIZES1", label: "Small", synonyms: ["Little"]},
+        ])
+    });
+
     it('returns an empty list of values for an non-existent key', () => {
         const tagValues = Vocabulary.getTagValues('IDNONSENSE', vocabulary);
         expect(tagValues).toEqual([]);
diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts
index 3c542844..6c629059 100644
--- a/src/models/vocabulary.ts
+++ b/src/models/vocabulary.ts
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { escapeRegExp } from 'common/regexp';
 import { isObject, has, every } from 'lodash/fp';
 
 export interface Vocabulary {
@@ -27,6 +28,7 @@ export interface Tag {
 export interface PropFieldSuggestion {
     id: string;
     label: string;
+    synonyms?: string[];
 }
 
 const VOCABULARY_VALIDATORS = [
@@ -64,9 +66,9 @@ const compare = (a: PropFieldSuggestion, b: PropFieldSuggestion) => {
     return 0;
 };
 
-export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
+export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
     const tag = vocabulary.tags[tagKeyID];
-    const ret = tag && tag.values
+    return tag && tag.values
         ? Object.keys(tag.values).map(
             tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0
                 ? tag.values![tagValueID].labels.map(
@@ -75,11 +77,30 @@ export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
             .reduce((prev, curr) => [...prev, ...curr], [])
             .sort(compare)
         : [];
-    return ret;
 };
 
-export const getTags = ({ tags }: Vocabulary) => {
-    const ret = tags && Object.keys(tags)
+export const getPreferredTagValues = (tagKeyID: string, vocabulary: Vocabulary, withMatch?: string): PropFieldSuggestion[] => {
+    const tag = vocabulary.tags[tagKeyID];
+    const regex = !!withMatch ? new RegExp(escapeRegExp(withMatch), 'i') : undefined;
+    return tag && tag.values
+        ? Object.keys(tag.values).map(
+            tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0
+                ? {
+                    "id": tagValueID,
+                    "label": tag.values![tagValueID].labels[0].label,
+                    "synonyms": !!withMatch && tag.values![tagValueID].labels.length > 1
+                        ? tag.values![tagValueID].labels.slice(1)
+                            .filter(l => !!regex ? regex.test(l.label) : true)
+                            .map(l => l.label)
+                        : []
+                }
+                : {"id": tagValueID, "label": tagValueID, "synonyms": []})
+            .sort(compare)
+        : [];
+};
+
+export const getTags = ({ tags }: Vocabulary): PropFieldSuggestion[] => {
+    return tags && Object.keys(tags)
         ? Object.keys(tags).map(
             tagID => tags[tagID].labels && tags[tagID].labels.length > 0
                 ? tags[tagID].labels.map(
@@ -88,7 +109,25 @@ export const getTags = ({ tags }: Vocabulary) => {
             .reduce((prev, curr) => [...prev, ...curr], [])
             .sort(compare)
         : [];
-    return ret;
+};
+
+export const getPreferredTags = ({ tags }: Vocabulary, withMatch?: string): PropFieldSuggestion[] => {
+    const regex = !!withMatch ? new RegExp(escapeRegExp(withMatch), 'i') : undefined;
+    return tags && Object.keys(tags)
+        ? Object.keys(tags).map(
+            tagID => tags[tagID].labels && tags[tagID].labels.length > 0
+                ? {
+                    "id": tagID,
+                    "label": tags[tagID].labels[0].label,
+                    "synonyms": !!withMatch && tags[tagID].labels.length > 1
+                        ? tags[tagID].labels.slice(1)
+                                .filter(l => !!regex ? regex.test(l.label) : true)
+                                .map(lbl => lbl.label)
+                        : []
+                }
+                : {"id": tagID, "label": tagID, "synonyms": []})
+            .sort(compare)
+        : [];
 };
 
 export const getTagKeyID = (tagKeyLabel:string, vocabulary: Vocabulary) =>
diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx
index 791949f5..0be4527a 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -6,7 +6,14 @@ import React from 'react';
 import { WrappedFieldProps, Field, FormName, reset, change, WrappedFieldInputProps, WrappedFieldMetaProps } from 'redux-form';
 import { memoize } from 'lodash';
 import { Autocomplete } from 'components/autocomplete/autocomplete';
-import { Vocabulary, getTags, getTagKeyID, getTagKeyLabel } from 'models/vocabulary';
+import {
+    Vocabulary,
+    getTags,
+    getTagKeyID,
+    getTagKeyLabel,
+    getPreferredTags,
+    PropFieldSuggestion
+} from 'models/vocabulary';
 import {
     handleSelect,
     handleBlur,
@@ -36,8 +43,14 @@ export const PropertyKeyField = connectVocabulary(
 const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
     <FormName children={data => (
         <Autocomplete
+            {...buildProps(props)}
             label='Key'
             suggestions={getSuggestions(props.input.value, vocabulary)}
+            renderSuggestion={
+                (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0
+                    ? `${s.label} (${s.synonyms.join('; ')})`
+                    : s.label
+            }
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={() => {
                 // Case-insensitive search for the key in the vocabulary
@@ -51,7 +64,6 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
                 const newValue = e.currentTarget.value;
                 handleChange(data.form, props.input, props.meta, newValue);
             }}
-            {...buildProps(props)}
         />
     )} />;
 
@@ -67,9 +79,11 @@ const matchTags = (vocabulary: Vocabulary) =>
             ? undefined
             : 'Incorrect key';
 
-const getSuggestions = (value: string, vocabulary: Vocabulary) => {
+const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
     const re = new RegExp(escapeRegExp(value), "i");
-    return getTags(vocabulary).filter(tag => re.test(tag.label) && tag.label !== value);
+    return getPreferredTags(vocabulary, value).filter(
+        tag => (tag.label !== value && re.test(tag.label)) ||
+            (tag.synonyms && tag.synonyms.some(s => re.test(s))));
 };
 
 const handleChange = (
diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx
index b023e412..b8e525bf 100644
--- a/src/views-components/resource-properties-form/property-value-field.tsx
+++ b/src/views-components/resource-properties-form/property-value-field.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import { WrappedFieldProps, Field, formValues, FormName, WrappedFieldInputProps, WrappedFieldMetaProps, change } from 'redux-form';
 import { compose } from 'redux';
 import { Autocomplete } from 'components/autocomplete/autocomplete';
-import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel } from 'models/vocabulary';
+import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel, PropFieldSuggestion, getPreferredTagValues } from 'models/vocabulary';
 import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from 'views-components/resource-properties-form/property-key-field';
 import {
     handleSelect,
@@ -56,9 +56,15 @@ export const PropertyValueField = connectVocabularyAndPropertyKey(
 const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
     <FormName children={data => (
         <Autocomplete
+            {...buildProps(props)}
             label='Value'
             disabled={props.disabled}
             suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)}
+            renderSuggestion={
+                (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0
+                    ? `${s.label} (${s.synonyms.join('; ')})`
+                    : s.label
+            }
             onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={() => {
                 // Case-insensitive search for the value in the vocabulary
@@ -73,7 +79,6 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro
                 const tagValueID = getTagValueID(propertyKeyId, newValue, vocabulary);
                 handleChange(data.form, tagValueID, props.input, props.meta, newValue);
             }}
-            {...buildProps(props)}
         />
     )} />;
 
@@ -90,7 +95,9 @@ const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps)
 
 const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => {
     const re = new RegExp(escapeRegExp(value), "i");
-    return getTagValues(tagName, vocabulary).filter(v => re.test(v.label) && v.label !== value);
+    return getPreferredTagValues(tagName, vocabulary, value).filter(
+        val => (val.label !== value && re.test(val.label)) ||
+            (val.synonyms && val.synonyms.some(s => re.test(s))));
 };
 
 const handleChange = (

commit f4a6396d59ca5f39c260b10dd9c63247fac47791
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Wed Feb 16 17:36:47 2022 -0300

    Merge branch '18315-collection-panel-refresh'. Closes #18315
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index bd211b1a..f91dbb5b 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -642,6 +642,34 @@ describe('Collection panel tests', function () {
             });
     });
 
+    it('automatically updates the collection UI contents without using the Refresh button', function () {
+        const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
+        const fileName = 'foobar'
+
+        cy.createCollection(adminUser.token, {
+            name: collName,
+            owner_uuid: activeUser.user.uuid,
+        }).as('testCollection');
+
+        cy.getAll('@testCollection').then(function ([testCollection]) {
+            cy.loginAs(activeUser);
+            cy.goToPath(`/collections/${testCollection.uuid}`);
+            cy.get('[data-cy=collection-files-panel]').should('contain', 'This collection is empty');
+            cy.get('[data-cy=collection-files-panel]').should('not.contain', fileName);
+            cy.get('[data-cy=collection-info-panel]').should('contain', collName);
+
+            cy.updateCollection(adminUser.token, testCollection.uuid, {
+                name: `${collName + ' updated'}`,
+                manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
+            }).as('updatedCollection');
+            cy.getAll('@updatedCollection').then(function ([updatedCollection]) {
+                expect(updatedCollection.name).to.equal(`${collName + ' updated'}`);
+                cy.get('[data-cy=collection-info-panel]').should('contain', updatedCollection.name);
+                cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
+            });
+        });
+    })
+
     it('makes a copy of an existing collection', function() {
         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
         const copyName = `Copy of: ${collName}`;
@@ -848,17 +876,15 @@ describe('Collection panel tests', function () {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
             owner_uuid: activeUser.user.uuid,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-        })
-            .as('testCollection1');
+        }).as('testCollection1');
 
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
             owner_uuid: adminUser.user.uuid,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-        })
-            .as('testCollection2').then(function (testCollection2) {
-                cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
-            });
+        }).as('testCollection2').then(function (testCollection2) {
+            cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
+        });
 
         cy.getAll('@testCollection1', '@testCollection2')
             .then(function ([testCollection1, testCollection2]) {
@@ -880,8 +906,29 @@ describe('Collection panel tests', function () {
                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
                 owner_uuid: activeUser.user.uuid,
                 manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-            })
-                .as('testCollection1');
+            }).as('testCollection1');
+        });
+
+        it('uploads a file and checks the collection UI to be fresh', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+                    cy.get('[data-cy=upload-button]').click();
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('5mb_a.bin').should('not.exist');
+                    cy.get('[data-cy=collection-file-count]').should('contain', '1');
+                    cy.fixture('files/5mb.bin', 'base64').then(content => {
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+                        cy.get('[data-cy=form-submit-btn]').click();
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                    });
+                    // Confirm that the file browser has been updated.
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('5mb_a.bin').should('exist');
+                    // Confirm that the collection panel has been updated.
+                    cy.get('[data-cy=collection-file-count]').should('contain', '2');
+                });
         });
 
         it('allows to cancel running upload', () => {
diff --git a/cypress/integration/side-panel.spec.js b/cypress/integration/side-panel.spec.js
index f9d4dca3..78087386 100644
--- a/cypress/integration/side-panel.spec.js
+++ b/cypress/integration/side-panel.spec.js
@@ -82,7 +82,6 @@ describe('Side panel tests', function() {
             group_class: 'filter',
             properties: {filters: []},
         }).as('myFavoriteFilterGroup').then(function (myFavoriteFilterGroup) {
-            cy.contains('Refresh').click();
             cy.goToPath(`/projects/${myFavoriteFilterGroup.uuid}`);
             cy.get('[data-cy=breadcrumb-last]').should('contain', 'my-favorite-filter-group');
 
diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
index a7001a61..dd28d0fc 100644
--- a/src/components/collection-panel-files/collection-panel-files.tsx
+++ b/src/components/collection-panel-files/collection-panel-files.tsx
@@ -202,7 +202,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     const [path, setPath]: any = React.useState([]);
     const [pathData, setPathData]: any = React.useState({});
     const [isLoading, setIsLoading] = React.useState(false);
-    const [collectionAutofetchEnabled, setCollectionAutofetchEnabled] = React.useState(false);
     const [leftSearch, setLeftSearch] = React.useState('');
     const [rightSearch, setRightSearch] = React.useState('');
 
@@ -279,13 +278,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
         }
     }, [rightKey]); // eslint-disable-line react-hooks/exhaustive-deps
 
+    const currentPDH = (collectionPanel.item || {}).portableDataHash;
     React.useEffect(() => {
-        const hash = (collectionPanel.item || {}).portableDataHash;
-
-        if (hash && collectionAutofetchEnabled) {
+        if (currentPDH) {
             fetchData([leftKey, rightKey], true);
         }
-    }, [(collectionPanel.item || {}).portableDataHash]); // eslint-disable-line react-hooks/exhaustive-deps
+    }, [currentPDH]); // eslint-disable-line react-hooks/exhaustive-deps
 
     React.useEffect(() => {
         if (rightData) {
@@ -316,10 +314,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
 
             if (id) {
                 onItemMenuOpen(event, item, isWritable);
-
-                if (!collectionAutofetchEnabled) {
-                    setCollectionAutofetchEnabled(true);
-                }
             }
         },
         [onItemMenuOpen, isWritable, rightData] // eslint-disable-line react-hooks/exhaustive-deps
@@ -446,9 +440,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                     <IconButton
                         data-cy='collection-files-panel-options-btn'
                         onClick={(ev) => {
-                            if (!collectionAutofetchEnabled) {
-                                setCollectionAutofetchEnabled(true);
-                            }
                             onOptionsMenuOpen(ev, isWritable);
                         }}>
                         <CustomizeTableIcon />
@@ -518,9 +509,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                             className={classes.uploadButton}
                             data-cy='upload-button'
                             onClick={() => {
-                                if (!collectionAutofetchEnabled) {
-                                    setCollectionAutofetchEnabled(true);
-                                }
                                 onUploadDataClick();
                             }}
                             variant='contained'
@@ -568,7 +556,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                                                     </div>
                                                 }
                                             }
-                                        </FixedSizeList> : <div className={classes.rowEmpty}>No data available</div>
+                                        </FixedSizeList> : <div className={classes.rowEmpty}>This collection is empty</div>
                                     }}
                                 </AutoSizer> : <div className={classes.row}><CircularProgress className={classes.loader} size={30} /></div>
                         }
diff --git a/src/index.tsx b/src/index.tsx
index 6ad22a55..b75b325b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -181,7 +181,7 @@ const initListener = (history: History, store: RootStore, services: ServiceRepos
     let initialized = false;
     return async () => {
         const { router, auth } = store.getState();
-        if (router.location && auth.user && !initialized) {
+        if (router.location && auth.user && services.authService.getApiToken() && !initialized) {
             initialized = true;
             initWebSocket(config, services.authService, store);
             await store.dispatch(loadWorkbench());
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index e78b1f3d..4a3cb4d3 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -336,7 +336,7 @@ export const CollectionDetailsAttributes = (props: { item: CollectionResource, t
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                label='Number of files' value={item.fileCount} />
+                label='Number of files' value={<span data-cy='collection-file-count'>{item.fileCount}</span>} />
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
diff --git a/src/websocket/websocket.ts b/src/websocket/websocket.ts
index b1265808..7c8e0171 100644
--- a/src/websocket/websocket.ts
+++ b/src/websocket/websocket.ts
@@ -15,6 +15,7 @@ import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-
 import { projectPanelActions } from "store/project-panel/project-panel-action";
 import { getProjectPanelCurrentUuid } from 'store/project-panel/project-panel-action';
 import { allProcessesPanelActions } from 'store/all-processes-panel/all-processes-panel-action';
+import { loadCollection } from 'store/workbench/workbench-actions';
 
 export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
     if (config.websocketUrl) {
@@ -29,6 +30,12 @@ export const initWebSocket = (config: Config, authService: AuthService, store: R
 const messageListener = (store: RootStore) => (message: ResourceEventMessage) => {
     if (message.eventType === LogEventType.CREATE || message.eventType === LogEventType.UPDATE) {
         switch (message.objectKind) {
+            case ResourceKind.COLLECTION:
+                const currentCollection = store.getState().collectionPanel.item;
+                if (currentCollection && currentCollection.uuid === message.objectUuid) {
+                    store.dispatch(loadCollection(message.objectUuid));
+                }
+                return;
             case ResourceKind.CONTAINER_REQUEST:
                 if (store.getState().processPanel.containerRequestUuid === message.objectUuid) {
                     store.dispatch(loadProcess(message.objectUuid));
diff --git a/tools/arvados_config.yml b/tools/arvados_config.yml
index 55dc8a02..b9bcfbe0 100644
--- a/tools/arvados_config.yml
+++ b/tools/arvados_config.yml
@@ -15,6 +15,8 @@ Clusters:
       ForwardSlashNameSubstitution: /
       ManagedProperties:
         original_owner_uuid: {Function: original_owner, Protected: true}
+      WebDAVCache:
+        UUIDTTL: 0s
     Login:
       PAM:
         Enable: true

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list