Merge branch '17579-Clear-table-filter-when-changing-the-project' into main
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Thu, 16 Dec 2021 16:11:03 +0000 (17:11 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Thu, 16 Dec 2021 16:11:13 +0000 (17:11 +0100)
closes #17579

Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

cypress/integration/collection.spec.js
cypress/integration/project.spec.js
src/components/collection-panel-files/collection-panel-files.tsx
src/components/collection-panel-files/collection-panel-files2.test.tsx [deleted file]
src/components/collection-panel-files/collection-panel-files2.tsx [deleted file]
src/components/data-explorer/data-explorer.tsx
src/components/search-input/search-input.test.tsx
src/components/search-input/search-input.tsx
src/views/run-process-panel/run-process-first-step.tsx

index bd211b1a7198a060122f66a1ba6278b2df6b5e63..82a26cef6f325993a199643934a26ab7b2d0b43e 100644 (file)
@@ -945,7 +945,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');
                     });
index 1c1759523ea0243113d0dd41fcbecbcfe636b257..b3d6bbed83b657ab3990c0aeb435aa1a34b8e67a 100644 (file)
@@ -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
index e33b7df0dc438100ac7e2f2f5eaaf75e840f9e6a..1ef6b5c94cdf117ea52a932c47a085957ce6c2d2 100644 (file)
@@ -462,7 +462,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}>
                         {
@@ -509,7 +509,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 (file)
index 4d8b815..0000000
+++ /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 (file)
index 4118248..0000000
+++ /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);
index d3d26708dfe7124441752cebf5c12ee2f0b4dca8..05125f12c7311b8a4fe700a7dd61923ce6e682c8 100644 (file)
@@ -93,11 +93,13 @@ type DataExplorerProps<T> = DataExplorerDataProps<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,
@@ -107,6 +109,7 @@ export const DataExplorer = withStyles(styles)(
                 paperKey, fetchMode, currentItemUuid, title,
                 doHidePanel, doMaximizePanel, panelName, panelMaximized
             } = this.props;
+
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
                 {title && <Grid item xs className={classes.title}>{title}</Grid>}
@@ -116,6 +119,7 @@ export const DataExplorer = withStyles(styles)(
                             {!hideSearchInput && <SearchInput
                                 label={searchLabel}
                                 value={searchValue}
+                                selfClearProp={currentItemUuid}
                                 onSearch={onSearch} />}
                         </div>
                         {actions}
index 90c52b7639e2d534d594bb1a6252c22b782109ff..c57d3608b06b470e260ed2ceabf76875290f3019 100644 (file)
@@ -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);
+        });
+    });
+
 });
index 5d5a9a226009c5cea5e7c2893ab5a9a85724f067..50338f401c9387b680ef417a715e2130d932b265 100644 (file)
@@ -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() {
index 906d3a37b652e06426090c946d5c4d42cc3525e7..ed6d5640ed44c068879222e7d72a5683c6645d14 100644 (file)
@@ -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}>