Create chips input
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 12 Oct 2018 12:05:07 +0000 (14:05 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 12 Oct 2018 12:05:07 +0000 (14:05 +0200)
Feature #14229

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

src/components/chips-input/chips-input.tsx [new file with mode: 0644]
src/components/chips/chips.tsx
src/index.tsx
src/views/workbench/workbench.tsx

diff --git a/src/components/chips-input/chips-input.tsx b/src/components/chips-input/chips-input.tsx
new file mode 100644 (file)
index 0000000..c35db1b
--- /dev/null
@@ -0,0 +1,85 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Chips } from '~/components/chips/chips';
+import { Input } from '@material-ui/core';
+
+interface ChipsInputProps<Value> {
+    values: Value[];
+    getLabel?: (value: Value) => string;
+    onChange: (value: Value[]) => void;
+    createNewValue: (value: string) => Value;
+}
+
+export class ChipsInput<Value> extends React.Component<ChipsInputProps<Value>> {
+
+    state = {
+        text: '',
+    };
+
+    filler = React.createRef<HTMLDivElement>();
+    timeout = -1;
+
+    setText = (event: React.ChangeEvent<HTMLInputElement>) => {
+        this.setState({ text: event.target.value });
+    }
+
+    handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
+        if (key === 'Enter') {
+            this.createNewValue();
+        } else if (key === 'Backspace') {
+            this.deleteLastValue();
+        }
+    }
+
+    createNewValue = () => {
+        if (this.state.text) {
+            const newValue = this.props.createNewValue(this.state.text);
+            this.setState({ text: '' });
+            this.props.onChange([...this.props.values, newValue]);
+        }
+    }
+
+    deleteLastValue = () => {
+        if (this.state.text.length === 0 && this.props.values.length > 0) {
+            this.props.onChange(this.props.values.slice(0, -1));
+        }
+    }
+
+    updateStyles = () => {
+        if(this.timeout){
+            clearTimeout(this.timeout);
+        }
+        this.timeout = setTimeout(() => this.forceUpdate());
+    }
+
+    render() {
+        this.updateStyles();
+        return <>
+            <div style={{ minHeight: '40px', zIndex: 1, position: 'relative' }}>
+                <Chips {...this.props} filler={<div ref={this.filler} />} />
+            </div>
+            <Input
+                value={this.state.text}
+                onChange={this.setText}
+                onKeyDown={this.handleKeyPress}
+                style={{ top: '-24px' }}
+                inputProps={{ style: this.getInputStyles(), }}
+                fullWidth />
+        </>;
+    }
+
+    getInputStyles = (): React.CSSProperties => ({
+        width: this.filler.current
+            ? this.filler.current.offsetWidth + 8
+            : '100%',
+        position: 'relative',
+        right: this.filler.current
+            ? `calc(${this.filler.current.offsetWidth}px - 100%)`
+            : 0,
+        top: '-5px',
+        zIndex: 1,
+    })
+}
\ No newline at end of file
index c013080045aa05f62500cc9a74383dcf93736299..c63b584a100ff2de0088057ced2c99d271a38925 100644 (file)
@@ -4,22 +4,21 @@
 
 import * as React from 'react';
 import { Chip, Grid } from '@material-ui/core';
-import { DragSource, DragSourceSpec, DragSourceCollector, ConnectDragSource, DragDropContextProvider, DropTarget, DropTargetSpec, DropTargetCollector, ConnectDropTarget } from 'react-dnd';
-import HTML5Backend from 'react-dnd-html5-backend';
+import { DragSource, DragSourceSpec, DragSourceCollector, ConnectDragSource, DropTarget, DropTargetSpec, DropTargetCollector, ConnectDropTarget } from 'react-dnd';
 import { compose } from 'lodash/fp';
-interface ChipsFieldProps<Value> {
+interface ChipsProps<Value> {
     values: Value[];
     getLabel?: (value: Value) => string;
+    filler?: React.ReactNode;
     onChange: (value: Value[]) => void;
 }
-export class Chips<Value> extends React.Component<ChipsFieldProps<Value>> {
+export class Chips<Value> extends React.Component<ChipsProps<Value>> {
     render() {
-        const { values } = this.props;
-        return <DragDropContextProvider backend={HTML5Backend}>
-            <Grid container spacing={8}>
-                {values.map(this.renderChip)}
-            </Grid>
-        </DragDropContextProvider>;
+        const { values, filler } = this.props;
+        return <Grid container spacing={8}>
+            {values.map(this.renderChip)}
+            {filler && <Grid item xs>{filler}</Grid>}
+        </Grid>;
     }
 
     renderChip = (value: Value, index: number) =>
@@ -32,13 +31,21 @@ export class Chips<Value> extends React.Component<ChipsFieldProps<Value>> {
     dragSpec: DragSourceSpec<DraggableChipProps<Value>, { value: Value }> = {
         beginDrag: ({ value }) => ({ value }),
         endDrag: ({ value: dragValue }, monitor) => {
-            const { value: dropValue } = monitor.getDropResult();
-            const dragIndex = this.props.values.indexOf(dragValue);
-            const dropIndex = this.props.values.indexOf(dropValue);
-            const newValues = this.props.values.slice(0);
-            newValues.splice(dragIndex, 1, dropValue);
-            newValues.splice(dropIndex, 1, dragValue);
-            this.props.onChange(newValues);
+            const result = monitor.getDropResult();
+            if (result) {
+                const { value: dropValue } = monitor.getDropResult();
+                const dragIndex = this.props.values.indexOf(dragValue);
+                const dropIndex = this.props.values.indexOf(dropValue);
+                const newValues = this.props.values.slice(0);
+                if (dragIndex < dropIndex) {
+                    newValues.splice(dragIndex, 1);
+                    newValues.splice(dropIndex - 1 || 0, 0, dragValue);
+                } else if (dragIndex > dropIndex) {
+                    newValues.splice(dragIndex, 1);
+                    newValues.splice(dropIndex, 0, dragValue);
+                }
+                this.props.onChange(newValues);
+            }
         }
     };
 
@@ -67,7 +74,11 @@ export class Chips<Value> extends React.Component<ChipsFieldProps<Value>> {
                     <Chip
                         color={isOver ? 'primary' : 'default'}
                         onDelete={this.deleteValue(value)}
-                        label={this.props.getLabel ? this.props.getLabel(value) : JSON.stringify(value)} />
+                        label={this.props.getLabel ?
+                            this.props.getLabel(value)
+                            : typeof value === 'object'
+                                ? JSON.stringify(value)
+                                : value} />
                 </span>
             )
     );
index 1d072d9d5d9b531e7108b9509f5faa4057d0b228..2b5c381b6f92e3d2b7bfda06714f3afca294b448 100644 (file)
@@ -42,9 +42,10 @@ import { setUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
 import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
 import { ContainerRequestState } from '~/models/container-request';
 import { MountKind } from '~/models/mount-types';
-import { initProjectsTreePicker } from './store/tree-picker/tree-picker-actions';
 import { setBuildInfo } from '~/store/app-info/app-info-actions';
 import { getBuildInfo } from '~/common/app-info';
+import { DragDropContextProvider } from 'react-dnd';
+import HTML5Backend from 'react-dnd-html5-backend';
 
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
@@ -86,14 +87,16 @@ fetchConfig()
 
         const App = () =>
             <MuiThemeProvider theme={CustomTheme}>
-                <Provider store={store}>
-                    <ConnectedRouter history={history}>
-                        <div>
-                            <Route path={Routes.TOKEN} component={TokenComponent} />
-                            <Route path={Routes.ROOT} component={MainPanelComponent} />
-                        </div>
-                    </ConnectedRouter>
-                </Provider>
+                <DragDropContextProvider backend={HTML5Backend}>
+                    <Provider store={store}>
+                        <ConnectedRouter history={history}>
+                            <div>
+                                <Route path={Routes.TOKEN} component={TokenComponent} />
+                                <Route path={Routes.ROOT} component={MainPanelComponent} />
+                            </div>
+                        </ConnectedRouter>
+                    </Provider>
+                </DragDropContextProvider>
             </MuiThemeProvider>;
 
         ReactDOM.render(
index f26d50fba6bc330ef42cccc8b3481b80007ea0be..02bf4c9d5719f9c92a07585cfbef57fba8843866 100644 (file)
@@ -45,6 +45,8 @@ import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tre
 import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker';
 import { FavoritesTreePicker } from '../../views-components/projects-tree-picker/favorites-tree-picker';
 import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { Chips } from '~/components/chips/chips';
+import { ChipsInput } from '../../components/chips-input/chips-input';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -78,6 +80,22 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 type WorkbenchPanelProps = WithStyles<CssRules>;
 
+class TestInput extends React.Component {
+    state = {
+        values: ['asd']
+    };
+
+    handleChange = (values: string[]) => {
+        this.setState({ values });
+    }
+    render() {
+        return <ChipsInput
+            onChange={this.handleChange}
+            createNewValue={v => v}
+            values={this.state.values} />;
+    }
+}
+
 export const WorkbenchPanel =
     withStyles(styles)(({ classes }: WorkbenchPanelProps) =>
         <Grid container item xs className={classes.root}>
@@ -92,6 +110,7 @@ export const WorkbenchPanel =
                             <MainContentBar />
                         </Grid>
                         <Grid item xs className={classes.content}>
+                            <TestInput />
                             <Switch>
                                 <Route path={Routes.PROJECTS} component={ProjectPanel} />
                                 <Route path={Routes.COLLECTIONS} component={CollectionPanel} />