"@types/js-yaml": "3.11.2",
"@types/lodash": "4.14.116",
"@types/react-copy-to-clipboard": "4.2.6",
+ "@types/react-dnd": "3.0.2",
"@types/react-dropzone": "4.2.2",
"@types/react-highlight-words": "0.12.0",
"@types/redux-form": "7.4.5",
+ "@types/reselect": "2.2.0",
"@types/shell-quote": "1.6.0",
"axios": "0.18.0",
"classnames": "2.2.6",
"lodash": "4.17.11",
"react": "16.5.2",
"react-copy-to-clipboard": "5.0.1",
+ "react-dnd": "5.0.0",
+ "react-dnd-html5-backend": "5.0.1",
"react-dom": "16.5.2",
"react-dropzone": "5.1.1",
"react-highlight-words": "0.14.0",
--- /dev/null
+// 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 as MuiInput, withStyles, WithStyles } from '@material-ui/core';
+import { StyleRulesCallback } from '@material-ui/core/styles';
+import { InputProps } from '@material-ui/core/Input';
+
+interface ChipsInputProps<Value> {
+ value: Value[];
+ getLabel?: (value: Value) => string;
+ onChange: (value: Value[]) => void;
+ createNewValue: (value: string) => Value;
+ inputComponent?: React.ComponentType<InputProps>;
+ inputProps?: InputProps;
+ deletable?: boolean;
+ orderable?: boolean;
+}
+
+type CssRules = 'chips' | 'input' | 'inputContainer';
+
+const styles: StyleRulesCallback = ({ spacing }) => ({
+ chips: {
+ minHeight: spacing.unit * 5,
+ zIndex: 1,
+ position: 'relative',
+ },
+ input: {
+ zIndex: 1,
+ marginBottom: 8,
+ position: 'relative',
+ },
+ inputContainer: {
+ marginTop: -34
+ },
+});
+
+export const ChipsInput = withStyles(styles)(
+ class ChipsInput<Value> extends React.Component<ChipsInputProps<Value> & WithStyles<CssRules>> {
+
+ 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.value, newValue]);
+ }
+ }
+
+ deleteLastValue = () => {
+ if (this.state.text.length === 0 && this.props.value.length > 0) {
+ this.props.onChange(this.props.value.slice(0, -1));
+ }
+ }
+
+ updateCursorPosition = () => {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.timeout = setTimeout(() => this.setState({ ...this.state }));
+ }
+
+ getInputStyles = (): React.CSSProperties => ({
+ width: this.filler.current
+ ? this.filler.current.offsetWidth
+ : '100%',
+ right: this.filler.current
+ ? `calc(${this.filler.current.offsetWidth}px - 100%)`
+ : 0,
+
+ })
+
+ componentDidMount() {
+ this.updateCursorPosition();
+ }
+
+ render() {
+ return <>
+ {this.renderChips()}
+ {this.renderInput()}
+ </>;
+ }
+
+ renderChips() {
+ const { classes, value, ...props } = this.props;
+ return <div className={classes.chips}>
+ <Chips
+ {...props}
+ values={value}
+ filler={<div ref={this.filler} />}
+ />
+ </div>;
+ }
+
+ renderInput() {
+ const { inputProps: InputProps, inputComponent: Input = MuiInput, classes } = this.props;
+ return <Input
+ {...InputProps}
+ value={this.state.text}
+ onChange={this.setText}
+ onKeyDown={this.handleKeyPress}
+ inputProps={{
+ ...(InputProps && InputProps.inputProps),
+ className: classes.input,
+ style: this.getInputStyles(),
+ }}
+ fullWidth
+ className={classes.inputContainer} />;
+ }
+
+ componentDidUpdate(prevProps: ChipsInputProps<Value>) {
+ if (prevProps.value !== this.props.value) {
+ this.updateCursorPosition();
+ }
+ }
+ componentWillUnmount() {
+ clearTimeout(this.timeout);
+ }
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as 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, noop } 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;
+}
+
+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) =>
+ <Grid item key={index}>
+ <this.chip {...{ 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}
+ 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;
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps } from 'redux-form';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { StyleRulesCallback, WithStyles, withStyles, FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
+
+type CssRules = 'formControl' | 'selectWrapper' | 'select' | 'option';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ formControl: {
+ width: '100%'
+ },
+ selectWrapper: {
+ backgroundColor: theme.palette.common.white,
+ '&:before': {
+ borderBottomColor: 'rgba(0, 0, 0, 0.42)'
+ },
+ '&:focus': {
+ outline: 'none'
+ }
+ },
+ select: {
+ fontSize: '0.875rem',
+ '&:focus': {
+ backgroundColor: 'rgba(0, 0, 0, 0.0)'
+ }
+ },
+ option: {
+ fontSize: '0.875rem',
+ backgroundColor: theme.palette.common.white,
+ height: '30px'
+ }
+});
+
+export const NativeSelectField = withStyles(styles)
+ ((props: WrappedFieldProps & WithStyles<CssRules> & { items: any[] }) =>
+ <FormControl className={props.classes.formControl}>
+ <Select className={props.classes.selectWrapper}
+ native
+ value={props.input.value}
+ onChange={props.input.onChange}
+ disabled={props.meta.submitting}
+ name={props.input.name}
+ inputProps={{
+ id: `id-${props.input.name}`,
+ className: props.classes.select
+ }}>
+ {props.items.map(item => (
+ <option key={item.key} value={item.key} className={props.classes.option}>
+ {item.value}
+ </option>
+ ))}
+ </Select>
+ </FormControl>
+ );
\ No newline at end of file
placeholder={this.props.label} />;
}
}
-);
\ No newline at end of file
+);
+
+type DateTextFieldProps = WrappedFieldProps & WithStyles<CssRules>;
+
+export const DateTextField = withStyles(styles)
+ ((props: DateTextFieldProps) =>
+ <MaterialTextField
+ disabled={props.meta.submitting}
+ error={props.meta.touched && !!props.meta.error}
+ type="date"
+ fullWidth={true}
+ name={props.input.name}
+ InputLabelProps={{
+ shrink: true
+ }}
+ onChange={props.input.onChange}
+ value={props.input.value}
+ />
+ );
\ No newline at end of file
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()}]`);
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(
};
};
+const createDirectoriesArrayCollectorWorkflow = ({workflowService}: ServiceRepository) => {
+ workflowService.create({
+ name: 'Directories array collector',
+ description: 'Workflow for collecting directories array',
+ definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"multiple_collections\":\n $(inputs.multiple_collections)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: array\n items: Directory\n id: '#input_collector.cwl/multiple_collections'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: array\n items: Directory\n label: Multiple Collections\n doc: This should allow for selecting multiple collections.\n id: '#main/multiple_collections'\n default:\n - class: Directory\n location: keep:1e1682585d576f031b2d8b4944f989ee+57\n basename: 1e1682585d576f031b2d8b4944f989ee+57\n - class: Directory\n location: keep:326f692370e9e121fcbd013796f7352a+57\n basename: 326f692370e9e121fcbd013796f7352a+57\n \n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/multiple_collections'\n id: '#main/input_collector/multiple_collections'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
+ });
+};
+
+const createFilesArrayCollectorWorkflow = ({workflowService}: ServiceRepository) => {
+ workflowService.create({
+ name: 'Files array collector',
+ description: 'Workflow for collecting files array',
+ definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"multiple_files\":\n $(inputs.multiple_files)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: array\n items: File\n id: '#input_collector.cwl/multiple_files'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n\n baseCommand: [cat]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: array\n items: File\n label: Multiple Files\n doc: This should allow for selecting multiple files.\n id: '#main/multiple_files'\n default:\n - class: File\n location: keep:af831660d820bcbb98f473355e6e1b85+67/fileA\n basename: fileA\n nameroot: fileA\n nameext: ''\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/multiple_files'\n id: '#main/input_collector/multiple_files'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n",
+ });
+};
+
const createPrimitivesCollectorWorkflow = ({ workflowService }: ServiceRepository) => {
workflowService.create({
name: 'Primitive values collector',
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourceKind } from '~/models/resource';
+
+export interface SearchBarAdvanceFormData {
+ type?: ResourceKind;
+ cluster?: ClusterObjectType;
+ project?: string;
+ inTrash: boolean;
+ dateFrom: string;
+ dateTo: string;
+ saveQuery: boolean;
+ searchQuery: string;
+}
+
+export enum ClusterObjectType {
+ INDIANAPOLIS = "indianapolis",
+ KAISERAUGST = "kaiseraugst",
+ PENZBERG = "penzberg"
+}
\ No newline at end of file
};
-export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & {parent?: string}): TreeNode<T> => ({
+export const selectNode = (id: string) => <T>(tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node && node.selected
+ ? tree
+ : toggleNodeSelection(id)(tree);
+};
+
+export const selectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+ const ids = typeof id === 'string' ? [id] : id;
+ return ids.reduce((tree, id) => selectNode(id)(tree), tree);
+};
+export const deselectNode = (id: string) => <T>(tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node && node.selected
+ ? toggleNodeSelection(id)(tree)
+ : tree;
+};
+
+export const deselectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+ const ids = typeof id === 'string' ? [id] : id;
+ return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
+};
+
+export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { parent?: string }): TreeNode<T> => ({
children: [],
active: false,
selected: false,
? input.type.indexOf(type) > -1
: input.type === type;
+export const isArrayOfType = (input: GenericCommandInputParameter<any, any>, type: CWLType) =>
+ typeof input.type === 'object' &&
+ input.type.type === 'array'
+ ? input.type.items === type
+ : false;
+
export const stringifyInputType = ({ type }: CommandInputParameter) => {
if (typeof type === 'string') {
return type;
import { SearchView } from '~/store/search-bar/search-bar-reducer';
import { navigateToSearchResults, navigateTo } from '~/store/navigation/navigation-action';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
export const searchBarActions = unionize({
SET_CURRENT_VIEW: ofType<string>(),
export type SearchBarActions = UnionOf<typeof searchBarActions>;
-export interface SearchBarAdvanceFormData {
- type?: GroupContentsResource;
- cluster?: string;
- project?: string;
- inTrash: boolean;
- dataFrom: string;
- dataTo: string;
- saveQuery: boolean;
- searchQuery: string;
-}
-
export const SEARCH_BAR_ADVANCE_FORM_NAME = 'searchBarAdvanceFormName';
export const goToView = (currentView: string) => searchBarActions.SET_CURRENT_VIEW(currentView);
dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
};
-export const closeSearchView = () =>
+export const closeSearchView = () =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
- dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
+ const isOpen = getState().searchBar.open;
+ if(isOpen) {
+ dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
+ dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
+ }
};
+
export const navigateToItem = (uuid: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { FilterBuilder } from '~/services/api/filter-builder';
-import { pipe } from 'lodash/fp';
+import { pipe, map, values, mapValues } from 'lodash/fp';
import { ResourceKind } from '~/models/resource';
import { GroupContentsResource } from '../../services/groups-service/groups-service';
import { CollectionDirectory, CollectionFile } from '../../models/collection-file';
-import { getTreePicker } from './tree-picker';
+import { getTreePicker, TreePicker } from './tree-picker';
import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
export const treePickerActions = unionize({
ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
+ SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
+ DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
RESET_TREE_PICKER: ofType<{ pickerId: string }>()
});
shared: `${pickerId}_shared`,
favorites: `${pickerId}_favorites`,
});
+
+export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
+ pipe(
+ () => values(getProjectsTreePickerIds(pickerId)),
+
+ ids => ids
+ .map(id => getTreePicker<Value>(id)(state)),
+
+ trees => trees
+ .map(getNodeDescendants(''))
+ .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
+
+ allNodes => allNodes
+ .reduce((map, node) =>
+ filter(node)
+ ? map.set(node.id, node)
+ : map, new Map<string, TreeNode<Value>>())
+ .values(),
+
+ uniqueNodes => Array.from(uniqueNodes),
+ )();
+export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
+ getAllNodes<Value>(pickerId, node => node.selected)(state);
+
export const initProjectsTreePicker = (pickerId: string) =>
async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode } from '~/models/tree';
+import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode, deselectNode, selectNode, selectNodes, deselectNodes } from '~/models/tree';
import { TreePicker } from "./tree-picker";
import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
import { compose } from "redux";
updateOrCreatePicker(state, pickerId, deactivateNode),
TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId }) =>
updateOrCreatePicker(state, pickerId, toggleNodeSelection(id)),
+ SELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, selectNodes(id)),
+ DESELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, deselectNodes(id)),
RESET_TREE_PICKER: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, createTree),
EXPAND_TREE_PICKER_NODES: ({ pickerId, ids }) =>
import * as React from "react";
import { Field } from 'redux-form';
-import { TextField } from "~/components/text-field/text-field";
+import { TextField, DateTextField } from "~/components/text-field/text-field";
import { CheckboxField } from '~/components/checkbox-field/checkbox-field';
+import { NativeSelectField } from '~/components/select-field/select-field';
+import { ResourceKind } from '~/models/resource';
+import { ClusterObjectType } from '~/models/search-bar';
export const SearchBarTypeField = () =>
<Field
name='type'
- component={TextField}
- label="Type"/>;
+ component={NativeSelectField}
+ items={[
+ { key: '', value: 'Any'},
+ { key: ResourceKind.COLLECTION, value: 'Collection'},
+ { key: ResourceKind.PROJECT, value: 'Project' },
+ { key: ResourceKind.PROCESS, value: 'Process' }
+ ]}/>;
export const SearchBarClusterField = () =>
<Field
name='cluster'
- component={TextField}
- label="Cluster name" />;
+ component={NativeSelectField}
+ items={[
+ { key: '', value: 'Any' },
+ { key: ClusterObjectType.INDIANAPOLIS, value: 'Indianapolis' },
+ { key: ClusterObjectType.KAISERAUGST, value: 'Kaiseraugst' },
+ { key: ClusterObjectType.PENZBERG, value: 'Penzberg' }
+ ]} />;
export const SearchBarProjectField = () =>
- <Field
- name='project'
- component={TextField}
- label="Project name" />;
+ <div>Box</div>;
export const SearchBarTrashField = () =>
<Field
export const SearchBarDataFromField = () =>
<Field
- name='dataFrom'
- component={TextField}
- label="From" />;
+ name='dateFrom'
+ component={DateTextField} />;
export const SearchBarDataToField = () =>
<Field
- name='dataTo'
- component={TextField}
- label="To" />;
+ name='dateTo'
+ component={DateTextField} />;
export const SearchBarKeyField = () =>
<Field
import { compose, Dispatch } from 'redux';
import { Paper, StyleRulesCallback, withStyles, WithStyles, Button, Grid, IconButton, CircularProgress } from '@material-ui/core';
import { SearchView } from '~/store/search-bar/search-bar-reducer';
-import { SEARCH_BAR_ADVANCE_FORM_NAME, SearchBarAdvanceFormData, saveQuery } from '~/store/search-bar/search-bar-actions';
+import { SEARCH_BAR_ADVANCE_FORM_NAME, saveQuery } from '~/store/search-bar/search-bar-actions';
import { ArvadosTheme } from '~/common/custom-theme';
import { CloseIcon } from '~/components/icon/icon';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
import {
SearchBarTypeField, SearchBarClusterField, SearchBarProjectField, SearchBarTrashField,
SearchBarDataFromField, SearchBarDataToField, SearchBarKeyField, SearchBarValueField,
SearchBarSaveSearchField, SearchBarQuerySearchField
} from '~/views-components/form-fields/search-bar-form-fields';
-type CssRules = 'form' | 'container' | 'closeIcon' | 'label' | 'buttonWrapper' | 'button' | 'circularProgress' | 'searchView';
+type CssRules = 'container' | 'closeIcon' | 'label' | 'buttonWrapper'
+ | 'button' | 'circularProgress' | 'searchView' | 'selectGrid';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- form: {
-
- },
container: {
- padding: theme.spacing.unit * 3,
+ padding: theme.spacing.unit * 2,
borderBottom: `1px solid ${theme.palette.grey["200"]}`
},
closeIcon: {
alignSelf: 'center'
},
buttonWrapper: {
+ paddingRight: '14px',
paddingTop: '14px',
position: 'relative',
},
searchView: {
color: theme.palette.common.black,
borderRadius: `0 0 ${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px`
+ },
+ selectGrid: {
+ marginBottom: theme.spacing.unit * 2
}
});
withStyles(styles))(
({ classes, setView, handleSubmit, invalid, submitting, pristine }: SearchBarAdvancedViewProps) =>
<Paper className={classes.searchView}>
- <form onSubmit={handleSubmit} className={classes.form}>
+ <form onSubmit={handleSubmit}>
<Grid container direction="column" justify="flex-start" alignItems="flex-start">
<Grid item xs={12} container className={classes.container}>
- <Grid item container xs={12}>
+ <Grid item container xs={12} className={classes.selectGrid}>
<Grid item xs={2} className={classes.label}>Type</Grid>
<Grid item xs={5}>
<SearchBarTypeField />
</Grid>
</Grid>
- <Grid item container xs={12}>
+ <Grid item container xs={12} className={classes.selectGrid}>
<Grid item xs={2} className={classes.label}>Cluster</Grid>
<Grid item xs={5}>
<SearchBarClusterField />
<CloseIcon />
</IconButton>
</Grid>
- <Grid container item xs={12} className={classes.container}>
+ <Grid container item xs={12} className={classes.container} spacing={16}>
<Grid item xs={2} className={classes.label}>Data modified</Grid>
- <Grid item xs={3}>
+ <Grid item xs={4}>
<SearchBarDataFromField />
</Grid>
- <Grid item xs={3}>
+ <Grid item xs={4}>
<SearchBarDataToField />
</Grid>
</Grid>
<Grid container item xs={12} className={classes.container}>
- <Grid container item xs={12}>
+ <Grid container item xs={12} spacing={16}>
<Grid item xs={2} className={classes.label}>Properties</Grid>
<Grid item xs={4}>
<SearchBarKeyField />
</Button>
</Grid>
</Grid>
- <Grid container item xs={12} justify="flex-start" alignItems="center">
+ <Grid container item xs={12} justify="flex-start" alignItems="center" spacing={16}>
<Grid item xs={2} className={classes.label} />
<Grid item xs={4}>
<SearchBarSaveSearchField />
import { SearchBarAdvancedView } from '~/views-components/search-bar/search-bar-advanced-view';
import { SearchBarAutocompleteView, SearchBarAutocompleteViewDataProps } from '~/views-components/search-bar/search-bar-autocomplete-view';
import { ArvadosTheme } from '~/common/custom-theme';
-import { SearchBarAdvanceFormData } from '~/store/search-bar/search-bar-actions';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
saveRecentQuery,
loadRecentQueries,
saveQuery,
- openSearchView
+ openSearchView,
+ closeSearchView,
+ navigateToItem
} from '~/store/search-bar/search-bar-actions';
import { SearchBarView } from '~/views-components/search-bar/search-bar-view';
-import { SearchBarAdvanceFormData } from '~/store/search-bar/search-bar-actions';
-import { closeSearchView, navigateToItem } from '~/store/search-bar/search-bar-actions';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
const mapStateToProps = ({ searchBar }: RootState) => {
return {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ isRequiredInput,
+ DirectoryArrayCommandInputParameter,
+ Directory,
+ CWLType
+} from '~/models/workflow';
+import { Field } from 'redux-form';
+import { ERROR_MESSAGE } from '~/validators/require';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, Grid, WithStyles, Typography } from '@material-ui/core';
+import { GenericInputProps, GenericInput } from './generic-input';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { connect, DispatchProp } from 'react-redux';
+import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, getAllNodes } from '~/store/tree-picker/tree-picker-actions';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { createSelector, createStructuredSelector } from 'reselect';
+import { ChipsInput } from '~/components/chips-input/chips-input';
+import { identity, values, noop } from 'lodash';
+import { InputProps } from '@material-ui/core/Input';
+import { TreePicker } from '~/store/tree-picker/tree-picker';
+import { RootState } from '~/store/store';
+import { Chips } from '~/components/chips/chips';
+import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
+import { CollectionResource } from '~/models/collection';
+import { ResourceKind } from '~/models/resource';
+
+export interface DirectoryArrayInputProps {
+ input: DirectoryArrayCommandInputParameter;
+}
+
+export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) =>
+ <Field
+ name={input.id}
+ commandInput={input}
+ component={DirectoryArrayInputComponent}
+ parse={parseDirectories}
+ format={formatDirectories}
+ validate={validationSelector(input)} />;
+
+interface FormattedDirectory {
+ name: string;
+ portableDataHash: string;
+}
+
+const parseDirectories = (directories: CollectionResource[] | string) =>
+ typeof directories === 'string'
+ ? undefined
+ : directories.map(parse);
+
+const parse = (directory: CollectionResource): Directory => ({
+ class: CWLType.DIRECTORY,
+ basename: directory.name,
+ location: `keep:${directory.portableDataHash}`,
+});
+
+const formatDirectories = (directories: Directory[] = []) =>
+ directories.map(format);
+
+const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
+ portableDataHash: location.replace('keep:', ''),
+ name: basename,
+});
+
+const validationSelector = createSelector(
+ isRequiredInput,
+ isRequired => isRequired
+ ? [required]
+ : undefined
+);
+
+const required = (value?: Directory[]) =>
+ value && value.length > 0
+ ? undefined
+ : ERROR_MESSAGE;
+interface DirectoryArrayInputComponentState {
+ open: boolean;
+ directories: CollectionResource[];
+ prevDirectories: CollectionResource[];
+}
+
+interface DirectoryArrayInputComponentProps {
+ treePickerState: TreePicker;
+}
+
+const treePickerSelector = (state: RootState) => state.treePicker;
+
+const mapStateToProps = createStructuredSelector({
+ treePickerState: treePickerSelector,
+});
+
+const DirectoryArrayInputComponent = connect(mapStateToProps)(
+ class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp, DirectoryArrayInputComponentState> {
+ state: DirectoryArrayInputComponentState = {
+ open: false,
+ directories: [],
+ prevDirectories: [],
+ };
+
+ directoryRefreshTimeout = -1;
+
+ componentDidMount() {
+ this.props.dispatch<any>(
+ initProjectsTreePicker(this.props.commandInput.id));
+ }
+
+ render() {
+ return <>
+ <this.input />
+ <this.dialog />
+ </>;
+ }
+
+ openDialog = () => {
+ this.setDirectoriesFromProps(this.props.input.value);
+ this.setState({ open: true });
+ }
+
+
+ closeDialog = () => {
+ this.setState({ open: false });
+ }
+
+ submit = () => {
+ this.closeDialog();
+ this.props.input.onChange(this.state.directories);
+ }
+
+ setDirectories = (directories: CollectionResource[]) => {
+
+ const deletedDirectories = this.state.directories
+ .reduce((deletedDirectories, directory) =>
+ directories.some(({ uuid }) => uuid === directory.uuid)
+ ? deletedDirectories
+ : [...deletedDirectories, directory]
+ , []);
+
+ this.setState({ directories });
+
+ const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
+ ids.forEach(pickerId => {
+ this.props.dispatch(
+ treePickerActions.DESELECT_TREE_PICKER_NODE({
+ pickerId, id: deletedDirectories.map(({ uuid }) => uuid),
+ })
+ );
+ });
+
+ }
+
+ setDirectoriesFromProps = (formattedDirectories: FormattedDirectory[]) => {
+ const nodes = getAllNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+ const initialDirectories: CollectionResource[] = [];
+ const directories = nodes
+ .reduce((directories, { value }) =>
+ 'kind' in value &&
+ value.kind === ResourceKind.COLLECTION &&
+ formattedDirectories.find(({ portableDataHash }) => value.portableDataHash === portableDataHash)
+ ? directories.concat(value)
+ : directories, initialDirectories);
+
+ const addedDirectories = directories
+ .reduce((addedDirectories, directory) =>
+ this.state.directories.find(({ uuid }) =>
+ uuid === directory.uuid)
+ ? addedDirectories
+ : [...addedDirectories, directory]
+ , []);
+
+ const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
+ ids.forEach(pickerId => {
+ this.props.dispatch(
+ treePickerActions.SELECT_TREE_PICKER_NODE({
+ pickerId, id: addedDirectories.map(({ uuid }) => uuid),
+ })
+ );
+ });
+
+ const orderedDirectories = formattedDirectories.reduce((dirs, formattedDir) => {
+ const dir = directories.find(({ portableDataHash }) => portableDataHash === formattedDir.portableDataHash);
+ return dir
+ ? [...dirs, dir]
+ : dirs;
+ }, []);
+
+ this.setDirectories(orderedDirectories);
+
+ }
+
+ refreshDirectories = () => {
+ clearTimeout(this.directoryRefreshTimeout);
+ this.directoryRefreshTimeout = setTimeout(this.setSelectedFiles);
+ }
+
+ setSelectedFiles = () => {
+ const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+ const initialDirectories: CollectionResource[] = [];
+ const directories = nodes
+ .reduce((directories, { value }) =>
+ 'kind' in value && value.kind === ResourceKind.COLLECTION
+ ? directories.concat(value)
+ : directories, initialDirectories);
+ this.setDirectories(directories);
+ }
+ input = () =>
+ <GenericInput
+ component={this.chipsInput}
+ {...this.props} />
+
+ chipsInput = () =>
+ <ChipsInput
+ value={this.props.input.value}
+ onChange={noop}
+ createNewValue={identity}
+ getLabel={(data: FormattedDirectory) => data.name}
+ inputComponent={this.textInput} />
+
+ textInput = (props: InputProps) =>
+ <Input
+ {...props}
+ error={this.props.meta.touched && !!this.props.meta.error}
+ readOnly
+ onClick={this.openDialog}
+ onKeyPress={this.openDialog}
+ onBlur={this.props.input.onBlur} />
+
+ dialog = () =>
+ <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ maxWidth='md' >
+ <DialogTitle>Choose collections</DialogTitle>
+ <DialogContent>
+ <this.dialogContent />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>
+
+ dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'column',
+ height: `${spacing.unit * 8}vh`,
+ },
+ tree: {
+ flex: 3,
+ overflow: 'auto',
+ },
+ divider: {
+ margin: `${spacing.unit}px 0`,
+ },
+ chips: {
+ flex: 1,
+ overflow: 'auto',
+ padding: `${spacing.unit}px 0`,
+ overflowX: 'hidden',
+ },
+ })
+
+ dialogContent = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <div className={classes.root}>
+ <div className={classes.tree}>
+ <ProjectsTreePicker
+ pickerId={this.props.commandInput.id}
+ includeCollections
+ showSelection
+ toggleItemSelection={this.refreshDirectories} />
+ </div>
+ <Divider />
+ <div className={classes.chips}>
+ <Typography variant='subheading'>Selected collections ({this.state.directories.length}):</Typography>
+ <Chips
+ orderable
+ deletable
+ values={this.state.directories}
+ onChange={this.setDirectories}
+ getLabel={(directory: CollectionResource) => directory.name} />
+ </div>
+ </div>
+ );
+
+ });
+
+type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ isRequiredInput,
+ FileArrayCommandInputParameter,
+ File,
+ CWLType
+} from '~/models/workflow';
+import { Field } from 'redux-form';
+import { ERROR_MESSAGE } from '~/validators/require';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, Grid, WithStyles, Typography } from '@material-ui/core';
+import { GenericInputProps, GenericInput } from './generic-input';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { connect, DispatchProp } from 'react-redux';
+import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds } from '~/store/tree-picker/tree-picker-actions';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { CollectionFile, CollectionFileType } from '~/models/collection-file';
+import { createSelector, createStructuredSelector } from 'reselect';
+import { ChipsInput } from '~/components/chips-input/chips-input';
+import { identity, values, noop } from 'lodash';
+import { InputProps } from '@material-ui/core/Input';
+import { TreePicker } from '~/store/tree-picker/tree-picker';
+import { RootState } from '~/store/store';
+import { Chips } from '~/components/chips/chips';
+import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
+
+export interface FileArrayInputProps {
+ input: FileArrayCommandInputParameter;
+}
+export const FileArrayInput = ({ input }: FileArrayInputProps) =>
+ <Field
+ name={input.id}
+ commandInput={input}
+ component={FileArrayInputComponent}
+ parse={parseFiles}
+ format={formatFiles}
+ validate={validationSelector(input)} />;
+
+const parseFiles = (files: CollectionFile[] | string) =>
+ typeof files === 'string'
+ ? undefined
+ : files.map(parse);
+
+const parse = (file: CollectionFile): File => ({
+ class: CWLType.FILE,
+ basename: file.name,
+ location: `keep:${file.id}`,
+ path: file.path,
+});
+
+const formatFiles = (files: File[] = []) =>
+ files.map(format);
+
+const format = (file: File): CollectionFile => ({
+ id: file.location
+ ? file.location.replace('keep:', '')
+ : '',
+ name: file.basename || '',
+ path: file.path || '',
+ size: 0,
+ type: CollectionFileType.FILE,
+ url: '',
+});
+
+const validationSelector = createSelector(
+ isRequiredInput,
+ isRequired => isRequired
+ ? [required]
+ : undefined
+);
+
+const required = (value?: File[]) =>
+ value && value.length > 0
+ ? undefined
+ : ERROR_MESSAGE;
+interface FileArrayInputComponentState {
+ open: boolean;
+ files: CollectionFile[];
+}
+
+interface FileArrayInputComponentProps {
+ treePickerState: TreePicker;
+}
+
+const treePickerSelector = (state: RootState) => state.treePicker;
+
+const mapStateToProps = createStructuredSelector({
+ treePickerState: treePickerSelector,
+});
+
+const FileArrayInputComponent = connect(mapStateToProps)(
+ class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp, FileArrayInputComponentState> {
+ state: FileArrayInputComponentState = {
+ open: false,
+ files: [],
+ };
+
+ fileRefreshTimeout = -1;
+
+ componentDidMount() {
+ this.props.dispatch<any>(
+ initProjectsTreePicker(this.props.commandInput.id));
+ }
+
+ render() {
+ return <>
+ <this.input />
+ <this.dialog />
+ </>;
+ }
+
+ openDialog = () => {
+ this.setFilesFromProps(this.props.input.value);
+ this.setState({ open: true });
+ }
+
+
+ closeDialog = () => {
+ this.setState({ open: false });
+ }
+
+ submit = () => {
+ this.closeDialog();
+ this.props.input.onChange(this.state.files);
+ }
+
+ setFiles = (files: CollectionFile[]) => {
+
+ const deletedFiles = this.state.files
+ .reduce((deletedFiles, file) =>
+ files.some(({ id }) => id === file.id)
+ ? deletedFiles
+ : [...deletedFiles, file]
+ , []);
+
+ this.setState({ files });
+
+ const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
+ ids.forEach(pickerId => {
+ this.props.dispatch(
+ treePickerActions.DESELECT_TREE_PICKER_NODE({
+ pickerId, id: deletedFiles.map(({ id }) => id),
+ })
+ );
+ });
+
+ }
+
+ setFilesFromProps = (files: CollectionFile[]) => {
+
+ const addedFiles = files
+ .reduce((addedFiles, file) =>
+ this.state.files.some(({ id }) => id === file.id)
+ ? addedFiles
+ : [...addedFiles, file]
+ , []);
+
+ const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
+ ids.forEach(pickerId => {
+ this.props.dispatch(
+ treePickerActions.SELECT_TREE_PICKER_NODE({
+ pickerId, id: addedFiles.map(({ id }) => id),
+ })
+ );
+ });
+
+ this.setFiles(files);
+
+ }
+
+ refreshFiles = () => {
+ clearTimeout(this.fileRefreshTimeout);
+ this.fileRefreshTimeout = setTimeout(this.setSelectedFiles);
+ }
+
+ setSelectedFiles = () => {
+ const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+ const initialFiles: CollectionFile[] = [];
+ const files = nodes
+ .reduce((files, { value }) =>
+ 'type' in value && value.type === CollectionFileType.FILE
+ ? files.concat(value)
+ : files, initialFiles);
+
+ this.setFiles(files);
+ }
+ input = () =>
+ <GenericInput
+ component={this.chipsInput}
+ {...this.props} />
+
+ chipsInput = () =>
+ <ChipsInput
+ value={this.props.input.value}
+ onChange={noop}
+ createNewValue={identity}
+ getLabel={(file: CollectionFile) => file.name}
+ inputComponent={this.textInput} />
+
+ textInput = (props: InputProps) =>
+ <Input
+ {...props}
+ error={this.props.meta.touched && !!this.props.meta.error}
+ readOnly
+ onClick={this.openDialog}
+ onKeyPress={this.openDialog}
+ onBlur={this.props.input.onBlur} />
+
+ dialog = () =>
+ <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ maxWidth='md' >
+ <DialogTitle>Choose files</DialogTitle>
+ <DialogContent>
+ <this.dialogContent />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>
+
+ dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'column',
+ height: `${spacing.unit * 8}vh`,
+ },
+ tree: {
+ flex: 3,
+ overflow: 'auto',
+ },
+ divider: {
+ margin: `${spacing.unit}px 0`,
+ },
+ chips: {
+ flex: 1,
+ overflow: 'auto',
+ padding: `${spacing.unit}px 0`,
+ overflowX: 'hidden',
+ },
+ })
+
+ dialogContent = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <div className={classes.root}>
+ <div className={classes.tree}>
+ <ProjectsTreePicker
+ pickerId={this.props.commandInput.id}
+ includeCollections
+ includeFiles
+ showSelection
+ toggleItemSelection={this.refreshFiles} />
+ </div>
+ <Divider />
+ <div className={classes.chips}>
+ <Typography variant='subheading'>Selected files ({this.state.files.length}):</Typography>
+ <Chips
+ orderable
+ deletable
+ values={this.state.files}
+ onChange={this.setFiles}
+ getLabel={(file: CollectionFile) => file.name} />
+ </div>
+ </div>
+ );
+
+ });
+
+type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+
+
+
import { TreeItem } from '~/components/tree/tree';
import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
import { CollectionFile, CollectionFileType } from '~/models/collection-file';
-import { getFileFullPath } from '~/services/collection-service/collection-service-files-response';
export interface FileInputProps {
input: FileCommandInputParameter;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { isRequiredInput, StringArrayCommandInputParameter } from '~/models/workflow';
+import { Field } from 'redux-form';
+import { ERROR_MESSAGE } from '~/validators/require';
+import { GenericInputProps, GenericInput } from '~/views/run-process-panel/inputs/generic-input';
+import { ChipsInput } from '~/components/chips-input/chips-input';
+import { identity } from 'lodash';
+import { createSelector } from 'reselect';
+import { Input } from '@material-ui/core';
+
+export interface StringArrayInputProps {
+ input: StringArrayCommandInputParameter;
+}
+export const StringArrayInput = ({ input }: StringArrayInputProps) =>
+ <Field
+ name={input.id}
+ commandInput={input}
+ component={StringArrayInputComponent}
+ validate={validationSelector(input)} />;
+
+
+const validationSelector = createSelector(
+ isRequiredInput,
+ isRequired => isRequired
+ ? [required]
+ : undefined
+);
+
+const required = (value: string[]) =>
+ value.length > 0
+ ? undefined
+ : ERROR_MESSAGE;
+
+const StringArrayInputComponent = (props: GenericInputProps) =>
+ <GenericInput
+ component={InputComponent}
+ {...props} />;
+
+class InputComponent extends React.PureComponent<GenericInputProps>{
+ render() {
+ return <ChipsInput
+ deletable
+ orderable
+ value={this.props.input.value}
+ onChange={this.handleChange}
+ createNewValue={identity}
+ inputComponent={Input}
+ inputProps={{
+ error: this.props.meta.error,
+ }} />;
+ }
+
+ handleChange = (values: {}[]) => {
+ const { input, meta } = this.props;
+ if (!meta.touched) {
+ input.onBlur(values);
+ }
+ input.onChange(values);
+ }
+}
import * as React from 'react';
import { reduxForm, InjectedFormProps } from 'redux-form';
-import { CommandInputParameter, CWLType, IntCommandInputParameter, BooleanCommandInputParameter, FileCommandInputParameter, DirectoryCommandInputParameter } from '~/models/workflow';
+import { CommandInputParameter, CWLType, IntCommandInputParameter, BooleanCommandInputParameter, FileCommandInputParameter, DirectoryCommandInputParameter, DirectoryArrayCommandInputParameter } from '~/models/workflow';
import { IntInput } from '~/views/run-process-panel/inputs/int-input';
import { StringInput } from '~/views/run-process-panel/inputs/string-input';
-import { StringCommandInputParameter, FloatCommandInputParameter, isPrimitiveOfType, File, Directory, WorkflowInputsData, EnumCommandInputParameter } from '../../models/workflow';
+import { StringCommandInputParameter, FloatCommandInputParameter, isPrimitiveOfType, File, Directory, WorkflowInputsData, EnumCommandInputParameter, isArrayOfType, StringArrayCommandInputParameter, FileArrayCommandInputParameter } from '../../models/workflow';
import { FloatInput } from '~/views/run-process-panel/inputs/float-input';
import { BooleanInput } from './inputs/boolean-input';
import { FileInput } from './inputs/file-input';
import { Grid, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
import { EnumInput } from './inputs/enum-input';
import { DirectoryInput } from './inputs/directory-input';
+import { StringArrayInput } from './inputs/string-array-input';
+import { createStructuredSelector, createSelector } from 'reselect';
+import { FileArrayInput } from './inputs/file-array-input';
+import { DirectoryArrayInput } from './inputs/directory-array-input';
export const RUN_PROCESS_INPUTS_FORM = 'runProcessInputsForm';
inputs: CommandInputParameter[];
}
+const inputsSelector = (props: RunProcessInputFormProps) =>
+ props.inputs;
+
+const initialValuesSelector = createSelector(
+ inputsSelector,
+ inputs => inputs.reduce(
+ (values, input) => ({ ...values, [input.id]: input.default }),
+ {}));
+
+const propsSelector = createStructuredSelector({
+ initialValues: initialValuesSelector,
+});
+
+const mapStateToProps = (_: any, props: RunProcessInputFormProps) =>
+ propsSelector(props);
+
export const RunProcessInputsForm = compose(
- connect((_: any, props: RunProcessInputFormProps) => ({
- initialValues: props.inputs.reduce(
- (values, input) => ({ ...values, [input.id]: input.default }),
- {}),
- })),
+ connect(mapStateToProps),
reduxForm<WorkflowInputsData, RunProcessInputFormProps>({
form: RUN_PROCESS_INPUTS_FORM
}))(
case isPrimitiveOfType(input, CWLType.FILE):
return <FileInput input={input as FileCommandInputParameter} />;
-
+
case isPrimitiveOfType(input, CWLType.DIRECTORY):
return <DirectoryInput input={input as DirectoryCommandInputParameter} />;
input.type.type === 'enum':
return <EnumInput input={input as EnumCommandInputParameter} />;
+ case isArrayOfType(input, CWLType.STRING):
+ return <StringArrayInput input={input as StringArrayCommandInputParameter} />;
+
+ case isArrayOfType(input, CWLType.FILE):
+ return <FileArrayInput input={input as FileArrayCommandInputParameter} />;
+
+ case isArrayOfType(input, CWLType.DIRECTORY):
+ return <DirectoryArrayInput input={input as DirectoryArrayCommandInputParameter} />;
+
default:
return null;
}
import { isValid } from 'redux-form';
import { RUN_PROCESS_INPUTS_FORM } from './run-process-inputs-form';
import { RunProcessAdvancedForm } from './run-process-advanced-form';
+import { createSelector, createStructuredSelector } from 'reselect';
export interface RunProcessSecondStepFormDataProps {
inputs: CommandInputParameter[];
runProcess: () => void;
}
-const mapStateToProps = (state: RootState): RunProcessSecondStepFormDataProps => ({
- inputs: state.runProcessPanel.inputs,
- valid: isValid(RUN_PROCESS_BASIC_FORM)(state) &&
- isValid(RUN_PROCESS_INPUTS_FORM)(state),
+const inputsSelector = (state: RootState) =>
+ state.runProcessPanel.inputs;
+
+const validSelector = (state: RootState) =>
+ isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state);
+
+const mapStateToProps = createStructuredSelector({
+ inputs: inputsSelector,
+ valid: validSelector,
});
export type RunProcessSecondStepFormProps = RunProcessSecondStepFormDataProps & RunProcessSecondStepFormActionProps;
import SplitterLayout from 'react-splitter-layout';
import { WorkflowPanel } from '~/views/workflow-panel/workflow-panel';
import { SearchResultsPanel } from '~/views/search-results-panel/search-results-panel';
+import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
+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';
dependencies:
"@types/react" "*"
+"@types/react-dnd@3.0.2":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/react-dnd/-/react-dnd-3.0.2.tgz#939e5a8ca5b83f847c3f64dabbe2f49a9fefb192"
+ dependencies:
+ react-dnd "*"
+
"@types/react-dom@16.0.8":
version "16.0.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.8.tgz#6e1366ed629cadf55860cbfcc25db533f5d2fa7d"
"@types/react" "*"
redux "^3.6.0 || ^4.0.0"
+"@types/reselect@2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@types/reselect/-/reselect-2.2.0.tgz#c667206cfdc38190e1d379babe08865b2288575f"
+ dependencies:
+ reselect "*"
+
"@types/shell-quote@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.6.0.tgz#537b2949a2ebdcb0d353e448fee45b081021963f"
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
-asap@~2.0.3:
+asap@^2.0.6, asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
dependencies:
core-js "^2.5.0"
+autobind-decorator@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.1.0.tgz#4451240dbfeff46361c506575a63ed40f0e5bc68"
+
autoprefixer@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7"
version "1.0.0"
resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+dnd-core@^4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-4.0.5.tgz#3b83d138d0d5e265c73ec978dec5e1ed441dc665"
+ dependencies:
+ asap "^2.0.6"
+ invariant "^2.2.4"
+ lodash "^4.17.10"
+ redux "^4.0.0"
+
dns-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
-invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
+invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
strip-ansi "3.0.1"
text-table "0.2.0"
+react-dnd-html5-backend@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-5.0.1.tgz#0b578d79c5c01317c70414c8d717f632b919d4f1"
+ dependencies:
+ autobind-decorator "^2.1.0"
+ dnd-core "^4.0.5"
+ lodash "^4.17.10"
+ shallowequal "^1.0.2"
+
+react-dnd@*, react-dnd@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-5.0.0.tgz#c4a17c70109e456dad8906be838e6ee8f32b06b5"
+ dependencies:
+ dnd-core "^4.0.5"
+ hoist-non-react-statics "^2.5.0"
+ invariant "^2.1.0"
+ lodash "^4.17.10"
+ recompose "^0.27.1"
+ shallowequal "^1.0.2"
+
react-dom@16.5.2:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
react-lifecycles-compat "^3.0.2"
symbol-observable "^1.0.4"
+recompose@^0.27.1:
+ version "0.27.1"
+ resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
+ dependencies:
+ babel-runtime "^6.26.0"
+ change-emitter "^0.1.2"
+ fbjs "^0.8.1"
+ hoist-non-react-statics "^2.3.1"
+ react-lifecycles-compat "^3.0.2"
+ symbol-observable "^1.0.4"
+
recompose@^0.29.0:
version "0.29.0"
resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.29.0.tgz#f1a4e20d5f24d6ef1440f83924e821de0b1bccef"
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
-reselect@4.0.0:
+reselect@*, reselect@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
inherits "^2.0.1"
safe-buffer "^5.0.1"
+shallowequal@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"