From 75d78b8fbe5c3e39f73691573f616ed35356fe45 Mon Sep 17 00:00:00 2001 From: James Cocker Date: Mon, 19 Jan 2026 10:37:05 +0000 Subject: [PATCH 1/9] Tag fetching and searchable tags, but overrides existing list of tags Signed-off-by: James Cocker --- galasa-ui/messages/de.json | 2 +- galasa-ui/messages/en.json | 2 +- galasa-ui/src/actions/runsAction.ts | 25 ++++- .../test-run-details/OverviewTab.tsx | 94 +++++++++++++------ 4 files changed, 90 insertions(+), 33 deletions(-) diff --git a/galasa-ui/messages/de.json b/galasa-ui/messages/de.json index c82c36a5..6e617844 100644 --- a/galasa-ui/messages/de.json +++ b/galasa-ui/messages/de.json @@ -261,7 +261,7 @@ "modalHeading": "Tags im Testlauf bearbeiten", "modalPrimaryButton": "Speichern", "modalLabelText": "Geben Sie neue Tag-Namen zum Hinzufügen ein oder entfernen Sie vorhandene Tags aus dem Testlauf", - "modalPlaceholderText": "Geben Sie hier einen neuen Tag(s) ein und drücken Sie die [Eingabetaste]", + "modalPlaceholderText": "Geben Sie hier ein neues Tag ein", "removeTag": "Tag entfernen", "modalSecondaryButton": "Abbrechen", "updateSuccess": "Tags erfolgreich aktualisiert", diff --git a/galasa-ui/messages/en.json b/galasa-ui/messages/en.json index 23ddae8b..38028948 100644 --- a/galasa-ui/messages/en.json +++ b/galasa-ui/messages/en.json @@ -240,7 +240,7 @@ "modalHeading": "Edit tags on test run", "modalPrimaryButton": "Save", "modalLabelText": "Type new tag names to add, or remove existing tags from test run", - "modalPlaceholderText": "Type new tag(s) here and hit [enter]", + "modalPlaceholderText": "Type new tag here", "removeTag": "Remove tag", "modalSecondaryButton": "Cancel", "updateSuccess": "Tags updated successfully", diff --git a/galasa-ui/src/actions/runsAction.ts b/galasa-ui/src/actions/runsAction.ts index 049520f1..279f2ed6 100644 --- a/galasa-ui/src/actions/runsAction.ts +++ b/galasa-ui/src/actions/runsAction.ts @@ -5,7 +5,7 @@ */ 'use server'; -import { ResultArchiveStoreAPIApi } from '@/generated/galasaapi'; +import { ResultArchiveStoreAPIApi, TagsAPIApi } from '@/generated/galasaapi'; import { createAuthenticatedApiConfiguration } from '@/utils/api'; import { CLIENT_API_VERSION } from '@/utils/constants/common'; @@ -69,3 +69,26 @@ export const updateRunTags = async (runId: string, tags: string[]) => { }; } }; + +export const getExistingTagObjects = async () => { + try { + const apiConfig = createAuthenticatedApiConfiguration(); + const tagsApiClient = new TagsAPIApi(apiConfig); + + const tagsResponse = await tagsApiClient.getTags(); + + // Convert to plain objects and extract tag names. + const tagNames = tagsResponse + .map(tag => tag.metadata?.name) + .filter((name): name is string => name !== undefined && name !== null); + + return { success: true, tags: tagNames }; + } catch (error: any) { + console.error('Error getting existing tags:', error); + return { + success: false, + error: error.message || 'Failed to get existing tags', + tags: [], + } + } +} diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index 90a49002..3d236e29 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -9,16 +9,14 @@ import styles from '@/styles/test-runs/test-run-details/OverviewTab.module.css'; import InlineText from './InlineText'; import { RunMetadata } from '@/utils/interfaces'; import { useTranslations } from 'next-intl'; -import { Link, InlineNotification } from '@carbon/react'; +import { Link, InlineNotification, FilterableMultiSelect, Modal } from '@carbon/react'; import { Launch, Edit } from '@carbon/icons-react'; import { getAWeekBeforeSubmittedTime } from '@/utils/timeOperations'; import useHistoryBreadCrumbs from '@/hooks/useHistoryBreadCrumbs'; import { TEST_RUNS_QUERY_PARAMS } from '@/utils/constants/common'; -import { TextInput } from '@carbon/react'; -import { Modal } from '@carbon/react'; import { TIME_TO_WAIT_BEFORE_CLOSING_TAG_EDIT_MODAL_MS } from '@/utils/constants/common'; import RenderTags from './RenderTags'; -import { updateRunTags } from '@/actions/runsAction'; +import { updateRunTags, getExistingTagObjects } from '@/actions/runsAction'; const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const translations = useTranslations('OverviewTab'); @@ -27,8 +25,10 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const [weekBefore, setWeekBefore] = useState(null); const [tags, setTags] = useState(metadata?.tags || []); + const [existingTagNames, setExistingTagNames] = useState([]); + const [isTagsEditModalOpen, setIsTagsEditModalOpen] = useState(false); - const [newTagInput, setNewTagInput] = useState(''); + const [filterInput, setFilterInput] = useState(''); const [stagedTags, setStagedTags] = useState>(new Set(tags)); const [notification, setNotification] = useState<{ kind: 'success' | 'error'; @@ -41,6 +41,23 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const OTHER_RECENT_RUNS = `/test-runs?${TEST_RUNS_QUERY_PARAMS.TEST_NAME}=${fullTestName}&${TEST_RUNS_QUERY_PARAMS.BUNDLE}=${metadata?.bundle}&${TEST_RUNS_QUERY_PARAMS.PACKAGE}=${metadata?.package}&${TEST_RUNS_QUERY_PARAMS.DURATION}=60,0,0&${TEST_RUNS_QUERY_PARAMS.TAB}=results&${TEST_RUNS_QUERY_PARAMS.QUERY_NAME}=Recent runs of test ${metadata?.testName}`; const RETRIES_FOR_THIS_TEST_RUN = `/test-runs?${TEST_RUNS_QUERY_PARAMS.SUBMISSION_ID}=${metadata?.submissionId}&${TEST_RUNS_QUERY_PARAMS.FROM}=${weekBefore}&${TEST_RUNS_QUERY_PARAMS.TAB}=results&${TEST_RUNS_QUERY_PARAMS.QUERY_NAME}=All attempts of test run ${metadata?.runName}`; + useEffect(() => { + const fetchExistingTags = async () => { + try { + const result = await getExistingTagObjects(); + setExistingTagNames(result.tags || []); + + if (!result.success) { + console.error('Failed to fetch existing tags:', result.error); + } + } catch (error) { + console.error('Error fetching existing tags:', error); + } + }; + + fetchExistingTags(); + }, []); + useEffect(() => { const validateTime = () => { const validatedTime = getAWeekBeforeSubmittedTime(metadata?.rawSubmittedAt!); @@ -68,34 +85,44 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { }); }; - const handleStageNewTags = () => { - // Parse new tags from input (comma or space separated). - const newTags = newTagInput - .split(/[,\s]+/) - .map((tag) => tag.trim()) - .filter((tag) => tag.length > 0); + const handleFilterableMultiSelectChange = (selectedItems: { id: string; text: string }[]) => { + // Update staged tags based on selected items + const newStagedTags = new Set(selectedItems.map(item => item.text)); + setStagedTags(newStagedTags); + }; - // Add new tags to staged tags Set (automatically handles duplicates). - setStagedTags((prev) => { - const newSet = new Set(prev); - newTags.forEach((tag) => newSet.add(tag)); - return newSet; + // Create items for FilterableMultiSelect + const getFilterableItems = () => { + const items: { id: string; text: string }[] = []; + + // Add existing tags from the system + existingTagNames.forEach((tagName, index) => { + items.push({ + id: `existing-tag-${index}`, + text: tagName, + }); }); - // Clear the input after staging - setNewTagInput(''); + // Add the current filter input as an option if it's not empty and not already in the list + if (filterInput.trim() && !items.some(item => item.text === filterInput.trim())) { + items.push({ + id: 'custom-input', + text: filterInput.trim(), + }); + } + + return items; }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleStageNewTags(); - } + // Get initially selected items based on staged tags + const getInitialSelectedItems = () => { + const items = getFilterableItems(); + return items.filter(item => stagedTags.has(item.text)); }; const handleModalClose = () => { setIsTagsEditModalOpen(false); - setNewTagInput(''); + setFilterInput(''); setNotification(null); }; @@ -206,13 +233,20 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { onCloseButtonClick={() => setNotification(null)} /> )} - ) => setNewTagInput(e.target.value)} - onKeyDown={handleKeyDown} + items={getFilterableItems()} + initialSelectedItems={getInitialSelectedItems()} + itemToString={(item: { id: string; text: string } | null) => (item ? item.text : '')} + selectionFeedback="top-after-reopen" + onChange={({ selectedItems }: { selectedItems: { id: string; text: string }[] }) => { + handleFilterableMultiSelectChange(selectedItems); + }} + onInputValueChange={(inputValue: string) => { + setFilterInput(inputValue); + }} className={styles.tagsTextInput} /> Date: Thu, 22 Jan 2026 15:31:20 +0000 Subject: [PATCH 2/9] Changed items to use label instead of text, and consolidated on a simple tag type Signed-off-by: James Cocker --- .../test-run-details/OverviewTab.tsx | 65 +++++++++++-------- .../test-run-details/OverviewTab.module.css | 17 +++++ 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index 3d236e29..5285497b 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: EPL-2.0 */ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import styles from '@/styles/test-runs/test-run-details/OverviewTab.module.css'; import InlineText from './InlineText'; import { RunMetadata } from '@/utils/interfaces'; @@ -15,9 +15,14 @@ import { getAWeekBeforeSubmittedTime } from '@/utils/timeOperations'; import useHistoryBreadCrumbs from '@/hooks/useHistoryBreadCrumbs'; import { TEST_RUNS_QUERY_PARAMS } from '@/utils/constants/common'; import { TIME_TO_WAIT_BEFORE_CLOSING_TAG_EDIT_MODAL_MS } from '@/utils/constants/common'; -import RenderTags from './RenderTags'; +import RenderTags from '@/components/test-runs/test-run-details/RenderTags'; import { updateRunTags, getExistingTagObjects } from '@/actions/runsAction'; +type SimpleTagType = { + id: string; + label: string +} + const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const translations = useTranslations('OverviewTab'); const { pushBreadCrumb } = useHistoryBreadCrumbs(); @@ -25,7 +30,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const [weekBefore, setWeekBefore] = useState(null); const [tags, setTags] = useState(metadata?.tags || []); - const [existingTagNames, setExistingTagNames] = useState([]); + const [existingTagObjectNames, setexistingTagObjectNames] = useState([]); const [isTagsEditModalOpen, setIsTagsEditModalOpen] = useState(false); const [filterInput, setFilterInput] = useState(''); @@ -45,7 +50,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const fetchExistingTags = async () => { try { const result = await getExistingTagObjects(); - setExistingTagNames(result.tags || []); + setexistingTagObjectNames(result.tags || []); if (!result.success) { console.error('Failed to fetch existing tags:', result.error); @@ -54,7 +59,6 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { console.error('Error fetching existing tags:', error); } }; - fetchExistingTags(); }, []); @@ -85,40 +89,48 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { }); }; - const handleFilterableMultiSelectChange = (selectedItems: { id: string; text: string }[]) => { + const handleFilterableMultiSelectChange = (selectedItems: SimpleTagType[]) => { // Update staged tags based on selected items - const newStagedTags = new Set(selectedItems.map(item => item.text)); + const newStagedTags = new Set(selectedItems.map(item => item.label)); setStagedTags(newStagedTags); }; - // Create items for FilterableMultiSelect - const getFilterableItems = () => { - const items: { id: string; text: string }[] = []; + // Create items for FilterableMultiSelect. + const filterableItems = useMemo(() => { + const items: SimpleTagType[] = []; + + // Add staged tags. + stagedTags.forEach((tagName, index) => { + items.push({ + id: `existing-tag-${index}`, + label: tagName, + }); + }); - // Add existing tags from the system - existingTagNames.forEach((tagName, index) => { + // Add existing tags from the system if they aren't already there. + existingTagObjectNames.filter((existingTagObjectName: string)=>(!stagedTags.has(existingTagObjectName))).forEach((tagName, index) => { items.push({ id: `existing-tag-${index}`, - text: tagName, + label: tagName, }); }); - // Add the current filter input as an option if it's not empty and not already in the list - if (filterInput.trim() && !items.some(item => item.text === filterInput.trim())) { + + // Add the current filter input as an option if it's not empty and not already in the list. + if (filterInput.trim() && !items.some(item => item.label === filterInput.trim())) { items.push({ id: 'custom-input', - text: filterInput.trim(), + label: filterInput.trim(), }); } return items; - }; + }, [existingTagObjectNames, stagedTags, filterInput]); - // Get initially selected items based on staged tags - const getInitialSelectedItems = () => { - const items = getFilterableItems(); - return items.filter(item => stagedTags.has(item.text)); - }; + // Get initially selected items based on staged tags. + const initialSelectedItems = useMemo(() => { + return filterableItems.filter(item => stagedTags.has(item.label)); + }, [filterableItems, stagedTags]); const handleModalClose = () => { setIsTagsEditModalOpen(false); @@ -221,6 +233,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { secondaryButtonText={translations('modalSecondaryButton')} onRequestSubmit={handleSaveTags} primaryButtonDisabled={isSaving} + className={styles.tagsEditModal} > {notification && ( { id="tags-filterable-multiselect" titleText={translations('modalLabelText')} placeholder={translations('modalPlaceholderText')} - items={getFilterableItems()} - initialSelectedItems={getInitialSelectedItems()} - itemToString={(item: { id: string; text: string } | null) => (item ? item.text : '')} + items={filterableItems} + initialSelectedItems={initialSelectedItems} + itemToString={(item: SimpleTagType | null) => (item ? item.label : '')} selectionFeedback="top-after-reopen" - onChange={({ selectedItems }: { selectedItems: { id: string; text: string }[] }) => { + onChange={({ selectedItems }: { selectedItems: SimpleTagType[] }) => { handleFilterableMultiSelectChange(selectedItems); }} onInputValueChange={(inputValue: string) => { diff --git a/galasa-ui/src/styles/test-runs/test-run-details/OverviewTab.module.css b/galasa-ui/src/styles/test-runs/test-run-details/OverviewTab.module.css index 21640ab3..ec9e43a8 100644 --- a/galasa-ui/src/styles/test-runs/test-run-details/OverviewTab.module.css +++ b/galasa-ui/src/styles/test-runs/test-run-details/OverviewTab.module.css @@ -80,3 +80,20 @@ .tagsTextInput { margin-bottom: 1.5rem; } + +/* Uses :global() to access Carbon's global class */ +.tagsEditModal :global(.cds--modal-container) { + min-height: 25rem; + max-height: 35rem; + height: 45vh; +} + +.tagsEditModal :global(#tags-filterable-multiselect__menu) { + max-height: 18vh; +} + +.tagsEditModal :global(.cds--tag.cds--tag--filter.cds--tag--high-contrast) { + visibility: collapse; + width: 0px; + margin: 0px; +} From f26a57231854226348289e9901c8d06edfa8b2db Mon Sep 17 00:00:00 2001 From: James Cocker Date: Fri, 23 Jan 2026 16:21:53 +0000 Subject: [PATCH 3/9] Fixed syncronisation issues Signed-off-by: James Cocker --- galasa-ui/src/actions/runsAction.ts | 10 +++---- .../test-run-details/OverviewTab.tsx | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/galasa-ui/src/actions/runsAction.ts b/galasa-ui/src/actions/runsAction.ts index 279f2ed6..d0169d24 100644 --- a/galasa-ui/src/actions/runsAction.ts +++ b/galasa-ui/src/actions/runsAction.ts @@ -74,12 +74,12 @@ export const getExistingTagObjects = async () => { try { const apiConfig = createAuthenticatedApiConfiguration(); const tagsApiClient = new TagsAPIApi(apiConfig); - + const tagsResponse = await tagsApiClient.getTags(); - + // Convert to plain objects and extract tag names. const tagNames = tagsResponse - .map(tag => tag.metadata?.name) + .map((tag) => tag.metadata?.name) .filter((name): name is string => name !== undefined && name !== null); return { success: true, tags: tagNames }; @@ -89,6 +89,6 @@ export const getExistingTagObjects = async () => { success: false, error: error.message || 'Failed to get existing tags', tags: [], - } + }; } -} +}; diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index 5285497b..5be69e7c 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -19,9 +19,9 @@ import RenderTags from '@/components/test-runs/test-run-details/RenderTags'; import { updateRunTags, getExistingTagObjects } from '@/actions/runsAction'; type SimpleTagType = { - id: string; - label: string -} + id: string; + label: string; +}; const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const translations = useTranslations('OverviewTab'); @@ -34,7 +34,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const [isTagsEditModalOpen, setIsTagsEditModalOpen] = useState(false); const [filterInput, setFilterInput] = useState(''); - const [stagedTags, setStagedTags] = useState>(new Set(tags)); + const [stagedTags, setStagedTags] = useState>(new Set()); const [notification, setNotification] = useState<{ kind: 'success' | 'error'; title: string; @@ -91,7 +91,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const handleFilterableMultiSelectChange = (selectedItems: SimpleTagType[]) => { // Update staged tags based on selected items - const newStagedTags = new Set(selectedItems.map(item => item.label)); + const newStagedTags = new Set(selectedItems.map((item) => item.label)); setStagedTags(newStagedTags); }; @@ -108,16 +108,17 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { }); // Add existing tags from the system if they aren't already there. - existingTagObjectNames.filter((existingTagObjectName: string)=>(!stagedTags.has(existingTagObjectName))).forEach((tagName, index) => { - items.push({ - id: `existing-tag-${index}`, - label: tagName, + existingTagObjectNames + .filter((existingTagObjectName: string) => !stagedTags.has(existingTagObjectName)) + .forEach((tagName, index) => { + items.push({ + id: `existing-tag-${index}`, + label: tagName, + }); }); - }); - // Add the current filter input as an option if it's not empty and not already in the list. - if (filterInput.trim() && !items.some(item => item.label === filterInput.trim())) { + if (filterInput.trim() && !items.some((item) => item.label === filterInput.trim())) { items.push({ id: 'custom-input', label: filterInput.trim(), @@ -129,7 +130,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { // Get initially selected items based on staged tags. const initialSelectedItems = useMemo(() => { - return filterableItems.filter(item => stagedTags.has(item.label)); + return filterableItems.filter((item) => stagedTags.has(item.label)); }, [filterableItems, stagedTags]); const handleModalClose = () => { @@ -200,6 +201,8 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
{ + // Initialize staged tags from current tags when opening modal + setStagedTags(new Set(tags)); setIsTagsEditModalOpen(true); }} > @@ -254,6 +257,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { initialSelectedItems={initialSelectedItems} itemToString={(item: SimpleTagType | null) => (item ? item.label : '')} selectionFeedback="top-after-reopen" + selectedItems={initialSelectedItems} onChange={({ selectedItems }: { selectedItems: SimpleTagType[] }) => { handleFilterableMultiSelectChange(selectedItems); }} From f1923bcfe3d4c7cb00567f3ff1ff64d0b57c937c Mon Sep 17 00:00:00 2001 From: James Cocker Date: Fri, 23 Jan 2026 17:17:26 +0000 Subject: [PATCH 4/9] Fixed tags swapping order when ticked/unticked Signed-off-by: James Cocker --- .../test-run-details/OverviewTab.tsx | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index 5be69e7c..c053ec02 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -97,35 +97,27 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { // Create items for FilterableMultiSelect. const filterableItems = useMemo(() => { - const items: SimpleTagType[] = []; + const itemsSet = new Set(); - // Add staged tags. - stagedTags.forEach((tagName, index) => { - items.push({ - id: `existing-tag-${index}`, - label: tagName, - }); - }); - - // Add existing tags from the system if they aren't already there. - existingTagObjectNames - .filter((existingTagObjectName: string) => !stagedTags.has(existingTagObjectName)) - .forEach((tagName, index) => { - items.push({ - id: `existing-tag-${index}`, - label: tagName, - }); - }); + // Collect all unique tag names. + stagedTags.forEach((tagName) => itemsSet.add(tagName)); + existingTagObjectNames.forEach((tagName) => itemsSet.add(tagName)); - // Add the current filter input as an option if it's not empty and not already in the list. - if (filterInput.trim() && !items.some((item) => item.label === filterInput.trim())) { - items.push({ - id: 'custom-input', - label: filterInput.trim(), - }); + // Add the current filter input if it's not empty and not already in the list. + if (filterInput.trim() && !itemsSet.has(filterInput.trim())) { + itemsSet.add(filterInput.trim()); } - return items; + // Convert to array and sort alphabetically. + const sortedTags = Array.from(itemsSet).sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()) + ); + + // Create items with consistent IDs based on sorted order. + return sortedTags.map((tagName, index) => ({ + id: `tag-${index}-${tagName}`, + label: tagName, + })); }, [existingTagObjectNames, stagedTags, filterInput]); // Get initially selected items based on staged tags. From 1336caadd7d40f5d4630f55077183d4beb99475c Mon Sep 17 00:00:00 2001 From: James Cocker Date: Fri, 23 Jan 2026 17:34:53 +0000 Subject: [PATCH 5/9] Added unit tests and fixed failing ones Signed-off-by: James Cocker --- .../test-run-details/OverviewTab.test.tsx | 255 +++++++++++++++++- 1 file changed, 251 insertions(+), 4 deletions(-) diff --git a/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx b/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx index 4095f4e6..2a26d79a 100644 --- a/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx @@ -15,6 +15,10 @@ import { updateRunTags } from '@/actions/runsAction'; // Mock the server action jest.mock('@/actions/runsAction', () => ({ updateRunTags: jest.fn(), + getExistingTagObjects: jest.fn().mockResolvedValue({ + success: true, + tags: ['existing-tag-1', 'existing-tag-2'], + }), })); // Mock RenderTags component @@ -75,6 +79,46 @@ jest.mock('@carbon/react', () => ({ aria-label={labelText} /> ), + FilterableMultiSelect: ({ + id, + titleText, + placeholder, + items, + initialSelectedItems, + selectedItems, + itemToString, + onChange, + onInputValueChange, + }: any) => ( +
+ + onInputValueChange && onInputValueChange(e.target.value)} + /> +
+ {items.map((item: any) => { + const isSelected = selectedItems?.some((selected: any) => selected.id === item.id); + return ( +
+ { + const newSelected = e.target.checked + ? [...(selectedItems || []), item] + : (selectedItems || []).filter((s: any) => s.id !== item.id); + onChange && onChange({ selectedItems: newSelected }); + }} + /> + +
+ ); + })} +
+
+ ), Modal: ({ open, children, @@ -508,7 +552,7 @@ describe('OverviewTab - Tags Edit Modal', () => { }); }); - it('should persist staged tags when modal is closed without saving', async () => { + it('should reset staged tags when modal is closed without saving', async () => { const user = userEvent.setup(); render(); @@ -539,9 +583,9 @@ describe('OverviewTab - Tags Edit Modal', () => { await user.click(editIcon); await waitFor(() => { - // Tag should still be removed (staged changes persist) - expect(screen.queryByTestId('remove-tag-smoke')).not.toBeInTheDocument(); - // But regression tag should still be there + // Tag should be back (staged changes reset on cancel) + expect(screen.getByTestId('remove-tag-smoke')).toBeInTheDocument(); + // And regression tag should still be there expect(screen.getByTestId('remove-tag-regression')).toBeInTheDocument(); }); }); @@ -557,4 +601,207 @@ describe('OverviewTab - Tags Edit Modal', () => { expect(screen.getByText(new RegExp(completeMetadata.runName))).toBeInTheDocument(); }); }); + + it('should initialise staged tags from current tags when modal opens', async () => { + const user = userEvent.setup(); + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + // Both original tags should be present in the modal + expect(screen.getByTestId('remove-tag-smoke')).toBeInTheDocument(); + expect(screen.getByTestId('remove-tag-regression')).toBeInTheDocument(); + }); + }); + + it('should display FilterableMultiSelect with sorted items alphabetically', async () => { + const user = userEvent.setup(); + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Check that items are present (they should be sorted alphabetically) + const items = screen.getByTestId('filterable-multiselect-items'); + expect(items).toBeInTheDocument(); + }); + + it('should maintain alphabetical sorting when adding new tags via filter input', async () => { + const user = userEvent.setup(); + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Type a new tag in the filter input + const filterInput = screen.getByTestId('filterable-multiselect-input'); + await user.type(filterInput, 'alpha-tag'); + + await waitFor(() => { + // The new tag should appear in the items list + expect(screen.getByTestId('multiselect-item-alpha-tag')).toBeInTheDocument(); + }); + }); + + it('should keep selected items ticked in the dropdown', async () => { + const user = userEvent.setup(); + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Check that initial tags are selected (checked) + const smokeCheckbox = screen.getByTestId('multiselect-item-smoke').querySelector('input[type="checkbox"]'); + const regressionCheckbox = screen.getByTestId('multiselect-item-regression').querySelector('input[type="checkbox"]'); + + expect(smokeCheckbox).toBeChecked(); + expect(regressionCheckbox).toBeChecked(); + }); + + it('should update staged tags when selecting items in FilterableMultiSelect', async () => { + const user = userEvent.setup(); + const metadataWithOneTag: RunMetadata = { + ...completeMetadata, + tags: ['smoke'], + }; + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Select an existing tag that wasn't initially selected + const existingTag1Checkbox = screen.getByTestId('multiselect-item-existing-tag-1').querySelector('input[type="checkbox"]'); + if (existingTag1Checkbox) { + await user.click(existingTag1Checkbox); + } + + await waitFor(() => { + // The tag should now appear in the RenderTags component + const tags = screen.getAllByTestId('mock-tag'); + const tagTexts = tags.map(tag => tag.textContent); + expect(tagTexts.some(text => text?.includes('existing-tag-1'))).toBe(true); + }); + }); + + it('should clear filter input when modal is closed', async () => { + const user = userEvent.setup(); + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Type something in the filter input + const filterInput = screen.getByTestId('filterable-multiselect-input'); + await user.type(filterInput, 'test-tag'); + + // Close modal + const secondaryButton = screen.getByTestId('modal-secondary-button'); + await user.click(secondaryButton); + + await waitFor(() => { + expect(screen.queryByTestId('mock-modal')).not.toBeInTheDocument(); + }); + + // Reopen modal + await user.click(editIcon); + + await waitFor(() => { + const filterInputReopened = screen.getByTestId('filterable-multiselect-input'); + // Filter input should be empty + expect(filterInputReopened).toHaveValue(''); + }); + }); + + it('should update main tags display after successful save', async () => { + const user = userEvent.setup(); + mockUpdateRunTags.mockResolvedValueOnce({ + success: true, + tags: ['smoke', 'regression', 'new-tag'], + }); + + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Add a new tag via filter input + const filterInput = screen.getByTestId('filterable-multiselect-input'); + await user.type(filterInput, 'new-tag'); + + await waitFor(async () => { + const newTagCheckbox = screen.getByTestId('multiselect-item-new-tag').querySelector('input[type="checkbox"]'); + if (newTagCheckbox) { + await user.click(newTagCheckbox); + } + }); + + // Save + const primaryButton = screen.getByTestId('modal-primary-button'); + await user.click(primaryButton); + + await waitFor(() => { + expect(mockUpdateRunTags).toHaveBeenCalled(); + }); + + // After modal closes, the main tags display should be updated + // Wait for modal to close + await waitFor(() => { + expect(screen.queryByTestId('mock-modal')).not.toBeInTheDocument(); + }, { timeout: 3000 }); + }); + + it('should fetch existing tags on component mount', async () => { + render(); + + // Wait for the component to render + await waitFor(() => { + expect(screen.getByText('Tags', { selector: 'h5' })).toBeInTheDocument(); + }); + + // The getExistingTagObjects should have been called + const { getExistingTagObjects } = require('@/actions/runsAction'); + expect(getExistingTagObjects).toHaveBeenCalled(); + }); + + it('should include existing system tags in FilterableMultiSelect items', async () => { + const user = userEvent.setup(); + render(); + + const editIcon = screen.getByTestId('edit-icon'); + await user.click(editIcon); + + await waitFor(() => { + expect(screen.getByTestId('mock-filterable-multiselect')).toBeInTheDocument(); + }); + + // Check that existing system tags are available + expect(screen.getByTestId('multiselect-item-existing-tag-1')).toBeInTheDocument(); + expect(screen.getByTestId('multiselect-item-existing-tag-2')).toBeInTheDocument(); + }); }); From 64c2e66b987f10594a010f88d4ca46f1edc52a78 Mon Sep 17 00:00:00 2001 From: James Cocker Date: Fri, 23 Jan 2026 17:35:23 +0000 Subject: [PATCH 6/9] Small formatting change on overview tab test file Signed-off-by: James Cocker --- .../test-run-details/OverviewTab.test.tsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx b/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx index 2a26d79a..4e4513a6 100644 --- a/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx @@ -665,8 +665,12 @@ describe('OverviewTab - Tags Edit Modal', () => { }); // Check that initial tags are selected (checked) - const smokeCheckbox = screen.getByTestId('multiselect-item-smoke').querySelector('input[type="checkbox"]'); - const regressionCheckbox = screen.getByTestId('multiselect-item-regression').querySelector('input[type="checkbox"]'); + const smokeCheckbox = screen + .getByTestId('multiselect-item-smoke') + .querySelector('input[type="checkbox"]'); + const regressionCheckbox = screen + .getByTestId('multiselect-item-regression') + .querySelector('input[type="checkbox"]'); expect(smokeCheckbox).toBeChecked(); expect(regressionCheckbox).toBeChecked(); @@ -688,7 +692,9 @@ describe('OverviewTab - Tags Edit Modal', () => { }); // Select an existing tag that wasn't initially selected - const existingTag1Checkbox = screen.getByTestId('multiselect-item-existing-tag-1').querySelector('input[type="checkbox"]'); + const existingTag1Checkbox = screen + .getByTestId('multiselect-item-existing-tag-1') + .querySelector('input[type="checkbox"]'); if (existingTag1Checkbox) { await user.click(existingTag1Checkbox); } @@ -696,8 +702,8 @@ describe('OverviewTab - Tags Edit Modal', () => { await waitFor(() => { // The tag should now appear in the RenderTags component const tags = screen.getAllByTestId('mock-tag'); - const tagTexts = tags.map(tag => tag.textContent); - expect(tagTexts.some(text => text?.includes('existing-tag-1'))).toBe(true); + const tagTexts = tags.map((tag) => tag.textContent); + expect(tagTexts.some((text) => text?.includes('existing-tag-1'))).toBe(true); }); }); @@ -755,7 +761,9 @@ describe('OverviewTab - Tags Edit Modal', () => { await user.type(filterInput, 'new-tag'); await waitFor(async () => { - const newTagCheckbox = screen.getByTestId('multiselect-item-new-tag').querySelector('input[type="checkbox"]'); + const newTagCheckbox = screen + .getByTestId('multiselect-item-new-tag') + .querySelector('input[type="checkbox"]'); if (newTagCheckbox) { await user.click(newTagCheckbox); } @@ -771,9 +779,12 @@ describe('OverviewTab - Tags Edit Modal', () => { // After modal closes, the main tags display should be updated // Wait for modal to close - await waitFor(() => { - expect(screen.queryByTestId('mock-modal')).not.toBeInTheDocument(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(screen.queryByTestId('mock-modal')).not.toBeInTheDocument(); + }, + { timeout: 3000 } + ); }); it('should fetch existing tags on component mount', async () => { From 865f35bb593355081d656f104bc7e3cf0774a92d Mon Sep 17 00:00:00 2001 From: James Cocker Date: Tue, 3 Feb 2026 12:06:45 +0000 Subject: [PATCH 7/9] Changed some variable names and sorted tags before rendering Signed-off-by: James Cocker --- galasa-ui/messages/de.json | 3 ++- galasa-ui/messages/en.json | 3 ++- .../test-runs/results/TestRunsTable.tsx | 2 +- .../test-run-details/OverviewTab.tsx | 26 +++++++++++++------ .../test-run-details/OverviewTab.test.tsx | 11 ++++---- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/galasa-ui/messages/de.json b/galasa-ui/messages/de.json index 6e617844..6b45f7d5 100644 --- a/galasa-ui/messages/de.json +++ b/galasa-ui/messages/de.json @@ -267,7 +267,8 @@ "updateSuccess": "Tags erfolgreich aktualisiert", "updateSuccessMessage": "Die Tags wurden für diesen Testlauf aktualisiert.", "updateError": "Fehler beim Aktualisieren der Tags", - "updateErrorMessage": "Beim Aktualisieren der Tags ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut." + "updateErrorMessage": "Beim Aktualisieren der Tags ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", + "editTags": "Tags bearbeiten" }, "3270Tab": { "Terminal": "Terminal", diff --git a/galasa-ui/messages/en.json b/galasa-ui/messages/en.json index 38028948..0ed1c5ad 100644 --- a/galasa-ui/messages/en.json +++ b/galasa-ui/messages/en.json @@ -246,7 +246,8 @@ "updateSuccess": "Tags updated successfully", "updateSuccessMessage": "The tags have been updated for this test run.", "updateError": "Failed to update tags", - "updateErrorMessage": "An error occurred while updating the tags. Please try again." + "updateErrorMessage": "An error occurred while updating the tags. Please try again.", + "editTags": "Edit tags" }, "3270Tab": { "Terminal": "Terminal", diff --git a/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx b/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx index f8824c8a..f49cbc4f 100644 --- a/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx +++ b/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx @@ -143,7 +143,7 @@ export default function TestRunsTable({ const tagsArray = value.split(', '); return ( - + ); diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index c053ec02..bc05c0bb 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -18,7 +18,7 @@ import { TIME_TO_WAIT_BEFORE_CLOSING_TAG_EDIT_MODAL_MS } from '@/utils/constants import RenderTags from '@/components/test-runs/test-run-details/RenderTags'; import { updateRunTags, getExistingTagObjects } from '@/actions/runsAction'; -type SimpleTagType = { +type DisplayedTagType = { id: string; label: string; }; @@ -30,7 +30,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const [weekBefore, setWeekBefore] = useState(null); const [tags, setTags] = useState(metadata?.tags || []); - const [existingTagObjectNames, setexistingTagObjectNames] = useState([]); + const [existingTagObjectNames, setExistingTagObjectNames] = useState([]); const [isTagsEditModalOpen, setIsTagsEditModalOpen] = useState(false); const [filterInput, setFilterInput] = useState(''); @@ -50,7 +50,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { const fetchExistingTags = async () => { try { const result = await getExistingTagObjects(); - setexistingTagObjectNames(result.tags || []); + setExistingTagObjectNames(result.tags || []); if (!result.success) { console.error('Failed to fetch existing tags:', result.error); @@ -89,7 +89,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { }); }; - const handleFilterableMultiSelectChange = (selectedItems: SimpleTagType[]) => { + const handleFilterableMultiSelectChange = (selectedItems: DisplayedTagType[]) => { // Update staged tags based on selected items const newStagedTags = new Set(selectedItems.map((item) => item.label)); setStagedTags(newStagedTags); @@ -193,15 +193,25 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => {
{ - // Initialize staged tags from current tags when opening modal + // Initialise staged tags from current tags when opening modal. setStagedTags(new Set(tags)); setIsTagsEditModalOpen(true); }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setStagedTags(new Set(tags)); + setIsTagsEditModalOpen(true); + } + }} + tabIndex={0} + role="button" + aria-label={translations('editTags')} >
- +
@@ -247,10 +257,10 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { placeholder={translations('modalPlaceholderText')} items={filterableItems} initialSelectedItems={initialSelectedItems} - itemToString={(item: SimpleTagType | null) => (item ? item.label : '')} + itemToString={(item: DisplayedTagType | null) => (item ? item.label : '')} selectionFeedback="top-after-reopen" selectedItems={initialSelectedItems} - onChange={({ selectedItems }: { selectedItems: SimpleTagType[] }) => { + onChange={({ selectedItems }: { selectedItems: DisplayedTagType[] }) => { handleFilterableMultiSelectChange(selectedItems); }} onInputValueChange={(inputValue: string) => { diff --git a/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx b/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx index 4e4513a6..ccdcef39 100644 --- a/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/test-run-details/OverviewTab.test.tsx @@ -248,15 +248,15 @@ describe('OverviewTab', () => { expect(screen.getByText(completeMetadata.duration)).toBeInTheDocument(); }); - it('renders each tag when tags array is non-empty', () => { + it('renders each tag when tags array is non-empty, sorted', () => { render(); // header - use getByText since h5 contains nested elements expect(screen.getByText('Tags', { selector: 'h5' })).toBeInTheDocument(); - // tags + const tagEls = screen.getAllByTestId('mock-tag'); expect(tagEls).toHaveLength(2); - expect(tagEls[0]).toHaveTextContent('smoke'); - expect(tagEls[1]).toHaveTextContent('regression'); + expect(tagEls[0]).toHaveTextContent('regression'); + expect(tagEls[1]).toHaveTextContent('smoke'); }); it('shows fallback text when tags is empty or missing', () => { @@ -492,8 +492,8 @@ describe('OverviewTab - Tags Edit Modal', () => { await waitFor(() => { expect(mockUpdateRunTags).toHaveBeenCalledWith(completeMetadata.runId, [ - 'smoke', 'regression', + 'smoke', ]); }); }); @@ -778,7 +778,6 @@ describe('OverviewTab - Tags Edit Modal', () => { }); // After modal closes, the main tags display should be updated - // Wait for modal to close await waitFor( () => { expect(screen.queryByTestId('mock-modal')).not.toBeInTheDocument(); From 93e52e5dc51c030ebd72c46492932d4e943c2864 Mon Sep 17 00:00:00 2001 From: James Cocker Date: Tue, 3 Feb 2026 14:59:01 +0000 Subject: [PATCH 8/9] Moved sorting outside of render tags as to not re-compute Signed-off-by: James Cocker --- galasa-ui/src/components/test-runs/results/TestRunsTable.tsx | 4 ++-- .../src/components/test-runs/test-run-details/OverviewTab.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx b/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx index f49cbc4f..abb1c996 100644 --- a/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx +++ b/galasa-ui/src/components/test-runs/results/TestRunsTable.tsx @@ -140,10 +140,10 @@ export default function TestRunsTable({ if (value.length === 0) { return N/A; } - const tagsArray = value.split(', '); + const tagsArray = value.split(', ').sort(); return ( - + ); diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index bc05c0bb..73f2d6f5 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -149,8 +149,8 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { subtitle: translations('updateSuccessMessage'), }); - // Set tags of the component to the staged tags tags. - setTags(Array.from(stagedTags)); + // Set tags of the component to the staged tags. + setTags(Array.from(stagedTags).sort()); // Close modal after a short delay to show success message. setTimeout(() => { From 153a84098f4112be98415e1e3173eaf200f8e90f Mon Sep 17 00:00:00 2001 From: James Cocker Date: Wed, 4 Feb 2026 11:26:35 +0000 Subject: [PATCH 9/9] Tags ordered based on whether they are ticked and small variable name change Signed-off-by: James Cocker --- .../test-run-details/OverviewTab.tsx | 23 ++++++++----------- .../styles/test-runs/TestRunsPage.module.css | 1 - .../test-runs/TestRunsSearch.module.css | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx index 73f2d6f5..24559dab 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/OverviewTab.tsx @@ -90,31 +90,28 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { }; const handleFilterableMultiSelectChange = (selectedItems: DisplayedTagType[]) => { - // Update staged tags based on selected items + // Update staged tags based on selected items. const newStagedTags = new Set(selectedItems.map((item) => item.label)); setStagedTags(newStagedTags); }; // Create items for FilterableMultiSelect. const filterableItems = useMemo(() => { - const itemsSet = new Set(); + const stagedAndExistingTags = new Set(); // Collect all unique tag names. - stagedTags.forEach((tagName) => itemsSet.add(tagName)); - existingTagObjectNames.forEach((tagName) => itemsSet.add(tagName)); + stagedTags.forEach((tagName) => stagedAndExistingTags.add(tagName)); + existingTagObjectNames.forEach((tagName) => stagedAndExistingTags.add(tagName)); // Add the current filter input if it's not empty and not already in the list. - if (filterInput.trim() && !itemsSet.has(filterInput.trim())) { - itemsSet.add(filterInput.trim()); + if (filterInput.trim() && !stagedAndExistingTags.has(filterInput.trim())) { + stagedAndExistingTags.add(filterInput.trim()); } - // Convert to array and sort alphabetically. - const sortedTags = Array.from(itemsSet).sort((a, b) => - a.toLowerCase().localeCompare(b.toLowerCase()) - ); + const arrayOfStagedAndExistingTags = Array.from(stagedAndExistingTags); // Create items with consistent IDs based on sorted order. - return sortedTags.map((tagName, index) => ({ + return arrayOfStagedAndExistingTags.map((tagName, index) => ({ id: `tag-${index}-${tagName}`, label: tagName, })); @@ -258,7 +255,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { items={filterableItems} initialSelectedItems={initialSelectedItems} itemToString={(item: DisplayedTagType | null) => (item ? item.label : '')} - selectionFeedback="top-after-reopen" + selectionFeedback="top" selectedItems={initialSelectedItems} onChange={({ selectedItems }: { selectedItems: DisplayedTagType[] }) => { handleFilterableMultiSelectChange(selectedItems); @@ -269,7 +266,7 @@ const OverviewTab = ({ metadata }: { metadata: RunMetadata }) => { className={styles.tagsTextInput} />