if (item.selected) { return; }
// Otherwise select this node and deselect the others
- const filters = selectNode(item.id)(this.props.filters);
+ const filters = selectNode(item.id, true)(this.props.filters);
const toDeselect = Object.keys(this.props.filters).filter((id) => (id !== item.id));
- onChange(deselectNodes(toDeselect)(filters));
+ onChange(deselectNodes(toDeselect, true)(filters));
}
toggleFilter = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
const { onChange = noop } = this.props;
- onChange(toggleNodeSelection(item.id)(this.props.filters));
+ onChange(toggleNodeSelection(item.id, true)(this.props.filters));
}
toggleOpen = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
export const expandNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+export const expandNodeAncestors = (...ids: string[]) => <T>(tree: Tree<T>) => {
+ const ancestors = ids.reduce((acc, id): string[] => ([...acc, ...getNodeAncestorsIds(id)(tree)]), [] as string[]);
+ return mapTree((node: TreeNode<T>) => ancestors.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+}
+
export const collapseNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
: tree;
};
-export const toggleNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
+export const toggleNodeSelection = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
+
return node
- ? pipe(
- setNode({ ...node, selected: !node.selected }),
- toggleAncestorsSelection(id),
- toggleDescendantsSelection(id))(tree)
+ ? cascade
+ ? pipe(
+ setNode({ ...node, selected: !node.selected }),
+ toggleAncestorsSelection(id),
+ toggleDescendantsSelection(id))(tree)
+ : setNode({ ...node, selected: !node.selected })(tree)
: tree;
};
-export const selectNode = (id: string) => <T>(tree: Tree<T>) => {
+export const selectNode = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
return node && node.selected
? tree
- : toggleNodeSelection(id)(tree);
+ : toggleNodeSelection(id, cascade)(tree);
};
-export const selectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+export const selectNodes = (id: string | string[], cascade: boolean) => <T>(tree: Tree<T>) => {
const ids = typeof id === 'string' ? [id] : id;
- return ids.reduce((tree, id) => selectNode(id)(tree), tree);
+ return ids.reduce((tree, id) => selectNode(id, cascade)(tree), tree);
};
-export const deselectNode = (id: string) => <T>(tree: Tree<T>) => {
+export const deselectNode = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
return node && node.selected
- ? toggleNodeSelection(id)(tree)
+ ? toggleNodeSelection(id, cascade)(tree)
: tree;
};
-export const deselectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+export const deselectNodes = (id: string | string[], cascade: boolean) => <T>(tree: Tree<T>) => {
const ids = typeof id === 'string' ? [id] : id;
- return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
+ return ids.reduce((tree, id) => deselectNode(id, cascade)(tree), tree);
};
export const getSelectedNodes = <T>(tree: Tree<T>) =>
});
it("should serialize all but collection filters", () => {
- const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
+ const filters = deselectNode(ObjectTypeFilter.COLLECTION, true)(getInitialResourceTypeFilters());
const serializedFilters = serializeResourceTypeFilters(filters);
expect(serializedFilters)
.toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
it("should serialize output collections and projects", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
- deselectNode(CollectionTypeFilter.LOG_COLLECTION),
- deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(CollectionTypeFilter.GENERAL_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.LOG_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize output collections and projects", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
- deselectNode(CollectionTypeFilter.LOG_COLLECTION),
- deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(CollectionTypeFilter.GENERAL_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.LOG_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize general collections", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.PROJECT),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
+ deselectNode(ObjectTypeFilter.PROJECT, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION, true)
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize only main processes", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.PROJECT),
- deselectNode(ProcessTypeFilter.CHILD_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION),
- deselectNode(ObjectTypeFilter.DEFINITION),
+ deselectNode(ObjectTypeFilter.PROJECT, true),
+ deselectNode(ProcessTypeFilter.CHILD_PROCESS, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize only child processes", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.PROJECT),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.PROJECT, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
- selectNode(ProcessTypeFilter.CHILD_PROCESS),
+ selectNode(ProcessTypeFilter.CHILD_PROCESS, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize all project types", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.COLLECTION),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize filter groups", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(GroupTypeFilter.PROJECT),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(GroupTypeFilter.PROJECT, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize projects (normal)", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(GroupTypeFilter.FILTER_GROUP),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(GroupTypeFilter.FILTER_GROUP, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
import { CollectionResource } from "models/collection";
import { getResource } from "store/resources/resources";
import { updateResources } from "store/resources/resources-actions";
+import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
+ EXPAND_TREE_PICKER_NODE_ANCESTORS: ofType<{ id: string, pickerId: string }>(),
ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: 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 }>(),
+ TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string, cascade: boolean }>(),
+ SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
+ DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
RESET_TREE_PICKER: ofType<{ pickerId: string }>()
});
export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
getAllNodes<Value>(pickerId, node => node.selected)(state);
-export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: string) =>
+interface TreePickerPreloadParams {
+ selectedItemUuids: string[];
+ includeDirectories: boolean;
+ includeFiles: boolean;
+ multi: boolean;
+}
+
+export const initProjectsTreePicker = (pickerId: string, preloadParams?: TreePickerPreloadParams) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
dispatch<any>(initUserProject(home));
dispatch<any>(initPublicFavoritesProject(publicFavorites));
dispatch<any>(initSearchProject(search));
- if (selectedItemUuid) {
- dispatch<any>(loadInitialValue(selectedItemUuid, pickerId));
+ if (preloadParams && preloadParams.selectedItemUuids.length) {
+ dispatch<any>(loadInitialValue(
+ preloadParams.selectedItemUuids,
+ pickerId,
+ preloadParams.includeDirectories,
+ preloadParams.includeFiles,
+ preloadParams.multi
+ ));
}
};
const sorted = sortFilesTree(tree);
const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
- dispatch(
+ // await tree modifications so that consumers can guarantee node presence
+ await dispatch(
treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
id,
pickerId,
subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
}));
+ // Expand collection root node
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
}
}
}));
};
-export const loadInitialValue = (initialValue: string, pickerId: string) =>
+type PickerItemPreloadData = {
+ itemId: string;
+ mainItemUuid: string;
+ ancestors: (GroupResource | CollectionResource)[];
+ isHomeProjectItem: boolean;
+}
+
+type PickerTreePreloadData = {
+ tree: Tree<GroupResource | CollectionResource>;
+ pickerTreeId: string;
+ pickerTreeRootUuid: string;
+};
+
+export const loadInitialValue = (pickerItemIds: string[], pickerId: string, includeDirectories: boolean, includeFiles: boolean, multi: boolean,) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const { home, shared } = getProjectsTreePickerIds(pickerId);
const homeUuid = getUserUuid(getState());
- const ancestors = (await services.ancestorsService.ancestors(initialValue, ''))
+
+ // Request ancestor trees in paralell and save home project status
+ const pickerItemsData: PickerItemPreloadData[] = await Promise.allSettled(pickerItemIds.map(async itemId => {
+ const mainItemUuid = itemId.includes('/') ? itemId.split('/')[0] : itemId;
+ const ancestors = (await services.ancestorsService.ancestors(mainItemUuid, ''))
.filter(item =>
item.kind === ResourceKind.GROUP ||
item.kind === ResourceKind.COLLECTION
) as (GroupResource | CollectionResource)[];
- if (ancestors.length) {
- const isUserHomeProject = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
- const pickerTreeId = isUserHomeProject ? home : shared;
- const pickerTreeRootUuid: string = (homeUuid && isUserHomeProject) ? homeUuid : SHARED_PROJECT_ID;
+ const isHomeProjectItem = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
- ancestors[0].ownerUuid = '';
- const tree = createInitialLocationTree(ancestors, initialValue);
+ return {
+ itemId,
+ mainItemUuid,
+ ancestors,
+ isHomeProjectItem,
+ };
+ })).then((res) => {
+ // Show toast if any selections failed to restore
+ if (res.find((promiseResult): promiseResult is PromiseRejectedResult => (promiseResult.status === 'rejected'))) {
+ dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Some selections failed to load and were removed`, kind: SnackbarKind.ERROR }));
+ }
+ // Filter out any failed promises and map to resulting preload data with ancestors
+ return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<PickerItemPreloadData> => (
+ promiseResult.status === 'fulfilled'
+ )).map(res => res.value)
+ });
+
+ // Group items to preload / ancestor data by home/shared picker and create initial Trees to preload
+ const initialTreePreloadData: PickerTreePreloadData[] = [
+ pickerItemsData.filter((item) => item.isHomeProjectItem),
+ pickerItemsData.filter((item) => !item.isHomeProjectItem),
+ ]
+ .filter((items) => items.length > 0)
+ .map((itemGroup) =>
+ itemGroup.reduce(
+ (preloadTree, itemData) => ({
+ tree: createInitialPickerTree(
+ itemData.ancestors,
+ itemData.mainItemUuid,
+ preloadTree.tree
+ ),
+ pickerTreeId: getPickerItemTreeId(itemData, homeUuid, pickerId),
+ pickerTreeRootUuid: getPickerItemRootUuid(itemData, homeUuid),
+ }),
+ {
+ tree: createTree<GroupResource | CollectionResource>(),
+ pickerTreeId: '',
+ pickerTreeRootUuid: '',
+ } as PickerTreePreloadData
+ )
+ );
+
+ // Load initial trees into corresponding picker store
+ await Promise.all(initialTreePreloadData.map(preloadTree => (
dispatch(
treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
- id: pickerTreeRootUuid,
- pickerId: pickerTreeId,
- subtree: tree
- }));
- dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: initialValue, pickerId: pickerTreeId }));
- dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: pickerTreeId }));
- }
+ id: preloadTree.pickerTreeRootUuid,
+ pickerId: preloadTree.pickerTreeId,
+ subtree: preloadTree.tree,
+ })
+ )
+ )));
+
+ // Await loading collection before attempting to select items
+ await Promise.all(pickerItemsData.map(async itemData => {
+ const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
+
+ // Selected item resides in collection subpath
+ if (itemData.itemId.includes('/')) {
+ // Load collection into tree
+ // loadCollection includes more than dispatched actions and must be awaited
+ await dispatch(loadCollection(itemData.mainItemUuid, pickerTreeId, includeDirectories, includeFiles));
+ }
+ // Expand nodes down to destination
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE_ANCESTORS({ id: itemData.itemId, pickerId: pickerTreeId }));
+ }));
+
+ // Select or activate nodes
+ pickerItemsData.forEach(itemData => {
+ const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
+
+ if (multi) {
+ dispatch(treePickerActions.SELECT_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId, cascade: false}));
+ } else {
+ dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId }));
+ }
+ });
+ // Refresh triggers loading in all adjacent items that were not included in the ancestor tree
+ await initialTreePreloadData.map(preloadTree => dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: preloadTree.pickerTreeId })));
}
+const getPickerItemTreeId = (itemData: PickerItemPreloadData, homeUuid: string | undefined, pickerId: string) => {
+ const { home, shared } = getProjectsTreePickerIds(pickerId);
+ return ((itemData.isHomeProjectItem && homeUuid) ? home : shared);
+};
+
+const getPickerItemRootUuid = (itemData: PickerItemPreloadData, homeUuid: string | undefined) => {
+ return (itemData.isHomeProjectItem && homeUuid) ? homeUuid : SHARED_PROJECT_ID;
+};
+
export const FAVORITES_PROJECT_ID = 'Favorites';
export const initFavoritesProject = (pickerId: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
/**
* Create an expanded tree picker subtree from array of nested projects/collection
- * Assumes the root item of the subtree already has an empty string ownerUuid
+ * First item is assumed to be root and gets empty parent id
+ * Nodes must be sorted from top down to prevent orphaned nodes
*/
-export const createInitialLocationTree = (data: Array<GroupResource | CollectionResource>, tailUuid: string) => {
- return data
- .reduce((tree, item) => setNode({
- children: [],
- id: item.uuid,
- parent: item.ownerUuid,
- value: item,
- active: false,
- selected: false,
- expanded: false,
- status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
- })(tree), createTree<GroupResource | CollectionResource>());
+export const createInitialPickerTree = (sortedAncestors: Array<GroupResource | CollectionResource>, tailUuid: string, initialTree: Tree<GroupResource | CollectionResource>) => {
+ return sortedAncestors
+ .reduce((tree, item, index) => {
+ if (getNode(item.uuid)(tree)) {
+ return tree;
+ } else {
+ return setNode({
+ children: [],
+ id: item.uuid,
+ parent: index === 0 ? '' : item.ownerUuid,
+ value: item,
+ active: false,
+ selected: false,
+ expanded: false,
+ status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
+ })(tree);
+ }
+ }, initialTree);
};
+
+export const fileOperationLocationToPickerId = (location: FileOperationLocation): string => {
+ let id = location.uuid;
+ if (location.subpath.length && location.subpath !== '/') {
+ id = id + location.subpath;
+ }
+ return id;
+}
const newState = pipe(
(state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })),
state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode], pickerId: "projects" })),
- state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects" })),
+ state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects", cascade: true })),
)({ projects: createTree<{}>() });
expect(getNode('1')(newState.projects)).toEqual({
...initTreeNode({ id: '1', value: '1' }),
import {
createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus,
expandNode, deactivateNode, selectNodes, deselectNodes,
- activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree
+ activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree, expandNodeAncestors
} from 'models/tree';
import { TreePicker } from "./tree-picker";
import { treePickerActions, treePickerSearchActions, TreePickerAction, TreePickerSearchAction, LoadProjectParams } from "./tree-picker-actions";
EXPAND_TREE_PICKER_NODE: ({ id, pickerId }) =>
updateOrCreatePicker(state, pickerId, expandNode(id)),
+ EXPAND_TREE_PICKER_NODE_ANCESTORS: ({ id, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, expandNodeAncestors(id)),
+
ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId, relatedTreePickers = [] }) =>
pipe(
() => relatedTreePickers.reduce(
DEACTIVATE_TREE_PICKER_NODE: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, deactivateNode),
- TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId }) =>
- updateOrCreatePicker(state, pickerId, toggleNodeSelection(id)),
+ TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId, cascade }) =>
+ updateOrCreatePicker(state, pickerId, toggleNodeSelection(id, cascade)),
- SELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
- updateOrCreatePicker(state, pickerId, selectNodes(id)),
+ SELECT_TREE_PICKER_NODE: ({ id, pickerId, cascade }) =>
+ updateOrCreatePicker(state, pickerId, selectNodes(id, cascade)),
- DESELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
- updateOrCreatePicker(state, pickerId, deselectNodes(id)),
+ DESELECT_TREE_PICKER_NODE: ({ id, pickerId, cascade }) =>
+ updateOrCreatePicker(state, pickerId, deselectNodes(id, cascade)),
RESET_TREE_PICKER: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, createTree),
type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
export interface ProjectsTreePickerDataProps {
+ cascadeSelection: boolean;
includeCollections?: boolean;
includeDirectories?: boolean;
includeFiles?: boolean;
export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
-const mapStateToProps = (_: any, { rootItemIcon, showSelection }: ProjectsTreePickerProps) => ({
+const mapStateToProps = (_: any, { rootItemIcon, showSelection, cascadeSelection }: ProjectsTreePickerProps) => ({
render: renderTreeItem(rootItemIcon),
- showSelection: isSelectionVisible(showSelection),
+ showSelection: isSelectionVisible(showSelection, cascadeSelection),
});
const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeDirectories, includeFiles, relatedTreePickers, options, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
}
},
toggleItemSelection: (event, item, pickerId) => {
- dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId }));
+ dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId, cascade: props.cascadeSelection }));
if (props.toggleItemSelection) {
props.toggleItemSelection(event, item, pickerId);
}
}
};
-const isSelectionVisible = (shouldBeVisible?: boolean) =>
- ({ status, items }: TreeItem<ProjectsTreePickerItem>): boolean => {
+const isSelectionVisible = (shouldBeVisible: boolean | undefined, cascadeSelection: boolean) =>
+ ({ status, items, data }: TreeItem<ProjectsTreePickerItem>): boolean => {
if (shouldBeVisible) {
- if (items && items.length > 0) {
- return items.every(isSelectionVisible(shouldBeVisible));
+ if (!cascadeSelection && 'kind' in data && data.kind === ResourceKind.COLLECTION) {
+ // In non-casecade mode collections are selectable without being loaded
+ return true;
+ } else if (items && items.length > 0) {
+ return items.every(isSelectionVisible(shouldBeVisible, cascadeSelection));
}
return status === TreeItemStatus.LOADED;
}
import { ArvadosTheme } from 'common/custom-theme';
export interface ToplevelPickerProps {
- currentUuid?: string;
+ currentUuids?: string[];
pickerId: string;
+ cascadeSelection: boolean;
includeCollections?: boolean;
includeDirectories?: boolean;
includeFiles?: boolean;
componentDidMount() {
const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
- this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId, this.props.currentUuid));
+ const preloadParams = this.props.currentUuids ? {
+ selectedItemUuids: this.props.currentUuids,
+ includeDirectories: !!this.props.includeDirectories,
+ includeFiles: !!this.props.includeFiles,
+ multi: !!this.props.showSelection,
+ } : undefined;
+ this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId, preloadParams));
this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue: "" }));
this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue: "" }));
const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
const relatedTreePickers = getRelatedTreePickers(pickerId);
const p = {
+ cascadeSelection: this.props.cascadeSelection,
includeCollections: this.props.includeCollections,
includeDirectories: this.props.includeDirectories,
includeFiles: this.props.includeFiles,
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
+ cascadeSelection={false}
options={{ showOnlyOwned: false, showOnlyWritable: true }} />
{props.meta.dirty && props.meta.error &&
<Typography variant='caption' color='error'>
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
+ cascadeSelection={false}
options={{ showOnlyOwned: false, showOnlyWritable: true }}
includeCollections />
{props.meta.dirty && props.meta.error &&
return <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
<div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<ProjectsTreePicker
- currentUuid={this.props.input.value.uuid}
+ currentUuids={[this.props.input.value.uuid]}
pickerId={this.props.pickerId}
toggleItemActive={this.handleDirectoryChange(this.props)}
+ cascadeSelection={false}
options={{ showOnlyOwned: false, showOnlyWritable: true }}
includeCollections
includeDirectories />
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 { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, FileOperationLocation, getFileOperationLocation, fileOperationLocationToPickerId } 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 { Chips } from 'components/chips/chips';
import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
import { CollectionResource } from 'models/collection';
-import { ResourceKind } from 'models/resource';
+import { PORTABLE_DATA_HASH_PATTERN, ResourceKind } from 'models/resource';
+import { Dispatch } from 'redux';
+import { CollectionDirectory, CollectionFileType } from 'models/collection-file';
+const LOCATION_REGEX = new RegExp("^(?:keep:)?(" + PORTABLE_DATA_HASH_PATTERN + ")(/.*)?$");
export interface DirectoryArrayInputProps {
input: DirectoryArrayCommandInputParameter;
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
interface FormattedDirectory {
name: string;
portableDataHash: string;
+ subpath: string;
}
-const parseDirectories = (directories: CollectionResource[] | string) =>
+const parseDirectories = (directories: FileOperationLocation[] | string) =>
typeof directories === 'string'
? undefined
: directories.map(parse);
-const parse = (directory: CollectionResource): Directory => ({
+const parse = (directory: FileOperationLocation): Directory => ({
class: CWLType.DIRECTORY,
basename: directory.name,
- location: `keep:${directory.portableDataHash}`,
+ location: `keep:${directory.pdh}${directory.subpath}`,
});
-const formatDirectories = (directories: Directory[] = []) =>
- directories ? directories.map(format) : [];
+const formatDirectories = (directories: Directory[] = []): FormattedDirectory[] =>
+ directories ? directories.map(format).filter((dir): dir is FormattedDirectory => Boolean(dir)) : [];
-const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
- portableDataHash: location.replace('keep:', ''),
- name: basename,
-});
+const format = ({ location = '', basename = '' }: Directory): FormattedDirectory | undefined => {
+ const match = LOCATION_REGEX.exec(location);
+
+ if (match) {
+ return {
+ portableDataHash: match[1],
+ subpath: match[2],
+ name: basename,
+ };
+ }
+ return undefined;
+};
const validationSelector = createSelector(
isRequiredInput,
: ERROR_MESSAGE;
interface DirectoryArrayInputComponentState {
open: boolean;
- directories: CollectionResource[];
- prevDirectories: CollectionResource[];
+ directories: FileOperationLocation[];
}
-interface DirectoryArrayInputComponentProps {
+interface DirectoryArrayInputDataProps {
treePickerState: TreePicker;
}
treePickerState: treePickerSelector,
});
-const DirectoryArrayInputComponent = connect(mapStateToProps)(
- class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp & {
+interface DirectoryArrayInputActionProps {
+ initProjectsTreePicker: (pickerId: string) => void;
+ selectTreePickerNode: (pickerId: string, id: string | string[]) => void;
+ deselectTreePickerNode: (pickerId: string, id: string | string[]) => void;
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DirectoryArrayInputActionProps => ({
+ initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
+ selectTreePickerNode: (pickerId: string, id: string | string[]) =>
+ dispatch<any>(treePickerActions.SELECT_TREE_PICKER_NODE({
+ pickerId, id, cascade: false
+ })),
+ deselectTreePickerNode: (pickerId: string, id: string | string[]) =>
+ dispatch<any>(treePickerActions.DESELECT_TREE_PICKER_NODE({
+ pickerId, id, cascade: false
+ })),
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+const DirectoryArrayInputComponent = connect(mapStateToProps, mapDispatchToProps)(
+ class DirectoryArrayInputComponent extends React.Component<GenericInputProps & DirectoryArrayInputDataProps & DirectoryArrayInputActionProps & DispatchProp & {
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}, DirectoryArrayInputComponentState> {
state: DirectoryArrayInputComponentState = {
open: false,
directories: [],
- prevDirectories: [],
};
directoryRefreshTimeout = -1;
componentDidMount() {
- this.props.dispatch<any>(
- initProjectsTreePicker(this.props.commandInput.id));
+ this.props.initProjectsTreePicker(this.props.commandInput.id);
}
render() {
}
openDialog = () => {
- this.setDirectoriesFromProps(this.props.input.value);
this.setState({ open: true });
}
this.props.input.onChange(this.state.directories);
}
- setDirectories = (directories: CollectionResource[]) => {
+ setDirectoriesFromResources = async (directories: (CollectionResource | CollectionDirectory)[]) => {
+ const locations = (await Promise.all(
+ directories.map(directory => (this.props.getFileOperationLocation(directory)))
+ )).filter((location): location is FileOperationLocation => (
+ location !== undefined
+ ));
- 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),
- })
- );
- });
+ this.setDirectories(locations);
+ }
+ refreshDirectories = () => {
+ clearTimeout(this.directoryRefreshTimeout);
+ this.directoryRefreshTimeout = window.setTimeout(this.setDirectoriesFromTree);
}
- setDirectoriesFromProps = (formattedDirectories: FormattedDirectory[]) => {
- const nodes = getAllNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
- const initialDirectories: CollectionResource[] = [];
+ setDirectoriesFromTree = () => {
+ const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+ const initialDirectories: (CollectionResource | CollectionDirectory)[] = [];
const directories = nodes
.reduce((directories, { value }) =>
- 'kind' in value &&
- value.kind === ResourceKind.COLLECTION &&
- formattedDirectories.find(({ portableDataHash, name }) => value.portableDataHash === portableDataHash && value.name === name)
+ (('kind' in value && value.kind === ResourceKind.COLLECTION) ||
+ ('type' in value && value.type === CollectionFileType.DIRECTORY))
? directories.concat(value)
: directories, initialDirectories);
+ this.setDirectoriesFromResources(directories);
+ }
+
+ setDirectories = (locations: FileOperationLocation[]) => {
+ const deletedDirectories = this.state.directories
+ .reduce((deletedDirectories, directory) =>
+ locations.some(({ uuid, subpath }) => uuid === directory.uuid && subpath === directory.subpath)
+ ? deletedDirectories
+ : [...deletedDirectories, directory]
+ , [] as FileOperationLocation[]);
- const addedDirectories = directories
- .reduce((addedDirectories, directory) =>
- this.state.directories.find(({ uuid }) =>
- uuid === directory.uuid)
- ? addedDirectories
- : [...addedDirectories, directory]
- , []);
+ this.setState({ directories: locations });
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),
- })
+ this.props.deselectTreePickerNode(
+ pickerId,
+ deletedDirectories.map(fileOperationLocationToPickerId)
);
});
+ };
- 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<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}
onClose={this.closeDialog}
fullWidth
maxWidth='md' >
- <DialogTitle>Choose collections</DialogTitle>
+ <DialogTitle>Choose directories</DialogTitle>
<DialogContent className={classes.root}>
<div className={classes.pickerWrapper}>
<div className={classes.tree}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
+ currentUuids={this.state.directories.map(dir => fileOperationLocationToPickerId(dir))}
includeCollections
+ includeDirectories
showSelection
+ cascadeSelection={false}
options={this.props.options}
toggleItemSelection={this.refreshDirectories} />
</div>
pickerId={this.props.commandInput.id}
includeCollections
includeDirectories
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setDirectory} />
</div>
ids.forEach(pickerId => {
this.props.dispatch(
treePickerActions.DESELECT_TREE_PICKER_NODE({
- pickerId, id: deletedFiles.map(({ id }) => id),
+ pickerId,
+ id: deletedFiles.map(({ id }) => id),
+ cascade: true,
})
);
});
ids.forEach(pickerId => {
this.props.dispatch(
treePickerActions.SELECT_TREE_PICKER_NODE({
- pickerId, id: addedFiles.map(({ id }) => id),
+ pickerId,
+ id: addedFiles.map(({ id }) => id),
+ cascade: true,
})
);
});
includeDirectories
includeFiles
showSelection
+ cascadeSelection={true}
options={this.props.options}
toggleItemSelection={this.refreshFiles} />
</div>
includeCollections
includeDirectories
includeFiles
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setFile} />
</div>
<div className={classes.pickerWrapper}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setProject} />
</div>