Merge branch 'main' into 21357-favorites-names
authorLisa Knox <lisaknox83@gmail.com>
Tue, 19 Mar 2024 18:41:12 +0000 (14:41 -0400)
committerLisa Knox <lisaknox83@gmail.com>
Tue, 19 Mar 2024 18:41:12 +0000 (14:41 -0400)
refs #21357

Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>

services/workbench2/src/common/link-update-name.ts [new file with mode: 0644]
services/workbench2/src/components/tree/tree.tsx
services/workbench2/src/services/services.ts
services/workbench2/src/store/collections/collection-update-actions.ts
services/workbench2/src/store/side-panel-tree/side-panel-tree-actions.ts
services/workbench2/src/views-components/tree-picker/tree-picker.ts

diff --git a/services/workbench2/src/common/link-update-name.ts b/services/workbench2/src/common/link-update-name.ts
new file mode 100644 (file)
index 0000000..d9a04df
--- /dev/null
@@ -0,0 +1,74 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkResource } from 'models/link';
+import { Dispatch } from 'redux';
+import { RootState } from 'store/store';
+import { ServiceRepository, getResourceService } from 'services/services';
+import { Resource, extractUuidKind } from 'models/resource';
+
+type NameableResource = Resource & { name?: string };
+
+export const verifyAndUpdateLink = async (link: LinkResource, dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<LinkResource> => {
+    //head resource should already be in the store
+    let headResource: Resource | undefined = getState().resources[link.headUuid];
+    //if not, fetch it
+    if (!headResource) {
+        headResource = await fetchResource(link.headUuid)(dispatch, getState, services);
+        if (!headResource) {
+            if (!link.name) console.error('Could not validate link', link, 'because link head', link.headUuid, 'is not available');
+            return link;
+        }
+    }
+
+    if (validateLinkNameProp(link, headResource) === true) return link;
+
+    const updatedLink = updateLinkNameProp(link, headResource);
+    updateRemoteLinkName(updatedLink)(dispatch, getState, services);
+
+    return updatedLink;
+};
+
+export const verifyAndUpdateLinks = async (links: LinkResource[], dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    
+    const updatedLinks = links.map((link) => verifyAndUpdateLink(link, dispatch, getState, services));
+        return Promise.all(updatedLinks);
+};
+
+const fetchResource = (uuid: string, showErrors?: boolean) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+        const kind = extractUuidKind(uuid);
+        const service = getResourceService(kind)(services);
+        if (service) {
+            const resource = await service.get(uuid, showErrors);
+            return resource;
+        }
+    } catch (e) {
+        console.error(`Could not fetch resource ${uuid}`, e);
+    }
+    return undefined;
+};
+
+const validateLinkNameProp = (link: LinkResource, head: NameableResource) => {
+    if (!link.name || link.name !== head.name) return false;
+    return true;
+};
+
+const updateLinkNameProp = (link: LinkResource, head: NameableResource) => {
+    const updatedLink = { ...link };
+    if (head.name) updatedLink.name = head.name;
+    return updatedLink;
+};
+
+const updateRemoteLinkName = (link: LinkResource) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+        const kind = extractUuidKind(link.uuid);
+        const service = getResourceService(kind)(services);
+        if (service) {
+            service.update(link.uuid, { name: link.name });
+        }
+    } catch (error) {
+        console.error('Could not update link name', link, error);
+    }
+};
index 11a9540290cc21eb9aa3995f93124608775f0254..1f7aa8326d2774720d782469cf27bdfb7af01846 100644 (file)
@@ -252,7 +252,7 @@ const FlatTree = (props: FlatTreeProps) =>
     >
         {
             (props.it.items || [])
-                .map((item: any) => <div key={item.id} data-id={item.id}
+                .map((item: any, index: number) => <div key={item.id || index} data-id={item.id}
                     className={classnames(props.classes.childItem, { [props.classes.active]: item.active })}
                     style={{ paddingLeft: `${item.depth * props.levelIndentation}px` }}>
                     {isInFavoritesTree(props.it) ? 
index cd04a65feff5286be41bf030fdcf241da8bf0a14..48cd931127d995b094f441250bf8baf14c525ad9 100644 (file)
@@ -131,6 +131,8 @@ export const getResourceService = (kind?: ResourceKind) => (serviceRepository: S
             return serviceRepository.groupsService;
         case ResourceKind.COLLECTION:
             return serviceRepository.collectionService;
+        case ResourceKind.LINK:
+            return serviceRepository.linkService;
         default:
             return undefined;
     }
index d955c9478d3d6d3198a2a249e94a142cd8c5a77a..f6e52a4f80ad45d537e94b924c0cc230dc79819f 100644 (file)
@@ -22,6 +22,7 @@ import { updateResources } from "../resources/resources-actions";
 import { loadDetailsPanel } from "../details-panel/details-panel-action";
 import { getResource } from "store/resources/resources";
 import { CollectionProperties } from "./collection-create-actions";
+import { loadSidePanelTreeProjects, SidePanelTreeCategory } from "store/side-panel-tree/side-panel-tree-actions";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
@@ -65,6 +66,7 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
             }));
             dispatch<any>(updateResources([updatedCollection]));
             dispatch<any>(loadDetailsPanel(updatedCollection.uuid));
+            dispatch<any>(loadSidePanelTreeProjects(SidePanelTreeCategory.FAVORITES))
         }).catch (e => {
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             const error = getCommonResourceServiceError(e);
index 3fad5f587a3c41586784987d4925ae01a39d2134..c5221399784ee3a8033c715f43bca71116ae5da6 100644 (file)
@@ -16,7 +16,8 @@ import { OrderBuilder } from 'services/api/order-builder';
 import { ResourceKind } from 'models/resource';
 import { CategoriesListReducer } from 'common/plugintypes';
 import { pluginConfig } from 'plugins';
-import { LinkClass } from 'models/link';
+import { LinkClass, LinkResource } from 'models/link';
+import { verifyAndUpdateLinks } from 'common/link-update-name';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Home Projects',
@@ -100,9 +101,12 @@ export const loadSidePanelTreeProjects = (projectUuid: string) =>
         const treePicker = getTreePicker(SIDE_PANEL_TREE)(getState().treePicker);
         const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
         if (projectUuid === SidePanelTreeCategory.PUBLIC_FAVORITES) {
-            await dispatch<any>(loadPublicFavoritesTree());
+            const unverifiedPubFaves = await dispatch<any>(loadPublicFavoritesTree());
+            verifyAndUpdateLinkNames(unverifiedPubFaves, dispatch, getState, services);
         } else if (projectUuid === SidePanelTreeCategory.FAVORITES) {
-            await dispatch<any>(loadFavoritesTree());
+            const unverifiedFaves = await dispatch<any>(loadFavoritesTree());
+            await setFaves(unverifiedFaves, dispatch, getState, services);
+            verifyAndUpdateLinkNames(unverifiedFaves, dispatch, getState, services);
         } else if (node || projectUuid !== '') {
             await dispatch<any>(loadProject(projectUuid));
         }
@@ -154,7 +158,46 @@ export const loadFavoritesTree = () => async (dispatch: Dispatch, getState: () =
         })
     );
 
-    dispatch(resourcesActions.SET_RESOURCES(items));
+    return items;
+};
+
+const setFaves = async(links: LinkResource[], dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+    const uuids = links.map(it => it.headUuid);
+    const groupItems: Promise<any> = services.groupsService.list({
+        filters: new FilterBuilder()
+            .addIn("uuid", uuids)
+            .getFilters()
+    });
+    const collectionItems: Promise<any> = services.collectionService.list({
+        filters: new FilterBuilder()
+            .addIn("uuid", uuids)
+            .getFilters()
+    });
+    const processItems: Promise<any> = services.containerRequestService.list({
+        filters: new FilterBuilder()
+            .addIn("uuid", uuids)
+            .getFilters()
+    });
+
+    const resolvedItems = await Promise.all([groupItems, collectionItems, processItems]);
+
+    const responseItems = resolvedItems.reduce((acc, response) => acc.concat(response.items), []);
+
+    //setting resources here so they won't be re-fetched in validation step
+    dispatch(resourcesActions.SET_RESOURCES(responseItems));
+};
+
+const verifyAndUpdateLinkNames = async (links: LinkResource[], dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    const verfifiedLinks = await verifyAndUpdateLinks(links, dispatch, getState, services);
+
+    dispatch(
+        treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            id: SidePanelTreeCategory.FAVORITES,
+            pickerId: SIDE_PANEL_TREE,
+            nodes: verfifiedLinks.map(item => initTreeNode({ id: item.headUuid, value: item })),
+        })
+    );
 };
 
 export const loadPublicFavoritesTree = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -176,15 +219,44 @@ export const loadPublicFavoritesTree = () => async (dispatch: Dispatch, getState
 
     const { items } = await services.linkService.list(params);
 
+    const uuids = items.map(it => it.headUuid);
+    const groupItems: Promise<any> = services.groupsService.list({
+        filters: new FilterBuilder()
+            .addIn("uuid", uuids)
+            .addIsA("uuid", typeFilters)
+            .getFilters()
+    });
+    const collectionItems: Promise<any> = services.collectionService.list({
+        filters: new FilterBuilder()
+            .addIn("uuid", uuids)
+            .addIsA("uuid", typeFilters)
+            .getFilters()
+    });
+    const processItems: Promise<any> = services.containerRequestService.list({
+        filters: new FilterBuilder()
+            .addIn("uuid", uuids)
+            .addIsA("uuid", typeFilters)
+            .getFilters()
+    });
+
+    const resolvedItems = await Promise.all([groupItems, collectionItems, processItems]);
+
+    const responseItems = resolvedItems.reduce((acc, response) => acc.concat(response.items), []);
+
+    const filteredItems = items.filter(item => responseItems.some(responseItem => responseItem.uuid === item.headUuid));
+
     dispatch(
         treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
             id: SidePanelTreeCategory.PUBLIC_FAVORITES,
             pickerId: SIDE_PANEL_TREE,
-            nodes: items.map(item => initTreeNode({ id: item.headUuid, value: item })),
+            nodes: filteredItems.map(item => initTreeNode({ id: item.headUuid, value: item })),
         })
     );
 
-    dispatch(resourcesActions.SET_RESOURCES(items));
+    //setting resources here so they won't be re-fetched in validation step
+    dispatch(resourcesActions.SET_RESOURCES(responseItems));
+
+    return filteredItems;
 };
 
 export const activateSidePanelTreeItem = (id: string) =>
index a6fdfefec9d21d013f72a6e87ffe3ecbc6ca83fe..1b6d2a26fd0dff5dbbeb2d7442c83c9af945419a 100644 (file)
@@ -70,7 +70,7 @@ const treePickerToTreeItems = (tree: Ttree<any>, resources: ResourcesState) =>
         const resource = resources[node.id];
         return {
             active: node.active,
-            data: resource ? { ...resource, name: node.value.name || node.value } : undefined || node.value,
+            data: resource ? { ...resource, name: node.value.name || node.value } : node.value,
             id: node.id,
             items: items.length > 0 ? items : undefined,
             open: node.expanded,