[arvados-workbench2] created: 2.6.3-110-g25c0de8a
git repository hosting
git at public.arvados.org
Fri Sep 22 14:37:30 UTC 2023
at 25c0de8aa20f801212dda2dff23216289487d08f (commit)
commit 25c0de8aa20f801212dda2dff23216289487d08f
Author: Stephen Smith <stephen at curii.com>
Date: Fri Sep 22 10:35:34 2023 -0400
20225: Correct cypress test names / add terminal newline (style)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/cypress/integration/banner-tooltip.spec.js b/cypress/integration/banner-tooltip.spec.js
index df84abe9..295bc380 100644
--- a/cypress/integration/banner-tooltip.spec.js
+++ b/cypress/integration/banner-tooltip.spec.js
@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Collection panel tests', function () {
+describe('Banner / tooltip tests', function () {
let activeUser;
let adminUser;
let collectionUUID;
diff --git a/cypress/integration/create-workflow.spec.js b/cypress/integration/create-workflow.spec.js
index 0fe7e341..df50a875 100644
--- a/cypress/integration/create-workflow.spec.js
+++ b/cypress/integration/create-workflow.spec.js
@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Multi-file deletion tests', function () {
+describe('Create workflow tests', function () {
let activeUser;
let adminUser;
diff --git a/src/services/collection-service/collection-service-files-response.ts b/src/services/collection-service/collection-service-files-response.ts
index b7e1f9c7..db56e317 100644
--- a/src/services/collection-service/collection-service-files-response.ts
+++ b/src/services/collection-service/collection-service-files-response.ts
@@ -60,4 +60,4 @@ export const extractFilesData = (document: Document) => {
export const getFileFullPath = ({ name, path }: CollectionFile | CollectionDirectory) => {
return `${path}/${name}`;
-};
\ No newline at end of file
+};
commit f63db3e0224e57695adb2c5fa975586065f6669f
Author: Stephen Smith <stephen at curii.com>
Date: Fri Sep 22 10:35:13 2023 -0400
20225: Remove unused imports / dead code
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts
index e3fb2eb3..bf82fac1 100644
--- a/src/store/resources/resources.ts
+++ b/src/store/resources/resources.ts
@@ -4,7 +4,6 @@
import { Resource, EditableResource } from "models/resource";
import { ResourceKind } from 'models/resource';
-import { ProjectResource } from "models/project";
import { GroupResource } from "models/group";
export type ResourcesState = { [key: string]: Resource };
diff --git a/src/store/run-process-panel/run-process-panel-actions.ts b/src/store/run-process-panel/run-process-panel-actions.ts
index 67a89e4c..000f0cd9 100644
--- a/src/store/run-process-panel/run-process-panel-actions.ts
+++ b/src/store/run-process-panel/run-process-panel-actions.ts
@@ -103,7 +103,6 @@ export const setWorkflow = (workflow: WorkflowResource, isWorkflowChanged = true
const advancedFormValues = getWorkflowRunnerSettings(workflow);
let owner = getResource<ProjectResource | UserResource>(getState().runProcessPanel.processOwnerUuid)(getState().resources);
- const userUuid = getUserUuid(getState());
if (!owner || !owner.canWrite) {
owner = undefined;
}
diff --git a/src/store/workflow-panel/workflow-panel-actions.ts b/src/store/workflow-panel/workflow-panel-actions.ts
index 94b35078..d8c3b651 100644
--- a/src/store/workflow-panel/workflow-panel-actions.ts
+++ b/src/store/workflow-panel/workflow-panel-actions.ts
@@ -23,7 +23,6 @@ import { RUN_PROCESS_ADVANCED_FORM } from 'views/run-process-panel/run-process-a
import { getResource } from 'store/resources/resources';
import { ProjectResource } from 'models/project';
import { UserResource } from 'models/user';
-import { getUserUuid } from "common/getuser";
import { getWorkflowInputs, parseWorkflowDefinition } from 'models/workflow';
export const WORKFLOW_PANEL_ID = "workflowPanel";
@@ -63,7 +62,6 @@ export const openRunProcess = (workflowUuid: string, ownerUuid?: string, name?:
let owner;
if (ownerUuid) {
// Must be writable.
- const userUuid = getUserUuid(getState());
owner = getResource<ProjectResource | UserResource>(ownerUuid)(getState().resources);
if (!owner || !owner.canWrite) {
owner = undefined;
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 25130407..013ec9b2 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -57,7 +57,6 @@ import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel
import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
import { PermissionLevel } from 'models/permission';
import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
-import { getUserUuid } from 'common/getuser';
import { VirtualMachinesResource } from 'models/virtual-machines';
import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
import { ProjectResource } from 'models/project';
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index 15e008d1..c2cc0e2a 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -39,16 +39,6 @@ const mapStateToProps = (state: RootState): AccountMenuProps => ({
localCluster: state.auth.localCluster
});
-const wb1URL = (route: string) => {
- const r = route.replace(/^\//, "");
- if (r.match(/^(projects|collections)\//)) {
- return r;
- } else if (r.match(/^processes\//)) {
- return r.replace(/^processes/, "container_requests");
- }
- return "";
-};
-
type CssRules = 'link';
const styles: StyleRulesCallback<CssRules> = () => ({
commit 95bfd77307f5d4571bbcf01565361ec991bc6575
Author: Stephen Smith <stephen at curii.com>
Date: Fri Sep 22 10:12:42 2023 -0400
20225: Add subdirectory selection support to directory array picker
Adds cascade flag to tree picker to disable recursive directory selection
Reworks initProjectsTreePicker to support initializing multiple selections.
Loads each selection's ancestor tree in parallel, combines updates to same tree
pickers before inserting the ancestor tree, then loads necessary collections in
parallel
Changes checkbox visibility logic to show collection selection checkbox even
when collection is not loaded/expanded when cascade mode is off - since the
selection won't cascade there is no need to require the collection to be opened
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx
index 7b97865b..d52b58f5 100644
--- a/src/components/data-table-filters/data-table-filters-tree.tsx
+++ b/src/components/data-table-filters/data-table-filters-tree.tsx
@@ -59,14 +59,14 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
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>) => {
diff --git a/src/models/tree.ts b/src/models/tree.ts
index 996f98a4..aeb41541 100644
--- a/src/models/tree.ts
+++ b/src/models/tree.ts
@@ -138,6 +138,11 @@ export const deactivateNode = <T>(tree: Tree<T>) =>
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);
@@ -151,37 +156,40 @@ export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => <T>(tre
: 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>) =>
diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts
index de231d66..216a59c7 100644
--- a/src/store/resource-type-filters/resource-type-filters.test.ts
+++ b/src/store/resource-type-filters/resource-type-filters.test.ts
@@ -35,7 +35,7 @@ describe("serializeResourceTypeFilters", () => {
});
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]`);
@@ -44,11 +44,11 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -59,11 +59,11 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -74,10 +74,10 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -88,10 +88,10 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -102,12 +102,12 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -118,9 +118,9 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -131,10 +131,10 @@ describe("serializeResourceTypeFilters", () => {
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);
@@ -145,10 +145,10 @@ describe("serializeResourceTypeFilters", () => {
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);
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index 18385e31..7b526710 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -25,6 +25,7 @@ import { GroupClass, GroupResource } from "models/group";
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 }>(),
@@ -32,11 +33,12 @@ export const treePickerActions = unionize({
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 }>()
});
@@ -91,7 +93,14 @@ export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Va
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));
@@ -100,8 +109,14 @@ export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: stri
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
+ ));
}
};
@@ -239,13 +254,15 @@ export const loadCollection = (id: string, pickerId: string, includeDirectories?
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 }));
}
}
@@ -291,35 +308,125 @@ export const initSharedProject = (pickerId: string) =>
}));
};
-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) => {
@@ -561,18 +668,33 @@ export const getFileOperationLocation = (item: ProjectsTreePickerItem) =>
/**
* 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;
+}
diff --git a/src/store/tree-picker/tree-picker-reducer.test.ts b/src/store/tree-picker/tree-picker-reducer.test.ts
index 25973bf6..2a5229ca 100644
--- a/src/store/tree-picker/tree-picker-reducer.test.ts
+++ b/src/store/tree-picker/tree-picker-reducer.test.ts
@@ -93,7 +93,7 @@ describe('TreePickerReducer', () => {
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' }),
diff --git a/src/store/tree-picker/tree-picker-reducer.ts b/src/store/tree-picker/tree-picker-reducer.ts
index df0ee0ad..84d5ed0c 100644
--- a/src/store/tree-picker/tree-picker-reducer.ts
+++ b/src/store/tree-picker/tree-picker-reducer.ts
@@ -5,7 +5,7 @@
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";
@@ -29,6 +29,9 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
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(
@@ -41,14 +44,14 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
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),
diff --git a/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx b/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
index 1ed2a551..70797f31 100644
--- a/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
@@ -21,6 +21,7 @@ import { CollectionFileType } from 'models/collection-file';
type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
export interface ProjectsTreePickerDataProps {
+ cascadeSelection: boolean;
includeCollections?: boolean;
includeDirectories?: boolean;
includeFiles?: boolean;
@@ -35,9 +36,9 @@ export interface ProjectsTreePickerDataProps {
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 => ({
@@ -71,7 +72,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollectio
}
},
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);
}
@@ -108,11 +109,14 @@ const getProjectPickerIcon = ({ data }: TreeItem<ProjectsTreePickerItem>, rootIc
}
};
-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;
}
diff --git a/src/views-components/projects-tree-picker/projects-tree-picker.tsx b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
index 773230d3..16f6cceb 100644
--- a/src/views-components/projects-tree-picker/projects-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
@@ -23,8 +23,9 @@ import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
export interface ToplevelPickerProps {
- currentUuid?: string;
+ currentUuids?: string[];
pickerId: string;
+ cascadeSelection: boolean;
includeCollections?: boolean;
includeDirectories?: boolean;
includeFiles?: boolean;
@@ -107,7 +108,13 @@ export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
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: "" }));
@@ -135,6 +142,7 @@ export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
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,
diff --git a/src/views-components/projects-tree-picker/tree-picker-field.tsx b/src/views-components/projects-tree-picker/tree-picker-field.tsx
index 793eeaa3..75cf40c6 100644
--- a/src/views-components/projects-tree-picker/tree-picker-field.tsx
+++ b/src/views-components/projects-tree-picker/tree-picker-field.tsx
@@ -19,6 +19,7 @@ export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp)
<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'>
@@ -37,6 +38,7 @@ export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdPro
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
+ cascadeSelection={false}
options={{ showOnlyOwned: false, showOnlyWritable: true }}
includeCollections />
{props.meta.dirty && props.meta.error &&
@@ -69,9 +71,10 @@ export const DirectoryTreePickerField = connect(null, projectsTreePickerMapDispa
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 />
diff --git a/src/views/run-process-panel/inputs/directory-array-input.tsx b/src/views/run-process-panel/inputs/directory-array-input.tsx
index 27255bd9..dd5bb2f8 100644
--- a/src/views/run-process-panel/inputs/directory-array-input.tsx
+++ b/src/views/run-process-panel/inputs/directory-array-input.tsx
@@ -15,7 +15,7 @@ import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divid
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';
@@ -26,8 +26,11 @@ 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';
+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 };
@@ -45,26 +48,35 @@ export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) =>
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,
@@ -79,11 +91,10 @@ const required = (value?: Directory[]) =>
: ERROR_MESSAGE;
interface DirectoryArrayInputComponentState {
open: boolean;
- directories: CollectionResource[];
- prevDirectories: CollectionResource[];
+ directories: FileOperationLocation[];
}
-interface DirectoryArrayInputComponentProps {
+interface DirectoryArrayInputDataProps {
treePickerState: TreePicker;
}
@@ -93,21 +104,39 @@ const mapStateToProps = createStructuredSelector({
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() {
@@ -118,7 +147,6 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
}
openDialog = () => {
- this.setDirectoriesFromProps(this.props.input.value);
this.setState({ open: true });
}
@@ -131,82 +159,52 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
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}
@@ -265,14 +263,17 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
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>
diff --git a/src/views/run-process-panel/inputs/directory-input.tsx b/src/views/run-process-panel/inputs/directory-input.tsx
index bd9dc67e..63c990fa 100644
--- a/src/views/run-process-panel/inputs/directory-input.tsx
+++ b/src/views/run-process-panel/inputs/directory-input.tsx
@@ -149,6 +149,7 @@ const DirectoryInputComponent = connect(null, mapDispatchToProps)(
pickerId={this.props.commandInput.id}
includeCollections
includeDirectories
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setDirectory} />
</div>
diff --git a/src/views/run-process-panel/inputs/file-array-input.tsx b/src/views/run-process-panel/inputs/file-array-input.tsx
index 1e1a4299..99338738 100644
--- a/src/views/run-process-panel/inputs/file-array-input.tsx
+++ b/src/views/run-process-panel/inputs/file-array-input.tsx
@@ -144,7 +144,9 @@ const FileArrayInputComponent = connect(mapStateToProps)(
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,
})
);
});
@@ -164,7 +166,9 @@ const FileArrayInputComponent = connect(mapStateToProps)(
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,
})
);
});
@@ -257,6 +261,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
includeDirectories
includeFiles
showSelection
+ cascadeSelection={true}
options={this.props.options}
toggleItemSelection={this.refreshFiles} />
</div>
diff --git a/src/views/run-process-panel/inputs/file-input.tsx b/src/views/run-process-panel/inputs/file-input.tsx
index 5f48f837..6970e2a5 100644
--- a/src/views/run-process-panel/inputs/file-input.tsx
+++ b/src/views/run-process-panel/inputs/file-input.tsx
@@ -144,6 +144,7 @@ const FileInputComponent = connect()(
includeCollections
includeDirectories
includeFiles
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setFile} />
</div>
diff --git a/src/views/run-process-panel/inputs/project-input.tsx b/src/views/run-process-panel/inputs/project-input.tsx
index d91a6b84..438bbe8e 100644
--- a/src/views/run-process-panel/inputs/project-input.tsx
+++ b/src/views/run-process-panel/inputs/project-input.tsx
@@ -140,6 +140,7 @@ export const ProjectInputComponent = connect(mapStateToProps)(
<div className={classes.pickerWrapper}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setProject} />
</div>
commit 4c34de655fc7f8839b205b48f460a168f302dd63
Author: Stephen Smith <stephen at curii.com>
Date: Thu Sep 21 15:02:10 2023 -0400
20225: Add subdirectory selection support to directory input
Converts getFileOperationLocation into a dispatchable helper so it can access
the store to augment FileOperationLocations with a PDH
Converts DirectoryTreePickerField into a connected class component in order to
use getFileOperationLocation
Change DirectoryInputComponent to include directories in the picker and use
getFileOperationLocation to pass FileOperationLocation instead of Collection
objects so that the subpath within the collection can be embedded in the
Directory object
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index 4b3af2a2..a0933c64 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -156,7 +156,13 @@ export const copyCollectionPartialToExistingCollection = (fileSelection: Collect
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
// Copy files
- const updatedCollection = await services.collectionService.copyFiles(fileSelection.collection.portableDataHash, fileSelection.selectedPaths, {uuid: formData.destination.uuid}, formData.destination.path || '/', false);
+ const updatedCollection = await services.collectionService.copyFiles(
+ fileSelection.collection.portableDataHash,
+ fileSelection.selectedPaths,
+ {uuid: formData.destination.uuid},
+ formData.destination.subpath || '/',
+ false
+ );
dispatch(updateResources([updatedCollection]));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
index 92e20981..56f7302d 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -158,7 +158,7 @@ export const moveCollectionPartialToExistingCollection = (fileSelection: Collect
fileSelection.collection.portableDataHash,
fileSelection.selectedPaths,
{uuid: formData.destination.uuid},
- formData.destination.path || '/', false
+ formData.destination.subpath || '/', false
);
dispatch(updateResources([updatedCollection]));
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index 72d1cb65..18385e31 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -23,6 +23,8 @@ import { mapTreeValues } from "models/tree";
import { sortFilesTree } from "services/collection-service/collection-service-files-response";
import { GroupClass, GroupResource } from "models/group";
import { CollectionResource } from "models/collection";
+import { getResource } from "store/resources/resources";
+import { updateResources } from "store/resources/resources-actions";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
@@ -167,6 +169,7 @@ export const loadProject = (params: LoadProjectParamsWithId) =>
const itemLimit = 200;
const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
+ dispatch<any>(updateResources(items));
if (itemsAvailable > itemLimit) {
items.push({
@@ -523,29 +526,38 @@ const buildParams = (ownerUuid: string) => {
* if the item represents a valid target/destination location
*/
export type FileOperationLocation = {
+ name: string;
uuid: string;
- path: string;
+ pdh?: string;
+ subpath: string;
}
-export const getFileOperationLocation = (item: ProjectsTreePickerItem): FileOperationLocation | undefined => {
- if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
- return {
- uuid: item.uuid,
- path: '/'
- };
- } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
- const uuid = getCollectionResourceCollectionUuid(item.id);
- if (uuid) {
+export const getFileOperationLocation = (item: ProjectsTreePickerItem) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<FileOperationLocation | undefined> => {
+ if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
return {
- uuid,
- path: [item.path, item.name].join('/')
+ name: item.name,
+ uuid: item.uuid,
+ pdh: item.portableDataHash,
+ subpath: '/',
};
- } else {
- return undefined;
+ } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
+ const uuid = getCollectionResourceCollectionUuid(item.id);
+ if (uuid) {
+ const collection = getResource<CollectionResource>(uuid)(getState().resources);
+ if (collection) {
+ const itemPath = [item.path, item.name].join('/');
+
+ return {
+ name: item.name,
+ uuid,
+ pdh: collection.portableDataHash,
+ subpath: itemPath,
+ };
+ }
+ }
}
- } else {
return undefined;
- }
-};
+ };
/**
* Create an expanded tree picker subtree from array of nested projects/collection
diff --git a/src/views-components/form-fields/collection-form-fields.tsx b/src/views-components/form-fields/collection-form-fields.tsx
index 23a44965..7d5fcf80 100644
--- a/src/views-components/form-fields/collection-form-fields.tsx
+++ b/src/views-components/form-fields/collection-form-fields.tsx
@@ -65,7 +65,7 @@ export const DirectoryPickerField = (props: PickerIdProp) =>
<Field
name="destination"
pickerId={props.pickerId}
- component={DirectoryTreePickerField}
+ component={DirectoryTreePickerField as any}
validate={validateDirectory} />;
interface StorageClassesProps {
diff --git a/src/views-components/projects-tree-picker/tree-picker-field.tsx b/src/views-components/projects-tree-picker/tree-picker-field.tsx
index 17417bf5..793eeaa3 100644
--- a/src/views-components/projects-tree-picker/tree-picker-field.tsx
+++ b/src/views-components/projects-tree-picker/tree-picker-field.tsx
@@ -9,7 +9,9 @@ import { WrappedFieldProps } from 'redux-form';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { PickerIdProp } from 'store/tree-picker/picker-id';
-import { getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
+import { FileOperationLocation, getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
<div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
@@ -44,24 +46,40 @@ export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdPro
</div>
</div>;
-const handleDirectoryChange = (props: WrappedFieldProps) =>
- (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
- props.input.onChange(getFileOperationLocation(data) || '');
- }
+type ProjectsTreePickerActionProps = {
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
-export const DirectoryTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
- <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
- <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
- <ProjectsTreePicker
- currentUuid={props.input.value.uuid}
- pickerId={props.pickerId}
- toggleItemActive={handleDirectoryChange(props)}
- options={{ showOnlyOwned: false, showOnlyWritable: true }}
- includeCollections
- includeDirectories />
- {props.meta.dirty && props.meta.error &&
- <Typography variant='caption' color='error'>
- {props.meta.error}
- </Typography>}
- </div>
- </div>;
+const projectsTreePickerMapDispatchToProps = (dispatch: Dispatch): ProjectsTreePickerActionProps => ({
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+type ProjectsTreePickerCombinedProps = ProjectsTreePickerActionProps & WrappedFieldProps & PickerIdProp;
+
+export const DirectoryTreePickerField = connect(null, projectsTreePickerMapDispatchToProps)(
+ class DirectoryTreePickerFieldComponent extends React.Component<ProjectsTreePickerCombinedProps> {
+
+ handleDirectoryChange = (props: WrappedFieldProps) =>
+ async (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
+ const location = await this.props.getFileOperationLocation(data);
+ props.input.onChange(location || '');
+ }
+
+ render() {
+ 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}
+ pickerId={this.props.pickerId}
+ toggleItemActive={this.handleDirectoryChange(this.props)}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }}
+ includeCollections
+ includeDirectories />
+ {this.props.meta.dirty && this.props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {this.props.meta.error}
+ </Typography>}
+ </div>
+ </div>;
+ }
+ });
diff --git a/src/views/run-process-panel/inputs/directory-input.tsx b/src/views/run-process-panel/inputs/directory-input.tsx
index 5348cc2b..bd9dc67e 100644
--- a/src/views/run-process-panel/inputs/directory-input.tsx
+++ b/src/views/run-process-panel/inputs/directory-input.tsx
@@ -15,12 +15,11 @@ import {
} from 'models/workflow';
import { GenericInputProps, GenericInput } from './generic-input';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { FileOperationLocation, getFileOperationLocation, initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
import { TreeItem } from 'components/tree/tree';
import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
-import { CollectionResource } from 'models/collection';
-import { ResourceKind } from 'models/resource';
import { ERROR_MESSAGE } from 'validators/require';
+import { Dispatch } from 'redux';
export interface DirectoryInputProps {
input: DirectoryCommandInputParameter;
@@ -43,9 +42,9 @@ export const DirectoryInput = ({ input, options }: DirectoryInputProps) =>
const format = (value?: Directory) => value ? value.basename : '';
-const parse = (directory: CollectionResource): Directory => ({
+const parse = (directory: FileOperationLocation): Directory => ({
class: CWLType.DIRECTORY,
- location: `keep:${directory.portableDataHash}`,
+ location: `keep:${directory.pdh}${directory.subpath}`,
basename: directory.name,
});
@@ -59,11 +58,21 @@ const getValidation = memoize(
interface DirectoryInputComponentState {
open: boolean;
- directory?: CollectionResource;
+ directory?: FileOperationLocation;
}
-const DirectoryInputComponent = connect()(
- class FileInputComponent extends React.Component<GenericInputProps & DispatchProp & {
+interface DirectoryInputActionProps {
+ initProjectsTreePicker: (pickerId: string) => void;
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DirectoryInputActionProps => ({
+ initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+const DirectoryInputComponent = connect(null, mapDispatchToProps)(
+ class FileInputComponent extends React.Component<GenericInputProps & DirectoryInputActionProps & DispatchProp & {
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}, DirectoryInputComponentState> {
state: DirectoryInputComponentState = {
@@ -71,8 +80,7 @@ const DirectoryInputComponent = connect()(
};
componentDidMount() {
- this.props.dispatch<any>(
- initProjectsTreePicker(this.props.commandInput.id));
+ this.props.initProjectsTreePicker(this.props.commandInput.id);
}
render() {
@@ -95,12 +103,9 @@ const DirectoryInputComponent = connect()(
this.props.input.onChange(this.state.directory);
}
- setDirectory = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
- if ('kind' in data && data.kind === ResourceKind.COLLECTION) {
- this.setState({ directory: data });
- } else {
- this.setState({ directory: undefined });
- }
+ setDirectory = async (_: {}, { data: item }: TreeItem<ProjectsTreePickerItem>) => {
+ const location = await this.props.getFileOperationLocation(item);
+ this.setState({ directory: location });
}
renderInput() {
@@ -143,6 +148,7 @@ const DirectoryInputComponent = connect()(
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
includeCollections
+ includeDirectories
options={this.props.options}
toggleItemActive={this.setDirectory} />
</div>
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list