diff --git a/src/elements/common/sub-header/SubHeader.tsx b/src/elements/common/sub-header/SubHeader.tsx index 7f3afbca73..004f9421ef 100644 --- a/src/elements/common/sub-header/SubHeader.tsx +++ b/src/elements/common/sub-header/SubHeader.tsx @@ -28,6 +28,7 @@ export interface SubHeaderProps { onGridViewSliderChange?: (newSliderValue: number) => void; onItemClick: (id: string | null, triggerNavigationEvent: boolean | null) => void; onSortChange: (sortBy: string, sortDirection: string) => void; + onMetadataSidePanelToggle?: () => void; onUpload: () => void; onViewModeChange?: (viewMode: ViewMode) => void; portalElement?: HTMLElement; @@ -53,6 +54,7 @@ const SubHeader = ({ onCreate, onItemClick, onSortChange, + onMetadataSidePanelToggle, onUpload, onViewModeChange, portalElement, @@ -109,9 +111,11 @@ const SubHeader = ({ onCreate={onCreate} onGridViewSliderChange={onGridViewSliderChange} onSortChange={onSortChange} + onMetadataSidePanelToggle={onMetadataSidePanelToggle} onUpload={onUpload} onViewModeChange={onViewModeChange} portalElement={portalElement} + selectedItemIds={selectedItemIds} view={view} viewMode={viewMode} /> diff --git a/src/elements/common/sub-header/SubHeaderLeftV2.tsx b/src/elements/common/sub-header/SubHeaderLeftV2.tsx index 252c1f1ee3..0aa00f5445 100644 --- a/src/elements/common/sub-header/SubHeaderLeftV2.tsx +++ b/src/elements/common/sub-header/SubHeaderLeftV2.tsx @@ -1,8 +1,9 @@ -import React, { useMemo } from 'react'; +import * as React from 'react'; import { useIntl } from 'react-intl'; import { XMark } from '@box/blueprint-web-assets/icons/Fill/index'; import { IconButton, PageHeader, Text } from '@box/blueprint-web'; import type { Selection } from 'react-aria-components'; +import { useSelectedItemText } from '../../content-explorer/utils'; import type { Collection } from '../../../common/types/core'; import messages from '../messages'; @@ -20,27 +21,7 @@ const SubHeaderLeftV2 = (props: SubHeaderLeftV2Props) => { const { currentCollection, onClearSelectedItemIds, rootName, selectedItemIds, title } = props; const { formatMessage } = useIntl(); - // Generate selected item text based on selected keys - const selectedItemText: string = useMemo(() => { - const selectedCount = selectedItemIds === 'all' ? currentCollection.items.length : selectedItemIds.size; - - if (selectedCount === 0) { - return ''; - } - - // Case 1: Single selected item - show item name - if (selectedCount === 1) { - const selectedKey = - selectedItemIds === 'all' ? currentCollection.items[0].id : selectedItemIds.values().next().value; - const selectedItem = currentCollection.items.find(item => item.id === selectedKey); - return selectedItem?.name ?? ''; - } - // Case 2: Multiple selected items - show count - if (selectedCount > 1) { - return formatMessage(messages.numFilesSelected, { numSelected: selectedCount }); - } - return ''; - }, [currentCollection.items, formatMessage, selectedItemIds]); + const selectedItemText = useSelectedItemText(currentCollection, selectedItemIds); // Case 1 and 2: selected item text with X button if (selectedItemText) { diff --git a/src/elements/common/sub-header/SubHeaderRight.tsx b/src/elements/common/sub-header/SubHeaderRight.tsx index deac81b762..017071162a 100644 --- a/src/elements/common/sub-header/SubHeaderRight.tsx +++ b/src/elements/common/sub-header/SubHeaderRight.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Button } from '@box/blueprint-web'; import { Pencil } from '@box/blueprint-web-assets/icons/Fill'; import { useIntl } from 'react-intl'; +import type { Selection } from 'react-aria-components'; import Sort from './Sort'; import Add from './Add'; import GridViewSlider from '../../../components/grid-view/GridViewSlider'; @@ -27,9 +28,11 @@ export interface SubHeaderRightProps { onCreate: () => void; onGridViewSliderChange: (newSliderValue: number) => void; onSortChange: (sortBy: SortBy, sortDirection: SortDirection) => void; + onMetadataSidePanelToggle?: () => void; onUpload: () => void; onViewModeChange?: (viewMode: ViewMode) => void; portalElement?: HTMLElement; + selectedItemIds?: Selection; view: View; viewMode: ViewMode; } @@ -45,9 +48,11 @@ const SubHeaderRight = ({ onCreate, onGridViewSliderChange, onSortChange, + onMetadataSidePanelToggle, onUpload, onViewModeChange, portalElement, + selectedItemIds, view, viewMode, }: SubHeaderRightProps) => { @@ -60,6 +65,7 @@ const SubHeaderRight = ({ const showSort: boolean = isFolder && hasItems; const showAdd: boolean = (!!canUpload || !!canCreateNewFolder) && isFolder; const isMetadataView: boolean = view === VIEW_METADATA; + const hasSelectedItems: boolean = !!(selectedItemIds && (selectedItemIds === 'all' || selectedItemIds.size > 0)); return (
{!isMetadataView && ( @@ -90,8 +96,8 @@ const SubHeaderRight = ({ )} - {isMetadataView && isMetadataViewV2Feature && ( - )} diff --git a/src/elements/content-explorer/ContentExplorer.scss b/src/elements/content-explorer/ContentExplorer.scss index 5741e32036..ca1fcf948a 100644 --- a/src/elements/content-explorer/ContentExplorer.scss +++ b/src/elements/content-explorer/ContentExplorer.scss @@ -7,5 +7,17 @@ .bcpr { z-index: 1; // Prevents overlay issues with list-item when a file is previewed } + + .be-app-element { + flex-direction: row; + gap: var(--space-4); + } + + .bce-ContentExplorer-main { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + } } } diff --git a/src/elements/content-explorer/ContentExplorer.tsx b/src/elements/content-explorer/ContentExplorer.tsx index d3d05964ba..fecd0588bc 100644 --- a/src/elements/content-explorer/ContentExplorer.tsx +++ b/src/elements/content-explorer/ContentExplorer.tsx @@ -24,6 +24,7 @@ import ThemingStyles from '../common/theming'; import API from '../../api'; import MetadataQueryAPIHelperV2 from './MetadataQueryAPIHelper'; import MetadataQueryAPIHelper from '../../features/metadata-based-view/MetadataQueryAPIHelper'; +import MetadataSidePanel from './MetadataSidePanel'; import Footer from './Footer'; import PreviewDialog from '../common/preview-dialog/PreviewDialog'; import ShareDialog from './ShareDialog'; @@ -169,6 +170,7 @@ type State = { isCreateFolderModalOpen: boolean; isDeleteModalOpen: boolean; isLoading: boolean; + isMetadataSidePanelOpen: boolean; isPreviewModalOpen: boolean; isRenameModalOpen: boolean; isShareModalOpen: boolean; @@ -294,6 +296,7 @@ class ContentExplorer extends Component { isCreateFolderModalOpen: false, isDeleteModalOpen: false, isLoading: false, + isMetadataSidePanelOpen: false, isPreviewModalOpen: false, isRenameModalOpen: false, isShareModalOpen: false, @@ -1562,7 +1565,11 @@ class ContentExplorer extends Component { selectedKeys: selectedItemIds, onSelectionChange: (ids: Selection) => { onSelectionChange?.(ids); - this.setState({ selectedItemIds: ids }); + const isSelectionEmpty = ids !== 'all' && ids.size === 0; + this.setState({ + selectedItemIds: ids, + ...(isSelectionEmpty && { isMetadataSidePanelOpen: false }), + }); }, }, }; @@ -1644,7 +1651,32 @@ class ContentExplorer extends Component { }; clearSelectedItemIds = () => { - this.setState({ selectedItemIds: new Set() }); + this.setState({ + selectedItemIds: new Set(), + isMetadataSidePanelOpen: false, + }); + }; + + /** + * Toggle metadata side panel visibility + * + * @private + * @return {void} + */ + onMetadataSidePanelToggle = () => { + this.setState(prevState => ({ + isMetadataSidePanelOpen: !prevState.isMetadataSidePanelOpen, + })); + }; + + /** + * Close metadata side panel + * + * @private + * @return {void} + */ + closeMetadataSidePanel = () => { + this.setState({ isMetadataSidePanelOpen: false }); }; /** @@ -1706,6 +1738,7 @@ class ContentExplorer extends Component { isCreateFolderModalOpen, isDeleteModalOpen, isLoading, + isMetadataSidePanelOpen, isPreviewModalOpen, isRenameModalOpen, isShareModalOpen, @@ -1714,6 +1747,7 @@ class ContentExplorer extends Component { metadataTemplate, rootName, selected, + selectedItemIds, view, }: State = this.state; @@ -1723,6 +1757,7 @@ class ContentExplorer extends Component { const allowUpload: boolean = canUpload && !!can_upload; const allowCreate: boolean = canCreateNewFolder && !!can_upload; const isDefaultViewMetadata: boolean = defaultView === DEFAULT_VIEW_METADATA; + const isMetadataViewV2Feature = isFeatureEnabled(features, 'contentExplorer.metadataViewV2'); const isErrorView: boolean = view === VIEW_ERROR; const viewMode = this.getViewMode(); @@ -1741,75 +1776,89 @@ class ContentExplorer extends Component {
- {!isDefaultViewMetadata &&
} - - - - - {!isErrorView && ( -
- -
+
+ {!isDefaultViewMetadata && ( +
+ )} + + + + + + {!isErrorView && ( +
+ +
+ )} +
+ {isDefaultViewMetadata && isMetadataViewV2Feature && isMetadataSidePanelOpen && ( + )}
{allowUpload && !!this.appElement ? ( diff --git a/src/elements/content-explorer/MetadataSidePanel.scss b/src/elements/content-explorer/MetadataSidePanel.scss new file mode 100644 index 0000000000..560fc938a2 --- /dev/null +++ b/src/elements/content-explorer/MetadataSidePanel.scss @@ -0,0 +1,12 @@ +.bce-MetadataSidePanel-subtitle { + display: flex; + align-items: center; +} + +.bce-MetadataSidePanel-content { + padding: var(--space-4); + + [data-target-id='TextButton-deleteButton'] { + visibility: hidden; + } +} diff --git a/src/elements/content-explorer/MetadataSidePanel.tsx b/src/elements/content-explorer/MetadataSidePanel.tsx new file mode 100644 index 0000000000..707b841a8e --- /dev/null +++ b/src/elements/content-explorer/MetadataSidePanel.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; + +import { IconButton, SidePanel, Text } from '@box/blueprint-web'; +import { XMark } from '@box/blueprint-web-assets/icons/Fill/index'; +import { FileDefault } from '@box/blueprint-web-assets/icons/Line/index'; +import { + AutofillContextProvider, + FormValues, + JSONPatchOperations, + MetadataInstance, + MetadataInstanceForm, +} from '@box/metadata-editor'; + +import type { Selection } from 'react-aria-components'; +import type { Collection } from '../../common/types/core'; +import type { MetadataTemplate } from '../../common/types/metadata'; +import { getTemplateInstance, useSelectedItemText } from './utils'; + +import messages from '../common/messages'; + +import './MetadataSidePanel.scss'; + +export interface MetadataSidePanelProps { + currentCollection: Collection; + onClose: () => void; + metadataTemplate: MetadataTemplate; + selectedItemIds: Selection; +} + +const MetadataSidePanel = ({ + currentCollection, + onClose, + selectedItemIds, + metadataTemplate, +}: MetadataSidePanelProps) => { + const { formatMessage } = useIntl(); + const [isEditing, setIsEditing] = useState(false); + const [isUnsavedChangesModalOpen, setIsUnsavedChangesModalOpen] = useState(false); + + const selectedItemText = useSelectedItemText(currentCollection, selectedItemIds); + const selectedItems = + selectedItemIds === 'all' + ? currentCollection.items + : currentCollection.items.filter(item => selectedItemIds.has(item.id)); + const templateInstance = getTemplateInstance(metadataTemplate, selectedItems); + + const handleMetadataInstanceEdit = () => { + setIsEditing(true); + }; + + const handleMetadataInstanceFormCancel = () => { + setIsEditing(false); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleMetadataInstanceFormChange = (values: FormValues) => { + // TODO: Implement on form change + }; + + const handleMetadataInstanceFormDiscardUnsavedChanges = () => { + setIsUnsavedChangesModalOpen(false); + setIsEditing(false); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleMetadataInstanceFormSubmit = async (values: FormValues, operations: JSONPatchOperations) => { + // TODO: Implement onSave callback + }; + + return ( + + +
+ + {formatMessage(messages.sidebarMetadataTitle)} + +
+ + + {selectedItemText} + +
+
+ +
+ +
+ + {isEditing ? ( + + ) : ( + + )} + +
+
+
+ ); +}; + +export default MetadataSidePanel; diff --git a/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx b/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx index 8b77e83f69..ee62cf2848 100644 --- a/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +++ b/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; +import type { MetadataFieldType } from '@box/metadata-view'; + import { render, screen, waitFor, within } from '../../../test-utils/testing-library'; import { ContentExplorerComponent as ContentExplorer, ContentExplorerProps } from '../ContentExplorer'; import { mockRecentItems, mockRootFolder, mockRootFolderSharedLink } from '../../common/__mocks__/mockRootFolder'; @@ -77,7 +79,13 @@ describe('elements/content-explorer/ContentExplorer', () => { const renderComponent = ({ features, ...props }: Partial = {}) => { return render( - + , ); }; @@ -414,31 +422,87 @@ describe('elements/content-explorer/ContentExplorer', () => { expect(screen.getByText('Technology')).toBeInTheDocument(); expect(screen.getByText('November 16, 2023')).toBeInTheDocument(); }); + describe('Metadata View V2', () => { - test('should render metadata view button', async () => { - renderComponent({ - defaultView: 'metadata', - features: { - contentExplorer: { - metadataViewV2: true, - }, + const { scope: templateScope, templateKey } = mockSchema; + const metadataScopeAndKey = `${templateScope}.${templateKey}`; + const metadataFieldNamePrefix = `metadata.${metadataScopeAndKey}`; + const metadataQuery = { + from: metadataScopeAndKey, + ancestor_folder_id: '69083462919', + sort_by: [ + { + field_key: `${metadataFieldNamePrefix}.${mockSchema.fields[0].key}`, // Default to sorting by the first field in the schema + direction: 'asc', + }, + ], + fields: [ + // Default to returning all fields in the metadata template schema, and name as a standalone (non-metadata) field + ...mockSchema.fields.map(field => `${metadataFieldNamePrefix}.${field.key}`), + 'name', + ], + }; + const fieldsToShow = [ + { key: `${metadataFieldNamePrefix}.name`, canEdit: false, displayName: 'Alias' }, + { key: `${metadataFieldNamePrefix}.industry`, canEdit: true }, + { key: `${metadataFieldNamePrefix}.last_contacted_at`, canEdit: true }, + { key: `${metadataFieldNamePrefix}.role`, canEdit: true }, + ]; + const columns = [ + { + // Always include the name column + textValue: 'Name', + id: 'name', + type: 'string' as const, + allowSorting: true, + minWidth: 150, + maxWidth: 150, + }, + ...mockSchema.fields.map(field => ({ + textValue: field.displayName, + id: `${metadataFieldNamePrefix}.${field.key}`, + type: field.type as MetadataFieldType, + allowSorting: true, + minWidth: 150, + maxWidth: 150, + })), + ]; + const defaultView = 'metadata'; + const metadataViewV2ElementProps = { + metadataViewProps: { + columns, + metadataTemplate: mockSchema, + tableProps: { + isSelectAllEnabled: true, }, + }, + metadataQuery, + fieldsToShow, + defaultView, + features: { + contentExplorer: { + metadataViewV2: true, + }, + }, + }; + + test('should render metadata view button', async () => { + renderComponent(metadataViewV2ElementProps); + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); }); - // two separate promises need to be resolved before the component is ready await waitFor(() => { - expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Switch to Grid View' })).toBeInTheDocument(); }); await waitFor(() => { - expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByRole('row', { name: /Child 2/i })).toBeInTheDocument(); }); - expect(screen.queryByRole('searchbox', { name: 'Search files and folders' })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Preview Test Folder' })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Switch to Grid View' })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Sort' })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Add' })).not.toBeInTheDocument(); + const selectAllCheckbox = screen.getByLabelText('Select all'); + await userEvent.click(selectAllCheckbox); + expect(screen.getByRole('button', { name: 'Metadata' })).toBeInTheDocument(); }); }); diff --git a/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx b/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx new file mode 100644 index 0000000000..7b811b38e0 --- /dev/null +++ b/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '../../../test-utils/testing-library'; +import MetadataSidePanel, { type MetadataSidePanelProps } from '../MetadataSidePanel'; + +// Mock scrollTo method +Object.defineProperty(Element.prototype, 'scrollTo', { + value: jest.fn(), + writable: true, +}); + +const mockCollection = { + items: [ + { + id: '1', + name: 'Test File 1.pdf', + type: 'file', + metadata: { + enterprise_123: { + mockTemplate: { + alias: 'mock-alias-1', + }, + }, + }, + }, + { + id: '2', + name: 'Test File 2.docx', + type: 'file', + metadata: { + enterprise_123: { + mockTemplate: { + alias: 'mock-alias-2', + }, + }, + }, + }, + ], + nextMarker: null, + offset: 0, + totalCount: 2, +}; + +const mockMetadataTemplate = { + id: 'template-id', + displayName: 'Mock Template', + scope: 'enterprise_123', + templateKey: 'mockTemplate', + type: 'metadata_template', + hidden: false, + fields: [ + { + id: '123', + key: 'alias', + displayName: 'Alias', + type: 'string', + hidden: false, + options: [], + }, + ], +}; + +const mockOnClose = jest.fn(); + +describe('elements/content-explorer/MetadataSidePanel', () => { + const defaultProps: MetadataSidePanelProps = { + currentCollection: mockCollection, + onClose: mockOnClose, + metadataTemplate: mockMetadataTemplate, + selectedItemIds: new Set(['1']), + }; + + const renderComponent = (props: Partial = {}) => + render(); + + test('renders the metadata title', () => { + renderComponent(); + expect(screen.getByText('Metadata')).toBeInTheDocument(); + }); + + test('renders the close button with proper aria-label', () => { + renderComponent(); + const closeButton = screen.getByLabelText('Close'); + expect(closeButton).toBeInTheDocument(); + }); + + test('renders the selected item text', () => { + renderComponent(); + expect(screen.getByText('Test File 1.pdf')).toBeInTheDocument(); + }); + + test('renders metadata instance (view mode) by default', () => { + renderComponent(); + const editTemplateButton = screen.getByLabelText('Edit Mock Template'); + expect(editTemplateButton).toBeInTheDocument(); + }); + + test('renders field value of selected item', () => { + renderComponent(); + const fieldValue = screen.getByText('mock-alias-1'); + expect(fieldValue).toBeInTheDocument(); + }); + + test('call onClose when close button is clicked', async () => { + renderComponent(); + const closeButton = screen.getByLabelText('Close'); + await userEvent.click(closeButton); + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + test('render correct subtitle when multiple items are selected', () => { + renderComponent({ selectedItemIds: new Set(['1', '2']) }); + const subtitle = screen.getByText('2 files selected'); + expect(subtitle).toBeInTheDocument(); + }); + + test('render cancel and submit button when in edit mode', async () => { + renderComponent(); + const editTemplateButton = screen.getByLabelText('Edit Mock Template'); + await userEvent.click(editTemplateButton); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + expect(cancelButton).toBeInTheDocument(); + const submitButton = screen.getByRole('button', { name: 'Save' }); + expect(submitButton).toBeInTheDocument(); + }); +}); diff --git a/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx b/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx index 0f4377080b..1758b55f3b 100644 --- a/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +++ b/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx @@ -164,6 +164,32 @@ export const metadataViewV2WithInitialFilterValues: Story = { }, }; +export const sidePanelOpenWithSingleItemSelected: Story = { + args: { + ...metadataViewV2ElementProps, + metadataViewProps: { + columns, + tableProps: { + isSelectAllEnabled: true, + }, + }, + }, + + play: async ({ canvas }) => { + await waitFor(() => { + expect(canvas.getByRole('row', { name: /Child 2/i })).toBeInTheDocument(); + }); + + // Select the first row by clicking its checkbox + const firstRow = canvas.getByRole('row', { name: /Child 2/i }); + const checkbox = within(firstRow).getByRole('checkbox'); + await userEvent.click(checkbox); + + const metadataButton = canvas.getByRole('button', { name: 'Metadata' }); + await userEvent.click(metadataButton); + }, +}; + const meta: Meta = { title: 'Elements/ContentExplorer/tests/MetadataView/visual', component: ContentExplorer, diff --git a/src/elements/content-explorer/utils.ts b/src/elements/content-explorer/utils.ts new file mode 100644 index 0000000000..4c3abf0c6c --- /dev/null +++ b/src/elements/content-explorer/utils.ts @@ -0,0 +1,58 @@ +import { useMemo } from 'react'; +import { useIntl } from 'react-intl'; + +import type { MetadataTemplate } from '@box/metadata-editor'; +import type { Selection } from 'react-aria-components'; +import type { BoxItem, Collection } from '../../common/types/core'; + +import messages from '../common/messages'; + +// Get selected item text +export function useSelectedItemText(currentCollection: Collection, selectedItemIds: Selection): string { + const { formatMessage } = useIntl(); + + return useMemo(() => { + const selectedCount = selectedItemIds === 'all' ? currentCollection.items.length : selectedItemIds.size; + if (selectedCount === 0) return ''; + + // Case 1: Single selected item - show item name + if (selectedCount === 1) { + const selectedKey = + selectedItemIds === 'all' ? currentCollection.items[0].id : selectedItemIds.values().next().value; + const selectedItem = currentCollection.items.find(item => item.id === selectedKey); + return selectedItem?.name ?? ''; + } + + // Case 2: Multiple selected items - show count + return formatMessage(messages.numFilesSelected, { numSelected: selectedCount }); + }, [currentCollection.items, formatMessage, selectedItemIds]); +} + +// Get template instance based on metadata template and selected items +export function getTemplateInstance(metadataTemplate: MetadataTemplate, selectedItems: BoxItem[]) { + const { displayName, fields, hidden, id, scope, templateKey, type } = metadataTemplate; + + const selectedItemsFields = fields.map( + ({ displayName: fieldDisplayName, hidden: fieldHidden, id: fieldId, key, options, type: fieldType }) => ({ + displayName: fieldDisplayName, + hidden: fieldHidden, + id: fieldId, + key, + options, + type: fieldType, + // TODO: Add support for multiple selected items + value: selectedItems[0].metadata[scope][templateKey][key], + }), + ); + + return { + canEdit: true, + displayName, + hidden, + id, + fields: selectedItemsFields, + scope, + templateKey, + type, + }; +}