diff --git a/packages/common-ui/lib/list-page/useElasticSearchDistinctTerm.tsx b/packages/common-ui/lib/list-page/useElasticSearchDistinctTerm.tsx index b9a2036a71..d88e69c37a 100644 --- a/packages/common-ui/lib/list-page/useElasticSearchDistinctTerm.tsx +++ b/packages/common-ui/lib/list-page/useElasticSearchDistinctTerm.tsx @@ -1,50 +1,57 @@ import Bodybuilder from "bodybuilder"; -import { castArray } from "lodash"; +import { castArray, pick } from "lodash"; import { useEffect, useState } from "react"; import { useApiClient, useQueryBuilderContext } from ".."; - const TOTAL_SUGGESTIONS: number = 100; const FILTER_AGGREGATION_NAME: string = "included_type_filter"; const AGGREGATION_NAME: string = "term_aggregation"; const NEST_AGGREGATION_NAME: string = "included_aggregation"; - interface QuerySuggestionFieldProps { /** The string you want elastic search to use. */ fieldName?: string; - /** If the field is a relationship, we need to know the type to filter it. */ relationshipType?: string; - /** The index you want elastic search to perform the search on */ indexName: string; - /** Used to determine if ".keyword" should be appended to the field name. */ keywordMultiFieldSupport: boolean; -} + /** Used to determine if field is an array in the back end. */ + isFieldArray?: boolean; + + /** User input used to query against */ + inputValue?: string; + + /** Group to be queried to only show the users most used values. */ + groupNames?: string[]; + + /** Number of suggestions */ + size?: number; +} export function useElasticSearchDistinctTerm({ fieldName, relationshipType, indexName, - keywordMultiFieldSupport + keywordMultiFieldSupport, + isFieldArray = false, + inputValue, + groupNames, + size }: QuerySuggestionFieldProps) { const { apiClient } = useApiClient(); - const [suggestions, setSuggestions] = useState([]); - - const { groups } = useQueryBuilderContext(); - + const { groups } = groupNames + ? { groups: groupNames } + : useQueryBuilderContext(); // Every time the textEntered has changed, perform a new request for new suggestions. useEffect(() => { if (!fieldName) return; queryElasticSearchForSuggestions(); - }, [fieldName, relationshipType, groups]); - + }, [fieldName, relationshipType, groups, inputValue]); async function queryElasticSearchForSuggestions() { // Use bodybuilder to generate the query to send to elastic search. const builder = Bodybuilder(); builder.size(0); - // Group needs to be queried to only show the users most used values. if (groups && groups.length !== 0) { // terms is used to be able to support multiple groups. @@ -54,7 +61,6 @@ export function useElasticSearchDistinctTerm({ castArray(groups) ); } - // If the field has a relationship type, we need to do a nested query to filter it. if (relationshipType) { builder.aggregation( @@ -83,16 +89,27 @@ export function useElasticSearchDistinctTerm({ ); } else { // If it's an attribute, no need to use nested filters. - builder.aggregation( - "terms", - fieldName + (keywordMultiFieldSupport ? ".keyword" : ""), - { - size: TOTAL_SUGGESTIONS - }, - AGGREGATION_NAME - ); + if (isFieldArray) { + builder.aggregation( + "terms", + fieldName + (keywordMultiFieldSupport ? ".keyword" : ""), + { + size: size ?? TOTAL_SUGGESTIONS, + include: `.*${inputValue}.*` + }, + AGGREGATION_NAME + ); + } else { + builder.aggregation( + "terms", + fieldName + (keywordMultiFieldSupport ? ".keyword" : ""), + { + size: TOTAL_SUGGESTIONS + }, + AGGREGATION_NAME + ); + } } - await apiClient.axios .post(`search-api/search-ws/search`, builder.build(), { params: { @@ -115,9 +132,7 @@ export function useElasticSearchDistinctTerm({ } return undefined; }; - let suggestionArray: string[] | undefined; - // The path to the results in the response changes if it contains the nested aggregation. if (relationshipType) { suggestionArray = findTermAggregationKey( @@ -136,7 +151,6 @@ export function useElasticSearchDistinctTerm({ AGGREGATION_NAME )?.buckets?.map((bucket) => bucket.key); } - if (suggestionArray !== undefined) { setSuggestions(suggestionArray); } else { @@ -149,6 +163,5 @@ export function useElasticSearchDistinctTerm({ setSuggestions([]); }); } - return suggestions; } diff --git a/packages/dina-ui/components/collection/collecting-event/CollectingEventFormLayout.tsx b/packages/dina-ui/components/collection/collecting-event/CollectingEventFormLayout.tsx index a08cd902fd..b15d4c281b 100644 --- a/packages/dina-ui/components/collection/collecting-event/CollectingEventFormLayout.tsx +++ b/packages/dina-ui/components/collection/collecting-event/CollectingEventFormLayout.tsx @@ -15,6 +15,7 @@ import { StringArrayField, TextField, TextFieldWithCoordButtons, + Tooltip, filterBy, useDinaFormContext, useInstanceContext @@ -32,7 +33,8 @@ import { NotPubliclyReleasableWarning, ParseVerbatimToRangeButton, PersonSelectField, - TagsAndRestrictionsSection + TagsAndRestrictionsSection, + TagSelectReadOnly } from "../.."; import { ManagedAttributesEditor } from "../../"; import { DinaMessage, useDinaIntl } from "../../../intl/dina-ui-intl"; @@ -602,7 +604,20 @@ export function CollectingEventFormLayout({ sectionName="general-section" > - + {readOnly ? ( + + ) : ( + + } + /> + )}
diff --git a/packages/dina-ui/components/collection/material-sample/MaterialSampleForm.tsx b/packages/dina-ui/components/collection/material-sample/MaterialSampleForm.tsx index 277752ca06..d4a545f86f 100644 --- a/packages/dina-ui/components/collection/material-sample/MaterialSampleForm.tsx +++ b/packages/dina-ui/components/collection/material-sample/MaterialSampleForm.tsx @@ -472,7 +472,10 @@ export function MaterialSampleForm({ - +
diff --git a/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx b/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx index e6d05ef0a2..d305a70cbe 100644 --- a/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx +++ b/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx @@ -234,9 +234,14 @@ export function useMetagenomicsWorkflowMolecularAnalysisRun({ const { bulkGet, save } = useApiClient(); const { formatMessage } = useDinaIntl(); const { compareByStringAndNumber } = useStringComparator(); + // Map of MolecularAnalysisRunItem {id:name} + const [molecularAnalysisRunItemNames, setMolecularAnalysisRunItemNames] = + useState>({}); const columns = getMolecularAnalysisRunColumns( compareByStringAndNumber, - "metagenomics-batch-item" + "metagenomics-batch-item", + setMolecularAnalysisRunItemNames, + !editMode ); // Used to display if the network calls are still in progress. @@ -253,8 +258,13 @@ export function useMetagenomicsWorkflowMolecularAnalysisRun({ const [sequencingRunItems, setSequencingRunItems] = useState(); + // Used to determine if the resource needs to be reloaded. + const [reloadResource, setReloadResource] = useState(Date.now()); + // Network Requests, starting with the SeqReaction - useQuery( + const { loading: loadingMetagenomicsBatchItems } = useQuery< + MetagenomicsBatchItem[] + >( { filter: filterBy([], { extraFilters: [ @@ -271,6 +281,7 @@ export function useMetagenomicsWorkflowMolecularAnalysisRun({ "indexI5,indexI7,pcrBatchItem,molecularAnalysisRunItem,molecularAnalysisRunItem.run" }, { + deps: [reloadResource], onSuccess: async ({ data: metagenomicsBatchItems }) => { /** * Go through each of the MetagenomicsBatchItems and retrieve the Molecular Analysis Run. There @@ -390,21 +401,29 @@ export function useMetagenomicsWorkflowMolecularAnalysisRun({ // Create a MolecularAnalysisRunitem for each MetagenomicsBatchItem. const molecularAnalysisRunItemSaveArgs: SaveArgs[] = - sequencingRunItems.map(() => ({ - type: "molecular-analysis-run-item", - resource: { + sequencingRunItems.map((item) => { + const molecularAnalysisRunItemName = item.materialSampleSummary?.id + ? molecularAnalysisRunItemNames[item.materialSampleSummary?.id] + : undefined; + return { type: "molecular-analysis-run-item", - usageType: "metagenomics-batch-item", - relationships: { - run: { - data: { - id: savedMolecularAnalysisRun[0].id, - type: "molecular-analysis-run" + resource: { + type: "molecular-analysis-run-item", + usageType: "metagenomics-batch-item", + ...(molecularAnalysisRunItemName && { + name: molecularAnalysisRunItemName + }), + relationships: { + run: { + data: { + id: savedMolecularAnalysisRun[0].id, + type: "molecular-analysis-run" + } } } - } - } as any - })); + } as any + }; + }); const savedMolecularAnalysisRunItem = await save( molecularAnalysisRunItemSaveArgs, { apiBaseUrl: "/seqdb-api" } @@ -481,10 +500,37 @@ export function useMetagenomicsWorkflowMolecularAnalysisRun({ apiBaseUrl: "/seqdb-api" }); + // Update existing MolecularAnalysisRunItem names + if (sequencingRunItems) { + const molecularAnalysisRunItemSaveArgs: SaveArgs[] = + []; + sequencingRunItems.forEach((item) => { + const molecularAnalysisRunItemName = item.materialSampleSummary?.id + ? molecularAnalysisRunItemNames[item.materialSampleSummary?.id] + : undefined; + if (molecularAnalysisRunItemName) { + molecularAnalysisRunItemSaveArgs.push({ + type: "molecular-analysis-run-item", + resource: { + id: item.molecularAnalysisRunItemId, + type: "molecular-analysis-run-item", + name: molecularAnalysisRunItemName + } + }); + } + }); + if (molecularAnalysisRunItemSaveArgs.length) { + await save(molecularAnalysisRunItemSaveArgs, { + apiBaseUrl: "/seqdb-api" + }); + } + } + // Go back to view mode once completed. setPerformSave(false); setEditMode(false); setLoading(false); + setReloadResource(Date.now()); } catch (error) { console.error("Error updating sequencing run: ", error); setPerformSave(false); @@ -527,7 +573,7 @@ export function useMetagenomicsWorkflowMolecularAnalysisRun({ }, [performSave, loading]); return { - loading, + loading: loading || loadingMetagenomicsBatchItems, errorMessage, multipleRunWarning, sequencingRunName, diff --git a/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx b/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx index 2679f0ee62..cb28028385 100644 --- a/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx +++ b/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx @@ -257,9 +257,15 @@ export function useMolecularAnalysisRun({ const { bulkGet, save, apiClient } = useApiClient(); const { formatMessage } = useDinaIntl(); const { compareByStringAndNumber } = useStringComparator(); + // Map of MolecularAnalysisRunItem {id:name} + const [molecularAnalysisRunItemNames, setMolecularAnalysisRunItemNames] = + useState>({}); + const columns = getMolecularAnalysisRunColumns( compareByStringAndNumber, - "seq-reaction" + "seq-reaction", + setMolecularAnalysisRunItemNames, + !editMode ); // Used to display if the network calls are still in progress. @@ -281,8 +287,11 @@ export function useMolecularAnalysisRun({ [] ); + // Used to determine if the resource needs to be reloaded. + const [reloadResource, setReloadResource] = useState(Date.now()); + // Network Requests, starting with the SeqReaction - useQuery( + const { loading: loadingSeqReactions } = useQuery( { filter: filterBy([], { extraFilters: [ @@ -299,6 +308,7 @@ export function useMolecularAnalysisRun({ "storageUnitUsage,molecularAnalysisRunItem,molecularAnalysisRunItem.run,pcrBatchItem,seqPrimer" }, { + deps: [reloadResource], onSuccess: async ({ data: seqReactions }) => { /** * Go through each of the SeqReactions and retrieve the Molecular Analysis Run. There @@ -441,21 +451,29 @@ export function useMolecularAnalysisRun({ // Create a run item for each seq reaction. const molecularAnalysisRunItemSaveArgs: SaveArgs[] = - sequencingRunItems.map(() => ({ - type: "molecular-analysis-run-item", - resource: { + sequencingRunItems.map((item) => { + const molecularAnalysisRunItemName = item.materialSampleSummary?.id + ? molecularAnalysisRunItemNames[item.materialSampleSummary?.id] + : undefined; + return { type: "molecular-analysis-run-item", - usageType: "seq-reaction", - relationships: { - run: { - data: { - id: savedMolecularAnalysisRun[0].id, - type: "molecular-analysis-run" + resource: { + type: "molecular-analysis-run-item", + usageType: "seq-reaction", + ...(molecularAnalysisRunItemName && { + name: molecularAnalysisRunItemName + }), + relationships: { + run: { + data: { + id: savedMolecularAnalysisRun[0].id, + type: "molecular-analysis-run" + } } } - } - } as any - })); + } as any + }; + }); const savedMolecularAnalysisRunItem = await save( molecularAnalysisRunItemSaveArgs, { apiBaseUrl: "/seqdb-api" } @@ -537,10 +555,37 @@ export function useMolecularAnalysisRun({ apiBaseUrl: "/seqdb-api" }); + // Update existing MolecularAnalysisRunItem names + if (sequencingRunItems) { + const molecularAnalysisRunItemSaveArgs: SaveArgs[] = + []; + sequencingRunItems.forEach((item) => { + const molecularAnalysisRunItemName = item.materialSampleSummary?.id + ? molecularAnalysisRunItemNames[item.materialSampleSummary?.id] + : undefined; + if (molecularAnalysisRunItemName) { + molecularAnalysisRunItemSaveArgs.push({ + type: "molecular-analysis-run-item", + resource: { + id: item.molecularAnalysisRunItemId, + type: "molecular-analysis-run-item", + name: molecularAnalysisRunItemName + } + }); + } + }); + if (molecularAnalysisRunItemSaveArgs.length) { + await save(molecularAnalysisRunItemSaveArgs, { + apiBaseUrl: "/seqdb-api" + }); + } + } + // Go back to view mode once completed. setPerformSave(false); setEditMode(false); setLoading(false); + setReloadResource(Date.now()); } catch (error) { console.error("Error updating sequencing run: ", error); setPerformSave(false); @@ -583,7 +628,7 @@ export function useMolecularAnalysisRun({ }, [performSave, loading]); return { - loading, + loading: loading || loadingSeqReactions, errorMessage, multipleRunWarning, sequencingRunName, @@ -888,6 +933,45 @@ export function getMolecularAnalysisRunColumns( b?.original?.materialSampleSummary?.materialSampleName ), enableSorting: true + }, + { + id: "molecularAnalysisRunItem.name", + cell: ({ row: { original } }) => { + return readOnly ? ( + <>{original.molecularAnalysisRunItem?.name} + ) : ( + ) => { + setMolecularAnalysisRunItemNames?.( + (molecularAnalysisRunItemNames) => { + const molecularAnalysisRunItemNamesMap = + molecularAnalysisRunItemNames; + if ( + original?.materialSampleSummary?.id && + event.target.value + ) { + molecularAnalysisRunItemNamesMap[ + original?.materialSampleSummary?.id + ] = event.target.value; + } + return molecularAnalysisRunItemNamesMap; + } + ); + }} + /> + ); + }, + header: () => , + accessorKey: "molecularAnalysisRunItem.name", + sortingFn: (a: any, b: any): number => + compareByStringAndNumber( + a?.original?.materialSampleSummary?.materialSampleName, + b?.original?.materialSampleSummary?.materialSampleName + ), + enableSorting: true } ]; @@ -1075,6 +1159,45 @@ export function getMolecularAnalysisRunColumns( b?.original?.materialSampleSummary?.materialSampleName ), enableSorting: true + }, + { + id: "molecularAnalysisRunItem.name", + cell: ({ row: { original } }) => { + return readOnly ? ( + <>{original.molecularAnalysisRunItem?.name} + ) : ( + ) => { + setMolecularAnalysisRunItemNames?.( + (molecularAnalysisRunItemNames) => { + const molecularAnalysisRunItemNamesMap = + molecularAnalysisRunItemNames; + if ( + original?.materialSampleSummary?.id && + event.target.value + ) { + molecularAnalysisRunItemNamesMap[ + original?.materialSampleSummary?.id + ] = event.target.value; + } + return molecularAnalysisRunItemNamesMap; + } + ); + }} + /> + ); + }, + header: () => , + accessorKey: "molecularAnalysisRunItem.name", + sortingFn: (a: any, b: any): number => + compareByStringAndNumber( + a?.original?.materialSampleSummary?.materialSampleName, + b?.original?.materialSampleSummary?.materialSampleName + ), + enableSorting: true } ]; const MOLECULAR_ANALYSIS_RUN_COLUMNS_MAP = { diff --git a/packages/dina-ui/components/object-store/attachment-list/__tests__/AttachmentUploader.test.tsx b/packages/dina-ui/components/object-store/attachment-list/__tests__/AttachmentUploader.test.tsx index 60aa1bb001..761144979f 100644 --- a/packages/dina-ui/components/object-store/attachment-list/__tests__/AttachmentUploader.test.tsx +++ b/packages/dina-ui/components/object-store/attachment-list/__tests__/AttachmentUploader.test.tsx @@ -3,15 +3,19 @@ import { AttachmentUploader } from "../AttachmentUploader"; import { screen, waitFor, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; -const mockPost = jest.fn(() => { - return { - data: { - dateTimeDigitized: "2003-12-14T12:01:44", - fileIdentifier: "c0f78fce-1825-4c4e-89c7-92fe0ed9dc73", - fileType: "text", - size: "500" - } - }; +const mockPost = jest.fn((path) => { + if (path === "search-api/search-ws/search") { + return new Promise((resolve) => resolve); + } else { + return { + data: { + dateTimeDigitized: "2003-12-14T12:01:44", + fileIdentifier: "c0f78fce-1825-4c4e-89c7-92fe0ed9dc73", + fileType: "text", + size: "500" + } + }; + } }); const mockGet = jest.fn((path) => { if (path === "objectstore-api/config/default-values") { diff --git a/packages/dina-ui/components/object-store/metadata/MetadataForm.tsx b/packages/dina-ui/components/object-store/metadata/MetadataForm.tsx index 47682384a2..14d7be1eab 100644 --- a/packages/dina-ui/components/object-store/metadata/MetadataForm.tsx +++ b/packages/dina-ui/components/object-store/metadata/MetadataForm.tsx @@ -83,6 +83,7 @@ export function MetadataForm({ resourcePath="objectstore-api/metadata" tagsFieldName="acTags" groupSelectorName="bucket" + indexName="dina_object_store_index" />
}>
diff --git a/packages/dina-ui/components/seqdb/seq-workflow/__mocks__/SangerRunStepMocks.ts b/packages/dina-ui/components/seqdb/seq-workflow/__mocks__/SangerRunStepMocks.ts index 68f5b8d86c..2184dee816 100644 --- a/packages/dina-ui/components/seqdb/seq-workflow/__mocks__/SangerRunStepMocks.ts +++ b/packages/dina-ui/components/seqdb/seq-workflow/__mocks__/SangerRunStepMocks.ts @@ -176,6 +176,7 @@ export const SEQ_REACTIONS: KitsuResponse = { molecularAnalysisRunItem: { id: "cd8c4d28-586a-45c0-8f27-63030aba07cf", type: "molecular-analysis-run-item", + name: "Provided run item name", usageType: "seq-reaction", createdBy: "dina-admin", createdOn: "2024-11-05T15:29:30.230786Z", diff --git a/packages/dina-ui/components/seqdb/seq-workflow/__tests__/SangerRunStep.test.tsx b/packages/dina-ui/components/seqdb/seq-workflow/__tests__/SangerRunStep.test.tsx index e7bcdce453..e669a97e4e 100644 --- a/packages/dina-ui/components/seqdb/seq-workflow/__tests__/SangerRunStep.test.tsx +++ b/packages/dina-ui/components/seqdb/seq-workflow/__tests__/SangerRunStep.test.tsx @@ -1,7 +1,11 @@ import { mountWithAppContext2 } from "../../../../../dina-ui/test-util/mock-app-context"; import { SangerRunStep, SangerRunStepProps } from "../SangerRunStep"; import { noop } from "lodash"; -import { waitFor, waitForElementToBeRemoved } from "@testing-library/react"; +import { + waitFor, + waitForElementToBeRemoved, + screen +} from "@testing-library/react"; import "@testing-library/jest-dom"; import { MATERIAL_SAMPLE_SUMMARY_1, @@ -152,7 +156,10 @@ describe("Sanger Run Step from Sanger Workflow", () => { expect(wrapper.queryByRole("alert")).not.toBeInTheDocument(); // Run name should be in the textbox. - expect(wrapper.getByRole("textbox")).toHaveDisplayValue("run-name-1"); + const sequencingRunNameInput = wrapper.container.querySelector( + 'input[name="sequencingRunName"]' + ); + expect(sequencingRunNameInput).toHaveDisplayValue("run-name-1"); // Ensure Primary IDs are rendered in the table with links: expect( @@ -192,6 +199,11 @@ describe("Sanger Run Step from Sanger Workflow", () => { expect(wrapper.getByRole("cell", { name: "A2" })).toBeInTheDocument(); expect(wrapper.getByRole("cell", { name: "A3" })).toBeInTheDocument(); + // Ensure the run item names are shown: + expect(wrapper.getAllByRole("textbox")[1]).toHaveDisplayValue( + "Provided run item name" + ); + // Ensure attachment appears. expect( wrapper.getByRole("heading", { @@ -226,7 +238,10 @@ describe("Sanger Run Step from Sanger Workflow", () => { ).toBeInTheDocument(); // Run name should be in the textbox for the first run found. - expect(wrapper.getByRole("textbox")).toHaveDisplayValue("run-name-1"); + const sequencingRunNameInput = wrapper.container.querySelector( + 'input[name="sequencingRunName"]' + ); + expect(sequencingRunNameInput).toHaveDisplayValue("run-name-1"); // Set edit mode should not be triggered in this test. expect(mockSetEditMode).toBeCalledTimes(0); @@ -270,7 +285,10 @@ describe("Sanger Run Step from Sanger Workflow", () => { }); // Expect the Sequencing run to be empty since no run exists yet. - expect(wrapper.getByRole("textbox")).toHaveDisplayValue(""); + const sequencingRunNameInput = wrapper.container.querySelector( + 'input[name="sequencingRunName"]' + ); + expect(sequencingRunNameInput).toHaveDisplayValue(""); // Try saving with no sequencing run name, it should report an error. userEvent.click(wrapper.getByRole("button", { name: /save/i })); @@ -284,7 +302,11 @@ describe("Sanger Run Step from Sanger Workflow", () => { ).toBeInTheDocument(); // Type a name for the run to be created. - userEvent.type(wrapper.getByRole("textbox"), "My new run"); + userEvent.type(sequencingRunNameInput!, "My new run"); + + // Enter in names for the run items: + userEvent.type(wrapper.getAllByRole("textbox")[1], "Run item name 1"); + userEvent.type(wrapper.getAllByRole("textbox")[2], "Run item name 2"); // Click the save button. userEvent.click(wrapper.getByRole("button", { name: /save/i })); @@ -319,6 +341,7 @@ describe("Sanger Run Step from Sanger Workflow", () => { [ { resource: { + name: "Run item name 1", relationships: { run: { data: { @@ -334,6 +357,7 @@ describe("Sanger Run Step from Sanger Workflow", () => { }, { resource: { + name: "Run item name 2", relationships: { run: { data: { @@ -441,8 +465,15 @@ describe("Sanger Run Step from Sanger Workflow", () => { expect(wrapper.queryByText(/edit mode: true/i)).toBeInTheDocument(); // Change the sequencing run name to something different. - userEvent.clear(wrapper.getByRole("textbox")); - userEvent.type(wrapper.getByRole("textbox"), "Updated run name"); + const sequencingRunNameInput = wrapper.container.querySelector( + 'input[name="sequencingRunName"]' + ); + userEvent.clear(sequencingRunNameInput!); + userEvent.type(sequencingRunNameInput!, "Updated run name"); + + // Update the two run iten name + userEvent.clear(wrapper.getAllByRole("textbox")[1]); + userEvent.type(wrapper.getAllByRole("textbox")[1], "Run item name 1"); // Click the save button. userEvent.click(wrapper.getByRole("button", { name: /save/i })); @@ -454,9 +485,6 @@ describe("Sanger Run Step from Sanger Workflow", () => { expect(wrapper.queryByRole("alert")).not.toBeInTheDocument(); expect(wrapper.queryByText(/edit mode: false/i)).toBeInTheDocument(); - // Name should have not changed from edit mode true to false. - expect(wrapper.queryByText(/updated run name/i)).toBeInTheDocument(); - // Expect the network request to only contain the update of the run. expect(mockSave.mock.calls).toEqual([ [ @@ -483,6 +511,23 @@ describe("Sanger Run Step from Sanger Workflow", () => { { apiBaseUrl: "/seqdb-api" } + ], + + // Update the run item names + [ + [ + { + resource: { + id: "cd8c4d28-586a-45c0-8f27-63030aba07cf", + name: "Run item name 1", + type: "molecular-analysis-run-item" + }, + type: "molecular-analysis-run-item" + } + ], + { + apiBaseUrl: "/seqdb-api" + } ] ]); }); diff --git a/packages/dina-ui/components/tag-editor/TagSelectField.tsx b/packages/dina-ui/components/tag-editor/TagSelectField.tsx index f913df9db5..c271c71a56 100644 --- a/packages/dina-ui/components/tag-editor/TagSelectField.tsx +++ b/packages/dina-ui/components/tag-editor/TagSelectField.tsx @@ -3,18 +3,21 @@ import { FieldWrapperProps, Tooltip, filterBy, + rsql, useAccount, useQuery } from "common-ui"; import { KitsuResource } from "kitsu"; import { compact, last, uniq, get } from "lodash"; -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { AiFillTag } from "react-icons/ai"; import { components as reactSelectComponents } from "react-select"; import CreatableSelect from "react-select/creatable"; import { SortableContainer, SortableElement } from "react-sortable-hoc"; import { useDinaIntl } from "../../intl/dina-ui-intl"; import { useFormikContext } from "formik"; +import { useElasticSearchDistinctTerm } from "../../../common-ui/lib/list-page/useElasticSearchDistinctTerm"; +import { useDebounce } from "use-debounce"; export interface TagSelectFieldProps extends FieldWrapperProps { /** The API path to search for previous tags. */ @@ -22,6 +25,7 @@ export interface TagSelectFieldProps extends FieldWrapperProps { groupSelectorName?: string; /** The field name to use when finding other tags via RSQL filter. */ tagsFieldName?: string; + indexName?: string; } export interface TagSelectOption { @@ -33,6 +37,7 @@ export interface TagSelectOption { export function TagSelectField({ resourcePath, tagsFieldName, + indexName, ...props }: TagSelectFieldProps) { return ( @@ -70,6 +75,7 @@ export function TagSelectField({ groupSelectorName={props.groupSelectorName} placeholder={placeholder} tagsFieldName={tagsFieldName} + indexName={indexName} /> )} @@ -84,6 +90,7 @@ interface TagSelectProps { tagsFieldName?: string; groupSelectorName?: string; placeholder?: string; + indexName?: string; } /** Tag Select/Create field. */ @@ -94,55 +101,75 @@ function TagSelect({ invalid, groupSelectorName = "group", tagsFieldName = "tags", - placeholder + placeholder, + indexName }: TagSelectProps) { const { formatMessage } = useDinaIntl(); const { isAdmin, groupNames } = useAccount(); /** The value of the input element. */ const [inputValue, setInputValue] = useState(""); + /** The debounced input value passed to the fetcher. */ + const [debouncedInputValue] = useDebounce(inputValue, 250); + const tagOptions = useRef([]); + const isLoading = useRef(false); const typeName = last(resourcePath?.split("/")); - const filter = filterBy( - [tagsFieldName], - !isAdmin - ? { - extraFilters: [ - // Restrict the list to just the user's groups: - { - selector: groupSelectorName, - comparison: "=in=", - arguments: groupNames || [] - } - ] - } - : undefined - ); - - const { loading, response } = useQuery( - { - path: resourcePath ?? "", - sort: "-createdOn", - fields: typeName ? { [typeName]: tagsFieldName } : undefined, - filter: { - tags: { NEQ: "null" }, - ...filter("") + if (indexName) { + const suggestions = useElasticSearchDistinctTerm({ + fieldName: `data.attributes.${tagsFieldName}`, + indexName, + keywordMultiFieldSupport: true, + isFieldArray: true, + inputValue: debouncedInputValue, + groupNames, + size: 10 + }); + tagOptions.current = suggestions.map((tag) => toOption(tag)); + } else { + const filter = filterBy( + [tagsFieldName], + !isAdmin + ? { + extraFilters: [ + // Restrict the list to just the user's groups: + { + selector: groupSelectorName, + comparison: "=in=", + arguments: groupNames || [] + } + ] + } + : undefined + ); + const { loading } = useQuery( + { + path: resourcePath ?? "", + sort: "-createdOn", + fields: typeName ? { [typeName]: tagsFieldName } : undefined, + filter: { + tags: { NEQ: "null" }, + ...filter("") + }, + page: { limit: 100 } }, - page: { limit: 100 } - }, - { disabled: !resourcePath } - ); - - const previousTagsOptions = useMemo( - () => - uniq( - compact((response?.data ?? []).flatMap((it) => get(it, tagsFieldName))) - ) - .filter((tag) => tag.includes(inputValue)) - .map((tag) => ({ label: tag, value: tag })), - [response] - ); + { + disabled: !resourcePath, + onSuccess(response) { + const tags = uniq( + compact( + (response?.data ?? []).flatMap((it) => get(it, tagsFieldName)) + ) + ) + .filter((tag) => tag.includes(inputValue)) + .map((tag) => toOption(tag)); + tagOptions.current = tags; + } + } + ); + isLoading.current = loading; + } function toOption(tagText: string): TagSelectOption { return { label: tagText, value: tagText }; @@ -182,14 +209,14 @@ function TagSelect({ options={[ { label: formatMessage("typeNewTagOrSearchPreviousTags"), - options: previousTagsOptions + options: tagOptions.current } ]} + isLoading={isLoading.current} // Select config: styles={customStyle} classNamePrefix="react-select" isMulti={true} - isLoading={loading} allowCreateWhileLoading={true} isClearable={true} placeholder={ diff --git a/packages/dina-ui/components/tag-editor/TagsAndRestrictionsSection.tsx b/packages/dina-ui/components/tag-editor/TagsAndRestrictionsSection.tsx index 8d54c19552..2661841719 100644 --- a/packages/dina-ui/components/tag-editor/TagsAndRestrictionsSection.tsx +++ b/packages/dina-ui/components/tag-editor/TagsAndRestrictionsSection.tsx @@ -15,12 +15,14 @@ export interface TagsAndRestrictionsSection { resourcePath?: string; tagsFieldName?: string; groupSelectorName?: string; + indexName?: string; } export function TagsAndRestrictionsSection({ resourcePath, groupSelectorName = "group", - tagsFieldName = "tags" + tagsFieldName = "tags", + indexName }: TagsAndRestrictionsSection) { const { readOnly, initialValues } = useDinaFormContext(); @@ -45,6 +47,7 @@ export function TagsAndRestrictionsSection({
{ } console.warn("No mock value for bulkGet paths: ", paths); }); - +const mockPost = jest.fn((path) => { + if (path === "search-api/search-ws/search") { + return new Promise((resolve) => resolve); + } +}); const apiContext: any = { - apiClient: { get: mockGet, axios: { patch: mockPatch } }, + apiClient: { get: mockGet, axios: { patch: mockPatch, post: mockPost } }, bulkGet: mockBulkGet }; diff --git a/packages/dina-ui/pages/object-store/metadata/external-resource-edit.tsx b/packages/dina-ui/pages/object-store/metadata/external-resource-edit.tsx index 93c4203ac0..a40a2aa70d 100644 --- a/packages/dina-ui/pages/object-store/metadata/external-resource-edit.tsx +++ b/packages/dina-ui/pages/object-store/metadata/external-resource-edit.tsx @@ -200,6 +200,7 @@ function ExternalResourceMetadataForm({ resourcePath="objectstore-api/metadata" tagsFieldName="acTags" groupSelectorName="bucket" + indexName="dina_object_store_index" />
}>