Merge branch '18874-merge-wb2'
[arvados.git] / services / workbench2 / src / components / chips / chips.tsx
diff --git a/services/workbench2/src/components/chips/chips.tsx b/services/workbench2/src/components/chips/chips.tsx
new file mode 100644 (file)
index 0000000..c4724d1
--- /dev/null
@@ -0,0 +1,138 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+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';
+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;
+}
+
+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 && 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>
+        }
+
+        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);
+                }
+            }
+        };
+
+        dragCollector: DragSourceCollector<{}> = connect => ({
+            connectDragSource: connect.dragSource(),
+        })
+
+        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,
+                );
+
+                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>;
+
+                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;
+    connectDropTarget: ConnectDropTarget;
+
+    isOver: boolean;
+}
+
+interface DraggableChipProps<Value> {
+    value: Value;
+}