// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import 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, 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 'store/tree-picker/tree-picker-middleware'; 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; options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }; } export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) => ; 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 ? 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 { state: DirectoryArrayInputComponentState = { open: false, directories: [], prevDirectories: [], }; directoryRefreshTimeout = -1; componentDidMount() { this.props.dispatch( initProjectsTreePicker(this.props.commandInput.id)); } render() { return <> ; } 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(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, name }) => value.portableDataHash === portableDataHash && value.name === name) ? 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, name }) => portableDataHash === formattedDir.portableDataHash && name === formattedDir.name); return dir ? [...dirs, dir] : dirs; }, []); this.setDirectories(orderedDirectories); } refreshDirectories = () => { clearTimeout(this.directoryRefreshTimeout); this.directoryRefreshTimeout = window.setTimeout(this.setSelectedFiles); } setSelectedFiles = () => { const nodes = getSelectedNodes(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 = () => chipsInput = () => data.name} inputComponent={this.textInput} /> textInput = (props: InputProps) => dialog = () => Choose collections dialogContentStyles: StyleRulesCallback = ({ 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) =>
Selected collections ({this.state.directories.length}): directory.name} />
); }); type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';