From e43120c21f14f71ace96d1b258f559031a678060 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:39:22 +0200 Subject: [PATCH 1/9] Fix sub-collection drilling --- .../CurrentCollection/CollectionPanel.vue | 23 ++++++++++++------- .../services/datasetCollection.service.ts | 4 ++-- client/src/stores/services/index.ts | 21 ++++++++++++++++- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/client/src/components/History/CurrentCollection/CollectionPanel.vue b/client/src/components/History/CurrentCollection/CollectionPanel.vue index 9a48db2afdd6..5f7f7d0d5c90 100644 --- a/client/src/components/History/CurrentCollection/CollectionPanel.vue +++ b/client/src/components/History/CurrentCollection/CollectionPanel.vue @@ -7,7 +7,7 @@ import ExpandedItems from "@/components/History/Content/ExpandedItems"; import { updateContentFields } from "@/components/History/model/queries"; import { useCollectionElementsStore } from "@/stores/collectionElementsStore"; import { HistorySummary } from "@/stores/historyStore"; -import { DCESummary, DCObject, HDCASummary } from "@/stores/services"; +import { CollectionEntry, DCESummary, DCObject, HDCASummary } from "@/stores/services"; import CollectionDetails from "./CollectionDetails.vue"; import CollectionNavigation from "./CollectionNavigation.vue"; @@ -17,7 +17,7 @@ import ListingLayout from "@/components/History/Layout/ListingLayout.vue"; interface Props { history: HistorySummary; - selectedCollections: HDCASummary[]; + selectedCollections: CollectionEntry[]; showControls?: boolean; filterable?: boolean; } @@ -30,8 +30,8 @@ const props = withDefaults(defineProps(), { const collectionElementsStore = useCollectionElementsStore(); const emit = defineEmits<{ - (e: "view-collection", collection: HDCASummary): void; - (e: "update:selected-collections", collections: HDCASummary[]): void; + (e: "view-collection", collection: CollectionEntry): void; + (e: "update:selected-collections", collections: CollectionEntry[]): void; }>(); const offset = ref(0); @@ -40,7 +40,7 @@ const dsc = computed(() => props.selectedCollections[props.selectedCollections.l const collectionElements = computed(() => collectionElementsStore.getCollectionElements(dsc.value, offset.value)); const loading = computed(() => collectionElementsStore.isLoadingCollectionElements(dsc.value)); const jobState = computed(() => dsc.value?.job_state_summary); -const rootCollection = computed(() => props.selectedCollections[0]); +const rootCollection = computed(() => props.selectedCollections[0] as HDCASummary); const isRoot = computed(() => dsc.value == rootCollection.value); function updateDsc(collection: any, fields: Object | undefined) { @@ -59,9 +59,16 @@ function onScroll(newOffset: number) { offset.value = newOffset; } -async function onViewSubCollection(itemObject: DCObject) { - const collection = await collectionElementsStore.getCollection(itemObject.id); - emit("view-collection", collection); +async function onViewSubCollection(itemObject: DCObject, name: string) { + // We need to convert the DCO to a CollectionEntry in order + // to be able to fetch the contents of a nested collection. + const collectionEntry: CollectionEntry = { + id: rootCollection.value.id, + name, + collection_id: itemObject.id, + collection_type: itemObject.collection_type, + }; + emit("view-collection", collectionEntry); } watch( diff --git a/client/src/stores/services/datasetCollection.service.ts b/client/src/stores/services/datasetCollection.service.ts index 1e0522085ef0..14931c56300d 100644 --- a/client/src/stores/services/datasetCollection.service.ts +++ b/client/src/stores/services/datasetCollection.service.ts @@ -1,6 +1,6 @@ import { fetcher } from "@/schema"; -import { DCESummary, HDCADetailed, HDCASummary } from "."; +import { CollectionEntry, DCESummary, HDCADetailed } from "."; const DEFAULT_LIMIT = 50; @@ -33,7 +33,7 @@ export async function fetchCollectionElements(params: { } export async function fetchElementsFromHDCA(params: { - hdca: HDCASummary; + hdca: CollectionEntry; offset?: number; limit?: number; }): Promise { diff --git a/client/src/stores/services/index.ts b/client/src/stores/services/index.ts index 5c4053cbf0dd..120c796af4ef 100644 --- a/client/src/stores/services/index.ts +++ b/client/src/stores/services/index.ts @@ -1,9 +1,28 @@ import { components } from "@/schema"; +/** Minimal representation of a collection that can contain datasets or other collections. + * + * This is used as a common interface to be able to fetch the contents of a collection regardless + * of whether it is an HDCA (top level) or a nested collection (DCO). + * + * To convert a DCO to a CollectionEntry we need the parent HDCA ID and the name of the + * collection (DCE identifier). The collection_id is the ID of the DCO and collection_type as well. + */ +export interface CollectionEntry { + /**HDCA ID */ + id: string; + /**DCO ID */ + collection_id: string; + /**Name of the HDCA or DatasetCollectionElement identifier */ + name: string; + /**Type of the collection */ + collection_type: string; +} + export type DatasetSummary = components["schemas"]["HDASummary"]; export type DatasetDetails = components["schemas"]["HDADetailed"]; export type DCESummary = components["schemas"]["DCESummary"]; -export type HDCASummary = components["schemas"]["HDCASummary"]; +export type HDCASummary = components["schemas"]["HDCASummary"] & CollectionEntry; export type HDCADetailed = components["schemas"]["HDCADetailed"]; export type DCObject = components["schemas"]["DCObject"]; From 6b37f39f4ca52002d9950cc4e9e7d3925ad9c9b5 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:33:17 +0200 Subject: [PATCH 2/9] Use composed key to store/retrieve collection elements Using just the ID can create conflicts when storing sub-collection elements. --- client/src/stores/collectionElementsStore.ts | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/client/src/stores/collectionElementsStore.ts b/client/src/stores/collectionElementsStore.ts index b73366ccdabc..b21544ed005d 100644 --- a/client/src/stores/collectionElementsStore.ts +++ b/client/src/stores/collectionElementsStore.ts @@ -9,9 +9,16 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", const loadingCollectionElements = ref<{ [key: string]: boolean }>({}); const storedCollectionElements = ref<{ [key: string]: DCESummary[] }>({}); + /** + * Returns a key that can be used to store or retrieve the elements of a collection in the store. + */ + function getCollectionKey(collection: HDCASummary) { + return `${collection.id}-${collection.collection_id}`; + } + const getCollectionElements = computed(() => { return (collection: HDCASummary, offset = 0, limit = 50) => { - const elements = storedCollectionElements.value[collection.id] ?? []; + const elements = storedCollectionElements.value[getCollectionKey(collection)] ?? []; fetchMissingElements({ collection, offset, limit }); return elements ?? null; }; @@ -19,14 +26,15 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", const isLoadingCollectionElements = computed(() => { return (collection: HDCASummary) => { - return loadingCollectionElements.value[collection.id] ?? false; + return loadingCollectionElements.value[getCollectionKey(collection)] ?? false; }; }); async function fetchMissingElements(params: { collection: HDCASummary; offset: number; limit: number }) { + const collectionKey = getCollectionKey(params.collection); try { const maxElementCountInCollection = params.collection.element_count ?? 0; - const storedElements = storedCollectionElements.value[params.collection.id] ?? []; + const storedElements = storedCollectionElements.value[collectionKey] ?? []; // Collections are immutable, so there is no need to fetch elements if the range we want is already stored if (params.offset + params.limit <= storedElements.length) { return; @@ -38,22 +46,22 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", return; } - Vue.set(loadingCollectionElements.value, params.collection.id, true); + Vue.set(loadingCollectionElements.value, collectionKey, true); const fetchedElements = await Service.fetchElementsFromHDCA({ hdca: params.collection, offset: params.offset, limit: params.limit, }); const updatedElements = [...storedElements, ...fetchedElements]; - Vue.set(storedCollectionElements.value, params.collection.id, updatedElements); + Vue.set(storedCollectionElements.value, collectionKey, updatedElements); } finally { - Vue.delete(loadingCollectionElements.value, params.collection.id); + Vue.delete(loadingCollectionElements.value, collectionKey); } } async function loadCollectionElements(collection: HDCASummary) { const elements = await Service.fetchElementsFromHDCA({ hdca: collection }); - Vue.set(storedCollectionElements.value, collection.id, elements); + Vue.set(storedCollectionElements.value, getCollectionKey(collection), elements); } function saveCollections(historyContentsPayload: HistoryContentItemBase[]) { @@ -93,5 +101,6 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", fetchCollection, loadCollectionElements, saveCollections, + getCollectionKey, }; }); From 82a42b01b24fe3cb6d08514df5687c7c9a139246 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:41:47 +0200 Subject: [PATCH 3/9] Adapt unit tests to use new collection key --- client/src/stores/collectionElementsStore.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/src/stores/collectionElementsStore.test.ts b/client/src/stores/collectionElementsStore.test.ts index 9e8bf1d6e557..bcbcbeb4bb98 100644 --- a/client/src/stores/collectionElementsStore.test.ts +++ b/client/src/stores/collectionElementsStore.test.ts @@ -46,7 +46,8 @@ describe("useCollectionElementsStore", () => { expect(store.isLoadingCollectionElements(collection1)).toEqual(false); expect(fetchCollectionElements).toHaveBeenCalled(); - const elements = store.storedCollectionElements[collection1.id]; + const collection1Key = store.getCollectionKey(collection1); + const elements = store.storedCollectionElements[collection1Key]; expect(elements).toBeDefined(); expect(elements).toHaveLength(limit); }); @@ -55,8 +56,9 @@ describe("useCollectionElementsStore", () => { const store = useCollectionElementsStore(); const storedCount = 5; const expectedStoredElements = Array.from({ length: storedCount }, (_, i) => mockElement(collection1.id, i)); - store.storedCollectionElements[collection1.id] = expectedStoredElements; - expect(store.storedCollectionElements[collection1.id]).toHaveLength(storedCount); + const collection1Key = store.getCollectionKey(collection1); + store.storedCollectionElements[collection1Key] = expectedStoredElements; + expect(store.storedCollectionElements[collection1Key]).toHaveLength(storedCount); const offset = 0; const limit = 5; @@ -70,8 +72,9 @@ describe("useCollectionElementsStore", () => { const store = useCollectionElementsStore(); const storedCount = 3; const expectedStoredElements = Array.from({ length: storedCount }, (_, i) => mockElement(collection1.id, i)); - store.storedCollectionElements[collection1.id] = expectedStoredElements; - expect(store.storedCollectionElements[collection1.id]).toHaveLength(storedCount); + const collection1Key = store.getCollectionKey(collection1); + store.storedCollectionElements[collection1Key] = expectedStoredElements; + expect(store.storedCollectionElements[collection1Key]).toHaveLength(storedCount); const offset = 2; const limit = 5; @@ -82,7 +85,7 @@ describe("useCollectionElementsStore", () => { expect(store.isLoadingCollectionElements(collection1)).toEqual(false); expect(fetchCollectionElements).toHaveBeenCalled(); - const elements = store.storedCollectionElements[collection1.id]; + const elements = store.storedCollectionElements[collection1Key]; expect(elements).toBeDefined(); // The offset was overlapping with the stored elements, so it was increased by the number of stored elements // so it fetches the next "limit" number of elements From ebf9253debcf111db4ab3938cc269b5a2a31a41f Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:48:13 +0200 Subject: [PATCH 4/9] Use collection_id as key for storing/retrieving DCEs --- client/src/stores/collectionElementsStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/stores/collectionElementsStore.ts b/client/src/stores/collectionElementsStore.ts index b21544ed005d..110064d0893b 100644 --- a/client/src/stores/collectionElementsStore.ts +++ b/client/src/stores/collectionElementsStore.ts @@ -12,8 +12,8 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", /** * Returns a key that can be used to store or retrieve the elements of a collection in the store. */ - function getCollectionKey(collection: HDCASummary) { - return `${collection.id}-${collection.collection_id}`; + function getCollectionKey(collection: HDCASummary): string { + return collection.collection_id; } const getCollectionElements = computed(() => { From 9fc98f927fc7c78c105067ef9def1b6e9a633b53 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:21:26 +0200 Subject: [PATCH 5/9] Refactor collection related type definitions + docs --- client/src/stores/services/index.ts | 87 ++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/client/src/stores/services/index.ts b/client/src/stores/services/index.ts index 120c796af4ef..2db47ad58c12 100644 --- a/client/src/stores/services/index.ts +++ b/client/src/stores/services/index.ts @@ -1,31 +1,76 @@ import { components } from "@/schema"; -/** Minimal representation of a collection that can contain datasets or other collections. - * - * This is used as a common interface to be able to fetch the contents of a collection regardless - * of whether it is an HDCA (top level) or a nested collection (DCO). - * - * To convert a DCO to a CollectionEntry we need the parent HDCA ID and the name of the - * collection (DCE identifier). The collection_id is the ID of the DCO and collection_type as well. - */ -export interface CollectionEntry { - /**HDCA ID */ - id: string; - /**DCO ID */ - collection_id: string; - /**Name of the HDCA or DatasetCollectionElement identifier */ - name: string; - /**Type of the collection */ - collection_type: string; -} +/** + * Contains minimal information about a HistoryContentItem. + */ +export type HistoryContentItemBase = components["schemas"]["EncodedHistoryContentItem"]; +/** + * Contains summary information about a HistoryDatasetAssociation. + */ export type DatasetSummary = components["schemas"]["HDASummary"]; + +/** + * Contains additional details about a HistoryDatasetAssociation. + */ export type DatasetDetails = components["schemas"]["HDADetailed"]; + +/** + * Represents a HistoryDatasetAssociation with either summary or detailed information. + */ +export type DatasetEntry = DatasetSummary | DatasetDetails; + +/** + * Contains summary information about a DCE (DatasetCollectionElement). + * + * DCEs associate a parent collection to its elements. Those elements can be either + * HDAs or other DCs (DatasetCollections). + * The type of the element is indicated by the `element_type` field and the element + * itself is contained in the `object` field. + */ export type DCESummary = components["schemas"]["DCESummary"]; -export type HDCASummary = components["schemas"]["HDCASummary"] & CollectionEntry; + +/** + * Contains summary information about a HDCA (HistoryDatasetCollectionAssociation). + * + * HDCAs are (top level only) history items that contains information about the association + * between a History and a DatasetCollection. + */ +export type HDCASummary = components["schemas"]["HDCASummary"]; + +/** + * Contains additional details about a HistoryDatasetCollectionAssociation. + */ export type HDCADetailed = components["schemas"]["HDCADetailed"]; + +/** + * Contains information about a DatasetCollection. + * + * DatasetCollections are immutable and contain one or more DCEs. + */ export type DCObject = components["schemas"]["DCObject"]; -export type HistoryContentItemBase = components["schemas"]["EncodedHistoryContentItem"]; +/** + * Represents a SubCollection as a DatasetCollection with additional information to simplify its handling. + * + * This is used to be able to distinguish between top level HDCAs and sub-collections. + * It helps simplify both, the representation of sub-collections in the UI, and fetching of elements. + */ +export interface SubCollection extends DCObject { + /** The name of the collection. Usually corresponds to the DCE identifier. */ + name: string; + /** The ID of the top level HDCA that associates this collection with the History it belongs to. */ + hdca_id: string; +} -export type DatasetEntry = DatasetSummary | DatasetDetails; +/** + * Represents either a top level HDCASummary or a sub-collection. + */ +export type CollectionEntry = HDCASummary | SubCollection; + +/** + * Returns true if the given entry is a top level HDCA and false for sub-collections. + */ +export function isHDCA(entry: CollectionEntry): entry is HDCASummary { + return "history_content_type" in entry && entry.history_content_type === "dataset_collection"; +} From 41fc04e29daec094bac4291321cc0e497a33efb8 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:24:57 +0200 Subject: [PATCH 6/9] Convert DCObjects to SubCollections when drilling This is just to make the handling of top level HDCAs and sub-collections more consistent for fetching and storing elements in the store. --- .../History/CurrentCollection/CollectionPanel.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/components/History/CurrentCollection/CollectionPanel.vue b/client/src/components/History/CurrentCollection/CollectionPanel.vue index 5f7f7d0d5c90..1f50561a4a23 100644 --- a/client/src/components/History/CurrentCollection/CollectionPanel.vue +++ b/client/src/components/History/CurrentCollection/CollectionPanel.vue @@ -7,7 +7,7 @@ import ExpandedItems from "@/components/History/Content/ExpandedItems"; import { updateContentFields } from "@/components/History/model/queries"; import { useCollectionElementsStore } from "@/stores/collectionElementsStore"; import { HistorySummary } from "@/stores/historyStore"; -import { CollectionEntry, DCESummary, DCObject, HDCASummary } from "@/stores/services"; +import { CollectionEntry, DCESummary, DCObject, HDCASummary, SubCollection } from "@/stores/services"; import CollectionDetails from "./CollectionDetails.vue"; import CollectionNavigation from "./CollectionNavigation.vue"; @@ -60,13 +60,13 @@ function onScroll(newOffset: number) { } async function onViewSubCollection(itemObject: DCObject, name: string) { - // We need to convert the DCO to a CollectionEntry in order - // to be able to fetch the contents of a nested collection. - const collectionEntry: CollectionEntry = { - id: rootCollection.value.id, + // We need to convert the DCO to a SubCollection by providing + // some more context to represent the collection in the UI and + // fetch the elements of the collection in a consistent way. + const collectionEntry: SubCollection = { + ...itemObject, name, - collection_id: itemObject.id, - collection_type: itemObject.collection_type, + hdca_id: rootCollection.value.id, }; emit("view-collection", collectionEntry); } From e2fe35a6541d550422a9fe8bba0daae39edf1130 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:29:24 +0200 Subject: [PATCH 7/9] Consistently use DatasetCollection.id in the store For storing and retrieving collection elements we only need to use the DatasetCollection ID whether the collection entry is an HDCA or a sub-collection. --- client/src/stores/collectionElementsStore.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/client/src/stores/collectionElementsStore.ts b/client/src/stores/collectionElementsStore.ts index 110064d0893b..6d898f8812c2 100644 --- a/client/src/stores/collectionElementsStore.ts +++ b/client/src/stores/collectionElementsStore.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import Vue, { computed, ref } from "vue"; -import { DCESummary, HDCASummary, HistoryContentItemBase } from "./services"; +import { CollectionEntry, DCESummary, HDCASummary, HistoryContentItemBase, isHDCA } from "./services"; import * as Service from "./services/datasetCollection.service"; export const useCollectionElementsStore = defineStore("collectionElementsStore", () => { @@ -11,9 +11,14 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", /** * Returns a key that can be used to store or retrieve the elements of a collection in the store. + * + * It consistently returns a DatasetCollection ID for (top level) HDCAs or sub-collections. */ - function getCollectionKey(collection: HDCASummary): string { - return collection.collection_id; + function getCollectionKey(collection: CollectionEntry): string { + if (isHDCA(collection)) { + return collection.collection_id; + } + return collection.id; } const getCollectionElements = computed(() => { @@ -47,8 +52,8 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", } Vue.set(loadingCollectionElements.value, collectionKey, true); - const fetchedElements = await Service.fetchElementsFromHDCA({ - hdca: params.collection, + const fetchedElements = await Service.fetchElementsFromCollection({ + entry: params.collection, offset: params.offset, limit: params.limit, }); @@ -60,7 +65,7 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", } async function loadCollectionElements(collection: HDCASummary) { - const elements = await Service.fetchElementsFromHDCA({ hdca: collection }); + const elements = await Service.fetchElementsFromCollection({ entry: collection }); Vue.set(storedCollectionElements.value, getCollectionKey(collection), elements); } From 568748d07434c8f3bb55c0a3994c4797dfe2344b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:30:57 +0200 Subject: [PATCH 8/9] Consistently fetch collection elements Whether we are fetching elements for a top level HDCA or a sub-collection we need to provide the top HDCA ID and the DatasetCollection ID to fetch their elements. --- .../services/datasetCollection.service.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/client/src/stores/services/datasetCollection.service.ts b/client/src/stores/services/datasetCollection.service.ts index 14931c56300d..5eca0c4f861e 100644 --- a/client/src/stores/services/datasetCollection.service.ts +++ b/client/src/stores/services/datasetCollection.service.ts @@ -1,6 +1,6 @@ import { fetcher } from "@/schema"; -import { CollectionEntry, DCESummary, HDCADetailed } from "."; +import { CollectionEntry, DCESummary, HDCADetailed, isHDCA } from "."; const DEFAULT_LIMIT = 50; @@ -17,9 +17,13 @@ const getCollectionContents = fetcher .create(); export async function fetchCollectionElements(params: { + /** The ID of the top level HDCA that associates this collection with the History it belongs to. */ hdcaId: string; + /** The ID of the collection itself. */ collectionId: string; + /** The offset to start fetching elements from. */ offset?: number; + /** The maximum number of elements to fetch. */ limit?: number; }): Promise { const { data } = await getCollectionContents({ @@ -32,14 +36,19 @@ export async function fetchCollectionElements(params: { return data; } -export async function fetchElementsFromHDCA(params: { - hdca: CollectionEntry; +export async function fetchElementsFromCollection(params: { + /** The HDCA or sub-collection to fetch elements from. */ + entry: CollectionEntry; + /** The offset to start fetching elements from. */ offset?: number; + /** The maximum number of elements to fetch. */ limit?: number; }): Promise { + const hdcaId = isHDCA(params.entry) ? params.entry.id : params.entry.hdca_id; + const collectionId = isHDCA(params.entry) ? params.entry.collection_id : params.entry.id; return fetchCollectionElements({ - hdcaId: params.hdca.id, - collectionId: params.hdca.collection_id, + hdcaId: hdcaId, + collectionId: collectionId, offset: params.offset ?? 0, limit: params.limit ?? DEFAULT_LIMIT, }); From 12b939b381c50afa6d6bc303ab98534350e4fd29 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 10 Oct 2023 18:05:07 +0200 Subject: [PATCH 9/9] Refactor from code review - Make sure the computed properties are of the right type or error out. - Rename onViewSubCollection to onViewDatasetCollectionElement. --- .../CurrentCollection/CollectionPanel.vue | 38 ++++++++++++------- client/src/stores/collectionElementsStore.ts | 8 ++-- client/src/stores/services/index.ts | 24 ++++++++++-- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/client/src/components/History/CurrentCollection/CollectionPanel.vue b/client/src/components/History/CurrentCollection/CollectionPanel.vue index 1f50561a4a23..c5d24b753815 100644 --- a/client/src/components/History/CurrentCollection/CollectionPanel.vue +++ b/client/src/components/History/CurrentCollection/CollectionPanel.vue @@ -7,7 +7,7 @@ import ExpandedItems from "@/components/History/Content/ExpandedItems"; import { updateContentFields } from "@/components/History/model/queries"; import { useCollectionElementsStore } from "@/stores/collectionElementsStore"; import { HistorySummary } from "@/stores/historyStore"; -import { CollectionEntry, DCESummary, DCObject, HDCASummary, SubCollection } from "@/stores/services"; +import { CollectionEntry, DCESummary, isCollectionElement, isHDCA, SubCollection } from "@/stores/services"; import CollectionDetails from "./CollectionDetails.vue"; import CollectionNavigation from "./CollectionNavigation.vue"; @@ -36,11 +36,23 @@ const emit = defineEmits<{ const offset = ref(0); -const dsc = computed(() => props.selectedCollections[props.selectedCollections.length - 1] as HDCASummary); +const dsc = computed(() => { + const currentCollection = props.selectedCollections[props.selectedCollections.length - 1]; + if (currentCollection === undefined) { + throw new Error("No collection selected"); + } + return currentCollection; +}); const collectionElements = computed(() => collectionElementsStore.getCollectionElements(dsc.value, offset.value)); const loading = computed(() => collectionElementsStore.isLoadingCollectionElements(dsc.value)); -const jobState = computed(() => dsc.value?.job_state_summary); -const rootCollection = computed(() => props.selectedCollections[0] as HDCASummary); +const jobState = computed(() => ("job_state_summary" in dsc.value ? dsc.value.job_state_summary : undefined)); +const rootCollection = computed(() => { + if (isHDCA(props.selectedCollections[0])) { + return props.selectedCollections[0]; + } else { + throw new Error("Root collection must be an HistoryDatasetCollectionAssociation"); + } +}); const isRoot = computed(() => dsc.value == rootCollection.value); function updateDsc(collection: any, fields: Object | undefined) { @@ -59,16 +71,16 @@ function onScroll(newOffset: number) { offset.value = newOffset; } -async function onViewSubCollection(itemObject: DCObject, name: string) { - // We need to convert the DCO to a SubCollection by providing - // some more context to represent the collection in the UI and - // fetch the elements of the collection in a consistent way. - const collectionEntry: SubCollection = { - ...itemObject, - name, +async function onViewDatasetCollectionElement(element: DCESummary) { + if (!isCollectionElement(element)) { + return; + } + const collection: SubCollection = { + ...element.object, + name: element.element_identifier, hdca_id: rootCollection.value.id, }; - emit("view-collection", collectionEntry); + emit("view-collection", collection); } watch( @@ -113,7 +125,7 @@ watch( :is-dataset="item.element_type == 'hda'" :filterable="filterable" @update:expand-dataset="setExpanded(item, $event)" - @view-collection="onViewSubCollection" /> + @view-collection="onViewDatasetCollectionElement(item)" /> diff --git a/client/src/stores/collectionElementsStore.ts b/client/src/stores/collectionElementsStore.ts index 6d898f8812c2..da14fddf97d0 100644 --- a/client/src/stores/collectionElementsStore.ts +++ b/client/src/stores/collectionElementsStore.ts @@ -22,7 +22,7 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", } const getCollectionElements = computed(() => { - return (collection: HDCASummary, offset = 0, limit = 50) => { + return (collection: CollectionEntry, offset = 0, limit = 50) => { const elements = storedCollectionElements.value[getCollectionKey(collection)] ?? []; fetchMissingElements({ collection, offset, limit }); return elements ?? null; @@ -30,12 +30,12 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", }); const isLoadingCollectionElements = computed(() => { - return (collection: HDCASummary) => { + return (collection: CollectionEntry) => { return loadingCollectionElements.value[getCollectionKey(collection)] ?? false; }; }); - async function fetchMissingElements(params: { collection: HDCASummary; offset: number; limit: number }) { + async function fetchMissingElements(params: { collection: CollectionEntry; offset: number; limit: number }) { const collectionKey = getCollectionKey(params.collection); try { const maxElementCountInCollection = params.collection.element_count ?? 0; @@ -64,7 +64,7 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore", } } - async function loadCollectionElements(collection: HDCASummary) { + async function loadCollectionElements(collection: CollectionEntry) { const elements = await Service.fetchElementsFromCollection({ entry: collection }); Vue.set(storedCollectionElements.value, getCollectionKey(collection), elements); } diff --git a/client/src/stores/services/index.ts b/client/src/stores/services/index.ts index 2db47ad58c12..2efb3132897e 100644 --- a/client/src/stores/services/index.ts +++ b/client/src/stores/services/index.ts @@ -30,6 +30,14 @@ export type DatasetEntry = DatasetSummary | DatasetDetails; */ export type DCESummary = components["schemas"]["DCESummary"]; +/** + * DatasetCollectionElement specific type for collections. + */ +export interface DCECollection extends DCESummary { + element_type: "dataset_collection"; + object: DCObject; +} + /** * Contains summary information about a HDCA (HistoryDatasetCollectionAssociation). * @@ -51,7 +59,8 @@ export type HDCADetailed = components["schemas"]["HDCADetailed"]; export type DCObject = components["schemas"]["DCObject"]; /** - * Represents a SubCollection as a DatasetCollection with additional information to simplify its handling. + * A SubCollection is a DatasetCollectionElement of type `dataset_collection` + * with additional information to simplify its handling. * * This is used to be able to distinguish between top level HDCAs and sub-collections. * It helps simplify both, the representation of sub-collections in the UI, and fetching of elements. @@ -71,6 +80,15 @@ export type CollectionEntry = HDCASummary | SubCollection; /** * Returns true if the given entry is a top level HDCA and false for sub-collections. */ -export function isHDCA(entry: CollectionEntry): entry is HDCASummary { - return "history_content_type" in entry && entry.history_content_type === "dataset_collection"; +export function isHDCA(entry?: CollectionEntry): entry is HDCASummary { + return ( + entry !== undefined && "history_content_type" in entry && entry.history_content_type === "dataset_collection" + ); +} + +/** + * Returns true if the given element of a collection is a DatasetCollection. + */ +export function isCollectionElement(element: DCESummary): element is DCECollection { + return element.element_type === "dataset_collection"; }