Merge branch '18116-fix-chips-bugs' into main. Closes #18116
[arvados-workbench2.git] / src / components / chips / chips.tsx
index c013080045aa05f62500cc9a74383dcf93736299..eb68ed7a257942a6fb554f80b544089d93fcaa43 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-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 React from 'react';
+import { Chip, Grid, StyleRulesCallback, withStyles } from '@material-ui/core';
+import {
+    DragSource,
+    DragSourceSpec,
+    DragSourceCollector,
+    ConnectDragSource,
+    DropTarget,
+    DropTargetSpec,
+    DropTargetCollector,
+    ConnectDropTarget
+} from 'react-dnd';
 import { compose } from 'lodash/fp';
-interface ChipsFieldProps<Value> {
+import { WithStyles } from '@material-ui/core/styles';
+interface ChipsProps<Value> {
     values: Value[];
     getLabel?: (value: Value) => string;
+    filler?: React.ReactNode;
+    deletable?: boolean;
+    orderable?: boolean;
     onChange: (value: Value[]) => void;
+    clickable?: boolean;
 }
-export class Chips<Value> extends React.Component<ChipsFieldProps<Value>> {
-    render() {
-        const { values } = this.props;
-        return <DragDropContextProvider backend={HTML5Backend}>
-            <Grid container spacing={8}>
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = ({ spacing }) => ({
+    root: {
+        margin: `0px -${spacing.unit / 2}px`,
+    },
+});
+export const Chips = withStyles(styles)(
+    class Chips<Value> extends React.Component<ChipsProps<Value> & WithStyles<CssRules>> {
+        render() {
+            const { values, filler } = this.props;
+            return <Grid container spacing={8} className={this.props.classes.root}>
                 {values.map(this.renderChip)}
+                {filler && <Grid item xs>{filler}</Grid>}
+            </Grid>;
+        }
+
+        renderChip = (value: Value, index: number) => {
+            const { deletable, getLabel } = this.props;
+            return <Grid item key={index}>
+                <Chip onDelete={deletable ? this.deleteValue(value) : undefined}
+                    label={getLabel !== undefined ? getLabel(value) : value} />
             </Grid>
-        </DragDropContextProvider>;
-    }
+        }
 
-    renderChip = (value: Value, index: number) =>
-        <Grid item key={index}>
-            <this.chip {...{ value }} />
-        </Grid>
+        type = 'chip';
 
-    type = 'chip';
+        dragSpec: DragSourceSpec<DraggableChipProps<Value>, { value: Value }> = {
+            beginDrag: ({ value }) => ({ value }),
+            endDrag: ({ value: dragValue }, monitor) => {
+                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);
+                }
+            }
+        };
 
-    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);
-        }
-    };
+        dragCollector: DragSourceCollector<{}> = connect => ({
+            connectDragSource: connect.dragSource(),
+        })
 
-    dragCollector: DragSourceCollector<{}> = connect => ({
-        connectDragSource: connect.dragSource(),
-    })
+        dropSpec: DropTargetSpec<DraggableChipProps<Value>> = {
+            drop: ({ value }) => ({ value }),
+        };
 
-    dropSpec: DropTargetSpec<DraggableChipProps<Value>> = {
-        drop: ({ value }) => ({ value }),
-    };
+        dropCollector: DropTargetCollector<{}> = (connect, monitor) => ({
+            connectDropTarget: connect.dropTarget(),
+            isOver: monitor.isOver(),
+        })
+        chip = compose(
+            DragSource(this.type, this.dragSpec, this.dragCollector),
+            DropTarget(this.type, this.dropSpec, this.dropCollector),
+        )(
+            ({ connectDragSource, connectDropTarget, isOver, value }: DraggableChipProps<Value> & CollectedProps) => {
+                const connect = compose(
+                    connectDragSource,
+                    connectDropTarget,
+                );
 
-    dropCollector: DropTargetCollector<{}> = (connect, monitor) => ({
-        connectDropTarget: connect.dropTarget(),
-        isOver: monitor.isOver(),
-    })
-    chip = compose(
-        DragSource(this.type, this.dragSpec, this.dragCollector),
-        DropTarget(this.type, this.dropSpec, this.dropCollector),
-    )(
-        ({ connectDragSource, connectDropTarget, isOver, value }: DraggableChipProps<Value> & CollectedProps) =>
-            compose(
-                connectDragSource,
-                connectDropTarget,
-            )(
-                <span>
-                    <Chip
-                        color={isOver ? 'primary' : 'default'}
-                        onDelete={this.deleteValue(value)}
-                        label={this.props.getLabel ? this.props.getLabel(value) : JSON.stringify(value)} />
-                </span>
-            )
-    );
+                const chip =
+                    <span>
+                        <Chip
+                            color={isOver ? 'primary' : 'default'}
+                            onDelete={this.props.deletable
+                                ? this.deleteValue(value)
+                                : undefined}
+                            clickable={this.props.clickable}
+                            label={this.props.getLabel ?
+                                this.props.getLabel(value)
+                                : typeof value === 'object'
+                                    ? JSON.stringify(value)
+                                    : value} />
+                    </span>;
 
-    deleteValue = (value: Value) => () => {
-        const { values } = this.props;
-        const index = values.indexOf(value);
-        const newValues = values.slice(0);
-        newValues.splice(index, 1);
-        this.props.onChange(newValues);
-    }
-}
+                return this.props.orderable
+                    ? connect(chip)
+                    : chip;
+            }
+        );
+
+        deleteValue = (value: Value) => () => {
+            const { values } = this.props;
+            const index = values.indexOf(value);
+            const newValues = values.slice(0);
+            newValues.splice(index, 1);
+            this.props.onChange(newValues);
+        }
+    });
 
 interface CollectedProps {
     connectDragSource: ConnectDragSource;
@@ -90,4 +135,4 @@ interface CollectedProps {
 
 interface DraggableChipProps<Value> {
     value: Value;
-}
\ No newline at end of file
+}