diff --git a/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.test.tsx b/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.test.tsx index 735ff043fdf..ef733897a07 100644 --- a/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.test.tsx +++ b/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.test.tsx @@ -68,7 +68,7 @@ describe('WebSocketSyncWrapper', () => { const queryClientMock = createQueryClientMock(); const invalidator = SyncSuccessQueriesInvalidator.getInstance(queryClientMock, org, app); - invalidator.invalidateQueryByFileLocation = jest.fn(); + invalidator.invalidateQueriesByFileLocation = jest.fn(); const mockOnWSMessageReceived = jest .fn() .mockImplementation((callback: Function) => callback(syncSuccessMock)); @@ -80,7 +80,7 @@ describe('WebSocketSyncWrapper', () => { renderWebSocketSyncWrapper(); await waitFor(() => { - expect(invalidator.invalidateQueryByFileLocation).toHaveBeenCalledWith( + expect(invalidator.invalidateQueriesByFileLocation).toHaveBeenCalledWith( syncSuccessMock.source.name, ); }); diff --git a/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.tsx b/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.tsx index 733e8e5219a..53284a5a7db 100644 --- a/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.tsx +++ b/frontend/app-development/components/WebSocketSyncWrapper/WebSocketSyncWrapper.tsx @@ -48,8 +48,8 @@ export const WebSocketSyncWrapper = ({ const isSuccessMessage = 'source' in message; if (isSuccessMessage) { - // Please extend the "fileNameCacheKeyMap" inside the "SyncSuccessQueriesInvalidator" class. Do not add query-client invalidation directly here. - invalidator.invalidateQueryByFileLocation(message.source.name); + // Please extend the "fileNameCacheKeysMap" inside the "SyncSuccessQueriesInvalidator" class. Do not add query-client invalidation directly here. + invalidator.invalidateQueriesByFileLocation(message.source.name); } }); diff --git a/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts b/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts index 68a2d3ff174..cedff5138b8 100644 --- a/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts +++ b/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts @@ -46,6 +46,7 @@ export const useAddLayoutSetMutation = (org: string, app: string) => { // when process-editor renders the tasks and 'adds' them on first mount, when they already exists. if (isLayoutSets(layoutSets)) { queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSets); + queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSetsExtended, org, app] }); } }, }); diff --git a/frontend/app-development/hooks/mutations/useDeleteLayoutSetMutation.ts b/frontend/app-development/hooks/mutations/useDeleteLayoutSetMutation.ts index 57cfa3b9a10..1b439b227e0 100644 --- a/frontend/app-development/hooks/mutations/useDeleteLayoutSetMutation.ts +++ b/frontend/app-development/hooks/mutations/useDeleteLayoutSetMutation.ts @@ -11,6 +11,7 @@ export const useDeleteLayoutSetMutation = (org: string, app: string) => { deleteLayoutSet(org, app, layoutSetIdToUpdate), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSets, org, app] }); + queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSetsExtended, org, app] }); queryClient.invalidateQueries({ queryKey: [QueryKey.AppMetadataModelIds, org, app] }); }, }); diff --git a/frontend/app-development/hooks/mutations/useUpdateLayoutSetIdMutation.ts b/frontend/app-development/hooks/mutations/useUpdateLayoutSetIdMutation.ts index 87baa945ea7..40a218cac0c 100644 --- a/frontend/app-development/hooks/mutations/useUpdateLayoutSetIdMutation.ts +++ b/frontend/app-development/hooks/mutations/useUpdateLayoutSetIdMutation.ts @@ -16,6 +16,7 @@ export const useUpdateLayoutSetIdMutation = (org: string, app: string) => { }) => updateLayoutSetId(org, app, layoutSetIdToUpdate, newLayoutSetId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSets, org, app] }); + queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSetsExtended, org, app] }); }, }); }; diff --git a/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.test.ts b/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.test.ts index fb3380ea275..7de62a67c34 100644 --- a/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.test.ts +++ b/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.test.ts @@ -43,7 +43,7 @@ describe('useUpdateProcessDataTypeMutation', () => { await renderHook({ queryClient }); - expect(invalidateQueriesSpy).toHaveBeenCalledTimes(2); + expect(invalidateQueriesSpy).toHaveBeenCalledTimes(3); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: [QueryKey.AppMetadataModelIds, org, app], }); diff --git a/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.ts b/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.ts index 8e2dea1b706..5aae6913df6 100644 --- a/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.ts +++ b/frontend/app-development/hooks/mutations/useUpdateProcessDataTypesMutation.ts @@ -11,6 +11,7 @@ export const useUpdateProcessDataTypesMutation = (org: string, app: string) => { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: [QueryKey.AppMetadataModelIds, org, app] }); await queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSets, org, app] }); + await queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSetsExtended, org, app] }); }, }); }; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 62ccde8e5d8..0e38334e340 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1200,6 +1200,8 @@ "ux_editor.component_help_text.TextArea": "Du bruker Stort tekstfelt når du vil at brukerne skal skrive litt lengre tekst.", "ux_editor.component_help_text.default": "Ingen informasjon å vise om denne komponenten.", "ux_editor.component_help_text_general_title": "Åpne hjelpetekst for komponenten", + "ux_editor.component_other_properties_hide_many_settings": "Skjul flere innstillinger", + "ux_editor.component_other_properties_show_many_settings": "Vis flere innstillinger", "ux_editor.component_other_properties_title": "Andre innstillinger", "ux_editor.component_properties.action": "Handling", "ux_editor.component_properties.actions": "Handlinger", @@ -1425,11 +1427,14 @@ "ux_editor.component_properties.tableHeadersMobile": "Felter som skal vises i tabellens overskrift (mobil)", "ux_editor.component_properties.tabs": "Faner", "ux_editor.component_properties.tagName": "Tag-navn", - "ux_editor.component_properties.target": "Mål", - "ux_editor.component_properties.target_description": "Mål for oppsummeringskomponenten", + "ux_editor.component_properties.target": "Hva vil du vise i oppsummeringen?", + "ux_editor.component_properties.target_description": "Her kan du velge hva som skal vises på oppsummeringssiden. Du kan for eksempel vise hele sidegrupper, utvalgte sider eller utvalgte komponenter", "ux_editor.component_properties.target_invalid": "Ugyldig mål", - "ux_editor.component_properties.target_taskId": "Oppgave-ID", - "ux_editor.component_properties.target_type": "Type", + "ux_editor.component_properties.target_taskId": "1. Oppsummer fra denne sidegruppen", + "ux_editor.component_properties.target_type": "2. Vis sidegruppe, side eller komponent", + "ux_editor.component_properties.target_unit_component": "3. Komponent", + "ux_editor.component_properties.target_unit_layout_set": "3. Sidegruppe", + "ux_editor.component_properties.target_unit_page": "3. Side", "ux_editor.component_properties.taskId": "Oppgave-ID", "ux_editor.component_properties.timeStamp": "Inkluder tidsstempel i dato (på som standard)", "ux_editor.component_properties.triggers": "Feltet skal utløse:", diff --git a/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.test.tsx b/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.test.tsx index b78acd0001b..ea8ff99beaa 100644 --- a/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.test.tsx @@ -46,11 +46,13 @@ const codeList: CodeList = [ helpText: 'Test 3 help text', }, ]; +const onBlurAny = jest.fn(); const onChange = jest.fn(); const onInvalid = jest.fn(); const defaultProps: StudioCodeListEditorProps = { codeList, texts, + onBlurAny, onChange, onInvalid, }; @@ -119,8 +121,7 @@ describe('StudioCodeListEditor', () => { const labelInput = screen.getByRole('textbox', { name: texts.itemLabel(1) }); const newValue = 'new text'; await user.type(labelInput, newValue); - await user.tab(); - expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(newValue.length); expect(onChange).toHaveBeenLastCalledWith([ { ...codeList[0], label: newValue }, codeList[1], @@ -134,8 +135,7 @@ describe('StudioCodeListEditor', () => { const valueInput = screen.getByRole('textbox', { name: texts.itemValue(1) }); const newValue = 'new text'; await user.type(valueInput, newValue); - await user.tab(); - expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(newValue.length); expect(onChange).toHaveBeenLastCalledWith([ { ...codeList[0], value: newValue }, codeList[1], @@ -149,8 +149,7 @@ describe('StudioCodeListEditor', () => { const descriptionInput = screen.getByRole('textbox', { name: texts.itemDescription(1) }); const newValue = 'new text'; await user.type(descriptionInput, newValue); - await user.tab(); - expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(newValue.length); expect(onChange).toHaveBeenLastCalledWith([ { ...codeList[0], description: newValue }, codeList[1], @@ -164,8 +163,7 @@ describe('StudioCodeListEditor', () => { const helpTextInput = screen.getByRole('textbox', { name: texts.itemHelpText(1) }); const newValue = 'new text'; await user.type(helpTextInput, newValue); - await user.tab(); - expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(newValue.length); expect(onChange).toHaveBeenLastCalledWith([ { ...codeList[0], helpText: newValue }, codeList[1], @@ -196,6 +194,21 @@ describe('StudioCodeListEditor', () => { ]); }); + it('Calls the onBlurAny callback with the current code list when an item in the table is blurred', async () => { + const user = userEvent.setup(); + renderCodeListEditor(); + const valueInput = screen.getByRole('textbox', { name: texts.itemValue(1) }); + const newValue = 'new text'; + await user.type(valueInput, newValue); + await user.tab(); + expect(onBlurAny).toHaveBeenCalledTimes(1); + expect(onBlurAny).toHaveBeenLastCalledWith([ + { ...codeList[0], value: newValue }, + codeList[1], + codeList[2], + ]); + }); + it('Updates itself when the user changes something', async () => { const user = userEvent.setup(); renderCodeListEditor(); @@ -267,8 +280,7 @@ describe('StudioCodeListEditor', () => { const validValueInput = screen.getByRole('textbox', { name: texts.itemValue(3) }); const newValue = 'new value'; await user.type(validValueInput, newValue); - await user.tab(); - expect(onInvalid).toHaveBeenCalledTimes(1); + expect(onInvalid).toHaveBeenCalledTimes(newValue.length); }); it('Does not trigger onInvalid if an invalid code list is changed to a valid state', async () => { diff --git a/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.tsx b/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.tsx index a4cff5e76da..d1da350578a 100644 --- a/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.tsx +++ b/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditor.tsx @@ -23,23 +23,32 @@ import { areThereCodeListErrors, findCodeListErrors, isCodeListValid } from './v import type { ValueErrorMap } from './types/ValueErrorMap'; import { StudioFieldset } from '../StudioFieldset'; import { StudioErrorMessage } from '../StudioErrorMessage'; +import type { Override } from '../../types/Override'; +import type { StudioInputTableProps } from '../StudioInputTable/StudioInputTable'; export type StudioCodeListEditorProps = { codeList: CodeList; - onChange: (codeList: CodeList) => void; + onBlurAny?: (codeList: CodeList) => void; + onChange?: (codeList: CodeList) => void; onInvalid?: () => void; texts: CodeListEditorTexts; }; export function StudioCodeListEditor({ codeList, + onBlurAny, onChange, onInvalid, texts, }: StudioCodeListEditorProps): ReactElement { return ( - + ); } @@ -48,6 +57,7 @@ type StatefulCodeListEditorProps = Omit; function StatefulCodeListEditor({ codeList: defaultCodeList, + onBlurAny, onChange, onInvalid, }: StatefulCodeListEditorProps): ReactElement { @@ -60,18 +70,32 @@ function StatefulCodeListEditor({ const handleChange = useCallback( (newCodeList: CodeList) => { setCodeList(newCodeList); - isCodeListValid(newCodeList) ? onChange(newCodeList) : onInvalid?.(); + isCodeListValid(newCodeList) ? onChange?.(newCodeList) : onInvalid?.(); }, [onChange, onInvalid], ); - return ; + const handleBlurAny = useCallback(() => { + onBlurAny?.(codeList); + }, [onBlurAny, codeList]); + + return ( + + ); } -type InternalCodeListEditorProps = Omit; +type InternalCodeListEditorProps = Override< + Pick, + Omit +>; function ControlledCodeListEditor({ codeList, + onBlurAny, onChange, }: InternalCodeListEditorProps): ReactElement { const { texts } = useStudioCodeListEditorContext(); @@ -86,12 +110,18 @@ function ControlledCodeListEditor({ return ( - + ); } + type InternalCodeListEditorWithErrorsProps = InternalCodeListEditorProps & ErrorsProps; function CodeListTable(props: InternalCodeListEditorWithErrorsProps): ReactElement { @@ -107,11 +137,14 @@ function EmptyCodeListTable(): ReactElement { return {texts.emptyCodeList}; } -function CodeListTableWithContent(props: InternalCodeListEditorWithErrorsProps): ReactElement { +function CodeListTableWithContent({ + onBlurAny, + ...rest +}: InternalCodeListEditorWithErrorsProps): ReactElement { return ( - + - + ); } @@ -145,7 +178,7 @@ function TableBody({ [codeList, onChange], ); - const handleBlur = useCallback( + const handleChange = useCallback( (index: number, newItem: CodeListItem) => { const updatedCodeList = changeCodeListItem(codeList, index, newItem); onChange(updatedCodeList); @@ -161,7 +194,7 @@ function TableBody({ item={item} key={index} number={index + 1} - onBlur={(newItem) => handleBlur(index, newItem)} + onChange={(newItem) => handleChange(index, newItem)} onDeleteButtonClick={() => handleDeleteButtonClick(index)} /> ))} diff --git a/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditorRow/StudioCodeListEditorRow.tsx b/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditorRow/StudioCodeListEditorRow.tsx index 161b6d04264..501f9f4fcac 100644 --- a/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditorRow/StudioCodeListEditorRow.tsx +++ b/frontend/libs/studio-components/src/components/StudioCodelistEditor/StudioCodeListEditorRow/StudioCodeListEditorRow.tsx @@ -13,7 +13,7 @@ type StudioCodeListEditorRowProps = { error: ValueError | null; item: CodeListItem; number: number; - onBlur: (newItem: CodeListItem) => void; + onChange: (newItem: CodeListItem) => void; onDeleteButtonClick: () => void; }; @@ -21,7 +21,7 @@ export function StudioCodeListEditorRow({ error, item, number, - onBlur, + onChange, onDeleteButtonClick, }: StudioCodeListEditorRowProps) { const { texts } = useStudioCodeListEditorContext(); @@ -29,33 +29,33 @@ export function StudioCodeListEditorRow({ const handleLabelChange = useCallback( (label: string) => { const updatedItem = changeLabel(item, label); - onBlur(updatedItem); + onChange(updatedItem); }, - [item, onBlur], + [item, onChange], ); const handleDescriptionChange = useCallback( (description: string) => { const updatedItem = changeDescription(item, description); - onBlur(updatedItem); + onChange(updatedItem); }, - [item, onBlur], + [item, onChange], ); const handleValueChange = useCallback( (value: string) => { const updatedItem = changeValue(item, value); - onBlur(updatedItem); + onChange(updatedItem); }, - [item, onBlur], + [item, onChange], ); const handleHelpTextChange = useCallback( (helpText: string) => { const updatedItem = changeHelpText(item, helpText); - onBlur(updatedItem); + onChange(updatedItem); }, - [item, onBlur], + [item, onChange], ); return ( @@ -64,22 +64,22 @@ export function StudioCodeListEditorRow({ autoComplete='off' error={error && texts.valueErrors[error]} label={texts.itemValue(number)} - onBlur={handleValueChange} + onChange={handleValueChange} value={item.value} /> @@ -90,23 +90,23 @@ export function StudioCodeListEditorRow({ type TextfieldCellProps = { error?: string; label: string; - onBlur: (newString: string) => void; + onChange: (newString: string) => void; value: CodeListItemValue; autoComplete?: HTMLInputAutoCompleteAttribute; }; -function TextfieldCell({ error, label, value, onBlur, autoComplete }: TextfieldCellProps) { +function TextfieldCell({ error, label, value, onChange, autoComplete }: TextfieldCellProps) { const ref = useRef(null); useEffect((): void => { ref.current?.setCustomValidity(error || ''); }, [error]); - const handleBlur = useCallback( + const handleChange = useCallback( (event: React.ChangeEvent): void => { - onBlur(event.target.value); + onChange(event.target.value); }, - [onBlur], + [onChange], ); const handleFocus = useCallback((event: FocusEvent): void => { @@ -118,7 +118,7 @@ function TextfieldCell({ error, label, value, onBlur, autoComplete }: TextfieldC aria-label={label} autoComplete={autoComplete} className={classes.textfieldCell} - onBlur={handleBlur} + onChange={handleChange} onFocus={handleFocus} ref={ref} value={(value as string) ?? ''} diff --git a/frontend/libs/studio-components/src/components/StudioTreeView/StudioTreeViewItem/StudioTreeViewItem.module.css b/frontend/libs/studio-components/src/components/StudioTreeView/StudioTreeViewItem/StudioTreeViewItem.module.css index 53072361185..ef9e3495f41 100644 --- a/frontend/libs/studio-components/src/components/StudioTreeView/StudioTreeViewItem/StudioTreeViewItem.module.css +++ b/frontend/libs/studio-components/src/components/StudioTreeView/StudioTreeViewItem/StudioTreeViewItem.module.css @@ -13,6 +13,12 @@ width: 100%; } +.button span { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .button[aria-selected='true'] { background-color: var(--studio-treeitem-selected-background-colour); border-color: var(--studio-treeitem-vertical-line-colour-root); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx index b2a4c57cfb4..c2553e24db5 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx @@ -24,7 +24,7 @@ type CodeListProps = { function CodeList({ codeList, onUpdateCodeList }: CodeListProps) { const editorTexts: CodeListEditorTexts = useOptionListEditorTexts(); - const handleUpdateCodeList = (updatedCodeList: StudioComponentsCodeList): void => { + const handleBlurAny = (updatedCodeList: StudioComponentsCodeList): void => { const updatedCodeListWithMetadata = updateCodeListWithMetadata(codeList, updatedCodeList); onUpdateCodeList(updatedCodeListWithMetadata); }; @@ -36,7 +36,7 @@ function CodeList({ codeList, onUpdateCodeList }: CodeListProps) { diff --git a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts index 87de0c60acd..1703079e69f 100644 --- a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts +++ b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts @@ -19,12 +19,12 @@ describe('SyncSuccessQueriesInvalidator', () => { jest.clearAllMocks(); }); - it('should invalidate query cache only once when invalidateQueryByFileLocation is called', async () => { + it('should invalidate query cache only once when invalidateQueriesByFileLocation is called', async () => { const queriesInvalidator = SyncSuccessQueriesInvalidator.getInstance(queryClientMock, org, app); const fileName = 'applicationmetadata.json'; - queriesInvalidator.invalidateQueryByFileLocation(fileName); - queriesInvalidator.invalidateQueryByFileLocation(fileName); + queriesInvalidator.invalidateQueriesByFileLocation(fileName); + queriesInvalidator.invalidateQueriesByFileLocation(fileName); await waitFor(() => expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ queryKey: [QueryKey.AppMetadata, org, app], @@ -33,22 +33,22 @@ describe('SyncSuccessQueriesInvalidator', () => { expect(queryClientMock.invalidateQueries).toHaveBeenCalledTimes(1); }); - it('should not invalidate query cache when invalidateQueryByFileLocation is called with an unknown file name', async () => { + it('should not invalidate query cache when invalidateQueriesByFileLocation is called with an unknown file name', async () => { const queriesInvalidator = SyncSuccessQueriesInvalidator.getInstance(queryClientMock, org, app); const fileName = 'unknown.json'; - queriesInvalidator.invalidateQueryByFileLocation(fileName); + queriesInvalidator.invalidateQueriesByFileLocation(fileName); await new Promise((resolve) => setTimeout(resolve, 501)); expect(queryClientMock.invalidateQueries).not.toHaveBeenCalled(); }); - it('should invalidate query cache with layoutSetName identifier when invalidateQueryByFileLocation is called and layoutSetName has been set', async () => { + it('should invalidate query cache with layoutSetName identifier when invalidateQueriesByFileLocation is called and layoutSetName has been set', async () => { const queriesInvalidator = SyncSuccessQueriesInvalidator.getInstance(queryClientMock, org, app); queriesInvalidator.layoutSetName = selectedLayoutSet; const fileName = 'Settings.json'; - queriesInvalidator.invalidateQueryByFileLocation(fileName); + queriesInvalidator.invalidateQueriesByFileLocation(fileName); await waitFor(() => { expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ @@ -58,12 +58,12 @@ describe('SyncSuccessQueriesInvalidator', () => { expect(queryClientMock.invalidateQueries).toHaveBeenCalledTimes(1); }); - it('should invalidate layouts query cache with layoutSetName identifier when invalidateQueryByFileLocation is called and layoutSetName has been set', async () => { + it('should invalidate layouts query cache with layoutSetName identifier when invalidateQueriesByFileLocation is called and layoutSetName has been set', async () => { const queriesInvalidator = SyncSuccessQueriesInvalidator.getInstance(queryClientMock, org, app); queriesInvalidator.layoutSetName = selectedLayoutSet; const folderName = 'layouts'; - queriesInvalidator.invalidateQueryByFileLocation(folderName); + queriesInvalidator.invalidateQueriesByFileLocation(folderName); await waitFor(() => expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ diff --git a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts index 0e240ae8590..3b88d565b4a 100644 --- a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts +++ b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts @@ -21,16 +21,19 @@ export class SyncSuccessQueriesInvalidator extends Queue { private _queryClient: QueryClient; // Maps file names to their cache keys for invalidation upon sync success - can be extended to include more files - private readonly fileNameCacheKeyMap: Record> = { - 'applicationmetadata.json': [QueryKey.AppMetadata, '[org]', '[app]'], - 'layout-sets.json': [QueryKey.LayoutSets, '[org]', '[app]'], - 'policy.xml': [QueryKey.AppPolicy, '[org]', '[app]'], - 'Settings.json': [QueryKey.FormLayoutSettings, '[org]', '[app]', '[layoutSetName]'], + private readonly fileNameCacheKeysMap: Record>> = { + 'applicationmetadata.json': [[QueryKey.AppMetadata, '[org]', '[app]']], + 'layout-sets.json': [ + [QueryKey.LayoutSets, '[org]', '[app]'], + [QueryKey.LayoutSetsExtended, '[org]', '[app]'], + ], + 'policy.xml': [[QueryKey.AppPolicy, '[org]', '[app]']], + 'Settings.json': [[QueryKey.FormLayoutSettings, '[org]', '[app]', '[layoutSetName]']], }; // Maps folder names to their cache keys for invalidation upon sync success - can be extended to include more folders - private readonly folderNameCacheKeyMap: Record> = { - layouts: [QueryKey.FormLayouts, '[org]', '[app]'], + private readonly folderNameCacheKeysMap: Record>> = { + layouts: [[QueryKey.FormLayouts, '[org]', '[app]']], }; public set layoutSetName(layoutSetName: string) { @@ -67,32 +70,36 @@ export class SyncSuccessQueriesInvalidator extends Queue { SyncSuccessQueriesInvalidator.instance = null; } - public invalidateQueryByFileLocation(fileOrFolderName: string): void { - const cacheKey = this.getCacheKeyByFileLocation(fileOrFolderName); - if (!cacheKey) return; + public invalidateQueriesByFileLocation(fileOrFolderName: string): void { + const cacheKeys = this.getCacheKeysByFileLocation(fileOrFolderName); + if (!cacheKeys) return; this.addTaskToQueue({ id: fileOrFolderName, callback: () => { - this._queryClient.invalidateQueries({ queryKey: cacheKey }); + cacheKeys.forEach((cacheKey) => { + this._queryClient.invalidateQueries({ queryKey: cacheKey }); + }); }, }); } - private getCacheKeyByFileLocation(fileOrFolderName: string): string[] { - const cacheKey = - this.fileNameCacheKeyMap[fileOrFolderName] || this.folderNameCacheKeyMap[fileOrFolderName]; - if (!cacheKey) return undefined; + private getCacheKeysByFileLocation(fileOrFolderName: string): Array { + const cacheKeys = + this.fileNameCacheKeysMap[fileOrFolderName] || this.folderNameCacheKeysMap[fileOrFolderName]; + if (!cacheKeys) return undefined; - return this.replaceCacheKeyPlaceholders(cacheKey); + return this.replaceCacheKeysPlaceholders(cacheKeys); } - private replaceCacheKeyPlaceholders(cacheKey: string[]): string[] { - return cacheKey.map((key) => - key - .replace('[org]', this._org) - .replace('[app]', this._app) - .replace('[layoutSetName]', this._layoutSetName), + private replaceCacheKeysPlaceholders(cacheKeys: Array): Array { + return cacheKeys.map((cacheKey) => + cacheKey.map((key) => + key + .replace('[org]', this._org) + .replace('[app]', this._app) + .replace('[layoutSetName]', this._layoutSetName), + ), ); } } diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx index 98e31569c4c..1f0c5df9a78 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx @@ -125,6 +125,31 @@ describe('CreateNewSubformLayoutSet ', () => { expect(saveButton).toBeDisabled(); }); + it('Toggles the save button disabling based on data model input validation', async () => { + const user = userEvent.setup(); + renderCreateNewSubformLayoutSet({}); + + const input = screen.getByRole('textbox'); + await user.type(input, 'NewSubform'); + + const saveButton = screen.getByRole('button', { name: textMock('general.save') }); + + const displayDataModelInput = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model'), + }); + await user.click(displayDataModelInput); + + const dataModelInput = screen.getByRole('textbox', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model_label'), + }); + await user.type(dataModelInput, 'æøå'); + expect(saveButton).toBeDisabled(); + + await user.clear(dataModelInput); + await user.type(dataModelInput, 'datamodel'); + expect(saveButton).not.toBeDisabled(); + }); + it('enables save button when both input and data model is valid', async () => { const user = userEvent.setup(); renderCreateNewSubformLayoutSet({}); @@ -159,6 +184,31 @@ describe('CreateNewSubformLayoutSet ', () => { await user.type(dataModelInput, 'datamodel'); expect(saveButton).not.toBeDisabled(); }); + + it('Should toggle ErrorMessage visibility based on input validity', async () => { + const user = userEvent.setup(); + renderCreateNewSubformLayoutSet({}); + + const input = screen.getByRole('textbox'); + await user.type(input, 'NewSubform'); + + const displayDataModelInput = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model'), + }); + await user.click(displayDataModelInput); + + const dataModelInput = screen.getByRole('textbox', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model_label'), + }); + + await user.type(dataModelInput, 'new'); + const errorMessage = screen.getByText(textMock('schema_editor.error_reserved_keyword')); + expect(errorMessage).toBeInTheDocument(); + + await user.clear(dataModelInput); + await user.type(dataModelInput, 'datamodel'); + expect(errorMessage).not.toBeInTheDocument(); + }); }); type RenderCreateNewSubformLayoutSetProps = { diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx index c6efe918455..6a3699740e5 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx @@ -12,6 +12,11 @@ import { SubformDataModel } from './SubformDataModel'; import { CreateNewSubformButtons } from './CreateNewSubformButtons'; import { SubformInstructions } from './SubformInstructions'; import { useCreateSubform } from '@altinn/ux-editor/hooks/useCreateSubform'; +import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; +import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMetadataModelIdsQuery'; +import { useAppMetadataQuery } from 'app-shared/hooks/queries'; +import { extractDataTypeNamesFromAppMetadata } from 'app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/utils/validationUtils'; +import { useValidateSchemaName } from 'app-shared/hooks/useValidateSchemaName'; type CreateNewSubformSectionProps = { layoutSets: LayoutSets; @@ -33,26 +38,36 @@ export const CreateNewSubformSection = ({ }: CreateNewSubformSectionProps): React.ReactElement => { const { t } = useTranslation(); const { validateLayoutSetName } = useValidateLayoutSetName(); - const [nameError, setNameError] = useState(); - const [newDataModel, setNewDataModel] = useState(''); + const [newSubformNameError, setNewSubformNameError] = useState(); const [selectedDataModel, setSelectedDataModel] = useState(''); const [displayDataModelInput, setDisplayDataModelInput] = useState(false); const { createSubform, isPendingNewSubformMutation } = useCreateSubform(); + const [isNewDataModelFieldEmpty, setIsNewDataModelFieldEmpty] = useState(true); + + const { org, app } = useStudioEnvironmentParams(); + const { data: dataModelIds } = useAppMetadataModelIdsQuery(org, app, false); + const { data: appMetadata } = useAppMetadataQuery(org, app); + const dataTypeNames = extractDataTypeNamesFromAppMetadata(appMetadata); + const { + validateName, + nameError: dataModelNameError, + setNameError: setDataModelNameError, + } = useValidateSchemaName(dataModelIds, dataTypeNames); const handleSubformName = (subformName: string) => { const subformNameValidation = validateLayoutSetName(subformName, layoutSets); - setNameError(subformNameValidation); + setNewSubformNameError(subformNameValidation); }; const handleCloseButton = () => { if (displayDataModelInput) { - setNewDataModel(''); + setDataModelNameError(''); + setIsNewDataModelFieldEmpty(true); setDisplayDataModelInput(false); } else { setShowCreateSubformCard(false); } }; - const handleCreateSubformSubmit = (e: React.FormEvent): void => { e.preventDefault(); const formData: FormData = new FormData(e.currentTarget); @@ -68,8 +83,11 @@ export const CreateNewSubformSection = ({ }); }; - const hasInvalidSubformName = nameError === undefined || Boolean(nameError); - const hasInvalidDataModel = displayDataModelInput ? !newDataModel : !selectedDataModel; + const hasInvalidSubformName = newSubformNameError === undefined || Boolean(newSubformNameError); + const hasInvalidDataModel = displayDataModelInput + ? Boolean(dataModelNameError) || isNewDataModelFieldEmpty + : !selectedDataModel; + const disableSaveButton = hasInvalidSubformName || hasInvalidDataModel; return ( handleSubformName(e.target.value)} - error={nameError} + error={newSubformNameError} /> diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx index 6c9807f9bc8..5e9cb0ef9fc 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx @@ -8,8 +8,6 @@ import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMeta jest.mock('app-shared/hooks/queries/useAppMetadataModelIdsQuery'); -const user = userEvent.setup(); - const mockDataModelIds = ['dataModelId1', 'dataModelId2']; (useAppMetadataModelIdsQuery as jest.Mock).mockReturnValue({ data: mockDataModelIds }); @@ -41,6 +39,7 @@ describe('SubformDataModel', () => { }); it('Calls setDataModel when selecting an option', async () => { + const user = userEvent.setup(); const setSelectedDataModel = jest.fn(); renderSubformDataModelSelect({ setSelectedDataModel }); @@ -53,6 +52,7 @@ describe('SubformDataModel', () => { }); it('Should call setDisplayDataModelInput true when clicking create new data model button', async () => { + const user = userEvent.setup(); const setDisplayDataModelInput = jest.fn(); renderSubformDataModelSelect({ setDisplayDataModelInput }); const displayDataModelInput = screen.getByRole('button', { @@ -75,9 +75,12 @@ describe('SubformDataModel', () => { const defaultProps: SubformDataModelProps = { setDisplayDataModelInput: jest.fn(), - setNewDataModel: jest.fn(), displayDataModelInput: false, setSelectedDataModel: jest.fn(), + dataModelIds: mockDataModelIds, + validateName: jest.fn(), + dataModelNameError: '', + setIsTextfieldEmpty: jest.fn(), }; const renderSubformDataModelSelect = (props: Partial = {}) => { diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx index 32508217bb1..47d7212440b 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx @@ -2,30 +2,32 @@ import React from 'react'; import { StudioTextfield, StudioNativeSelect, StudioProperty } from '@studio/components'; import { LinkIcon } from '@studio/icons'; import { useTranslation } from 'react-i18next'; -import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; -import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMetadataModelIdsQuery'; import classes from './SubformDataModel.module.css'; export type SubformDataModelProps = { setDisplayDataModelInput: (setDisplayDataModelInput: boolean) => void; - setNewDataModel: (dataModelId: string) => void; displayDataModelInput: boolean; setSelectedDataModel: (dataModelId: string) => void; + dataModelIds?: string[]; + validateName: (name: string) => void; + dataModelNameError: string; + setIsTextfieldEmpty: (isEmpty: boolean) => void; }; export const SubformDataModel = ({ setDisplayDataModelInput, setSelectedDataModel, - setNewDataModel, displayDataModelInput, + dataModelIds, + validateName, + dataModelNameError, + setIsTextfieldEmpty, }: SubformDataModelProps): React.ReactElement => { const { t } = useTranslation(); - const { org, app } = useStudioEnvironmentParams(); - const { data: dataModelIds } = useAppMetadataModelIdsQuery(org, app, false); - const handleDataModel = (dataModelId: string) => { - // TODO: https://github.com/Altinn/altinn-studio/issues/14184 - setNewDataModel(dataModelId); + const handleNewDataModel = (dataModelId: string) => { + validateName(dataModelId); + setIsTextfieldEmpty(dataModelId === ''); }; const handleDisplayInput = () => { @@ -59,7 +61,8 @@ export const SubformDataModel = ({ name='newSubformDataModel' label={t('ux_editor.component_properties.subform.create_new_data_model_label')} size='sm' - onChange={(e) => handleDataModel(e.target.value)} + onChange={(e) => handleNewDataModel(e.target.value)} + error={dataModelNameError} /> ) : ( ({ })); describe('FormComponentConfig', () => { - it('should render expected components', async () => { + it('should render expected default components', async () => { render({}); + const properties = ['readOnly', 'required', 'hidden']; + for (const property of properties) { + expect( + await screen.findByText(textMock(`ux_editor.component_properties.${property}`)), + ).toBeInTheDocument(); + } + }); + it('should render the show-button', async () => { + render({}); + const button = screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_show_many_settings'), + }); + expect(button).toBeInTheDocument(); + }); + + it('should render the hide-button after clikcing on show-button', async () => { + const user = userEvent.setup(); + render({}); + const button = screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_show_many_settings'), + }); + expect(button).toBeInTheDocument(); + await user.click(button); + expect( + screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_hide_many_settings'), + }), + ).toBeInTheDocument(); + }); + + it('Should render the rest of the components when show-button is clicked and show hide-button', async () => { + const use = userEvent.setup(); + render({}); + const button = screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_show_many_settings'), + }); + expect(button).toBeInTheDocument(); + await use.click(button); const properties = [ - 'grid', - 'readOnly', - 'required', - 'hidden', 'renderAsSummary', 'variant', 'autocomplete', @@ -47,12 +81,15 @@ describe('FormComponentConfig', () => { 'pageBreak', 'formatting', ]; - for (const property of properties) { expect( await screen.findByText(textMock(`ux_editor.component_properties.${property}`)), ).toBeInTheDocument(); } + const hideButton = screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_hide_many_settings'), + }); + expect(hideButton).toBeInTheDocument(); }); it('should render "RedirectToLayoutSet"', () => { @@ -174,12 +211,18 @@ describe('FormComponentConfig', () => { ).toBeInTheDocument(); }); - it('should render default boolean values if defined', () => { + it('should render default boolean values if defined', async () => { + const user = userEvent.setup(); render({ props: { schema: DatepickerSchema, }, }); + const button = screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_show_many_settings'), + }); + expect(button).toBeInTheDocument(); + await user.click(button); const timeStampSwitch = screen.getByRole('checkbox', { name: textMock('ux_editor.component_properties.timeStamp'), }); @@ -196,6 +239,11 @@ describe('FormComponentConfig', () => { handleComponentUpdate: handleComponentUpdateMock, }, }); + const button = screen.getByRole('button', { + name: textMock('ux_editor.component_other_properties_show_many_settings'), + }); + expect(button).toBeInTheDocument(); + await user.click(button); const timeStampSwitch = screen.getByRole('checkbox', { name: textMock('ux_editor.component_properties.timeStamp'), }); diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx index 5ab3cf2935c..3d7f2c9a3d7 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Alert, Card, Heading, Paragraph } from '@digdir/designsystemet-react'; import type { FormComponent } from '../../types/FormComponent'; import { EditBooleanValue } from './editModal/EditBooleanValue'; @@ -16,6 +16,8 @@ import type { UpdateFormMutateOptions } from '../../containers/FormItemContext'; import { useComponentPropertyDescription } from '../../hooks/useComponentPropertyDescription'; import classes from './FormComponentConfig.module.css'; import { RedirectToLayoutSet } from './editModal/RedirectToLayoutSet'; +import { ChevronDownIcon, ChevronUpIcon } from '@studio/icons'; +import { StudioProperty } from '@studio/components'; export interface IEditFormComponentProps { editFormId: string; @@ -38,6 +40,7 @@ export const FormComponentConfig = ({ const t = useText(); const componentPropertyLabel = useComponentPropertyLabel(); const componentPropertyDescription = useComponentPropertyDescription(); + const [showOtherComponents, setShowOtherComponents] = useState(false); if (!schema?.properties) return null; @@ -95,6 +98,18 @@ export const FormComponentConfig = ({ ); }); + const defaultDisplayedBooleanKeys = booleanPropertyKeys.slice(0, 3); + const restOfBooleanKeys = booleanPropertyKeys.slice(3); + + const renderIcon = showOtherComponents ? ( + + ) : ( + + ); + const rendertext = showOtherComponents + ? t('ux_editor.component_other_properties_hide_many_settings') + : t('ux_editor.component_other_properties_show_many_settings'); + return ( <> {layoutSet && component['layoutSet'] && ( @@ -119,8 +134,17 @@ export const FormComponentConfig = ({ )} {/** Boolean fields, incl. expression type */} - {booleanPropertyKeys.map((propertyKey) => { - return ( + {defaultDisplayedBooleanKeys.map((propertyKey) => ( + + ))} + {showOtherComponents && + restOfBooleanKeys.map((propertyKey) => ( - ); - })} + ))} + {restOfBooleanKeys.length > 0 && ( + setShowOtherComponents((prev) => !prev)} + property={rendertext} + /> + )} {/** Custom logic for custom file endings */} {hasCustomFileEndings && ( diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Summary2Component.test.tsx b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Summary2Component.test.tsx index 4161539e532..edb9f118615 100644 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Summary2Component.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Summary2Component.test.tsx @@ -31,7 +31,7 @@ describe('Summary2ComponentTargetSelector', () => { expect(addNewOverrideButton()).toBeInTheDocument(); - expect(componentTargetSelect()).toBeInTheDocument(); + expect(disabledLayoutSetTargetSelect()).toBeInTheDocument(); }); it('should select the task id from the current layout when the task id of the target is not defined', async () => { @@ -52,7 +52,9 @@ describe('Summary2ComponentTargetSelector', () => { it('should allow selecting a task id', async () => { const user = userEvent.setup(); - render(); + render({ + component: { ...defaultProps.component, target: { type: 'component', id: component1IdMock } }, + }); await user.selectOptions(targetTaskIdSelect(), 'Task_2'); expect(defaultProps.handleComponentChange).toHaveBeenCalledWith( @@ -62,7 +64,9 @@ describe('Summary2ComponentTargetSelector', () => { it('should remove the task id from the target if the task id is the same as the current layout set', async () => { const user = userEvent.setup(); - render(); + render({ + component: { ...defaultProps.component, target: { type: 'component', id: component1IdMock } }, + }); await user.selectOptions(targetTaskIdSelect(), 'Task_1'); expect(defaultProps.handleComponentChange).toHaveBeenCalledWith( @@ -70,14 +74,11 @@ describe('Summary2ComponentTargetSelector', () => { ); }); - it('should allow selecting page target and defaults to same page', async () => { - const user = userEvent.setup(); + it('should defaults to page target and disabled target select', async () => { render(); - - await user.selectOptions(targetTypeSelect(), 'page'); - expect(defaultProps.handleComponentChange).toHaveBeenCalledWith( - expect.objectContaining({ target: { type: 'page', id: layout1NameMock } }), - ); + expect(targetTypeSelect()).toHaveValue('layoutSet'); + expect(disabledLayoutSetTargetSelect()).toBeDisabled(); + expect(disabledLayoutSetTargetSelect()).toHaveValue(layoutSet1NameMock); }); it('should allow selecting layoutSet target', async () => { @@ -92,8 +93,11 @@ describe('Summary2ComponentTargetSelector', () => { it('should allow selecting component target', async () => { const user = userEvent.setup(); - render(); + render({ + component: { ...defaultProps.component, target: { type: 'component', id: component1IdMock } }, + }); + await user.selectOptions(targetTypeSelect(), 'component'); const componentId = component1IdMock; await user.click(componentTargetSelect()); @@ -170,12 +174,17 @@ const targetTypeSelect = () => const componentTargetSelect = () => screen.getByRole('combobox', { - name: textMock('general.component'), + name: textMock('ux_editor.component_properties.target_unit_component'), }); const pageTargetSelect = () => screen.getByRole('combobox', { - name: textMock('general.page'), + name: textMock('ux_editor.component_properties.target_unit_page'), + }); + +const disabledLayoutSetTargetSelect = () => + screen.getByRole('textbox', { + name: textMock('ux_editor.component_properties.target_unit_layout_set'), }); const addNewOverrideButton = () => diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Target/Summary2Target.tsx b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Target/Summary2Target.tsx index 170360003b0..6a880a0e226 100644 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Target/Summary2Target.tsx +++ b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Summary2/Target/Summary2Target.tsx @@ -51,10 +51,28 @@ export const Summary2Target = ({ target, onChange }: Summary2TargetProps) => { const getComponentTitle = useComponentTitle(); const excludedComponents = [ - ComponentType.Summary2, - ComponentType.NavigationButtons, + ComponentType.ActionButton, + ComponentType.Alert, + ComponentType.AttachmentList, + ComponentType.Button, + ComponentType.ButtonGroup, + ComponentType.CustomButton, + ComponentType.Grid, + ComponentType.Header, + ComponentType.IFrame, + ComponentType.Image, + ComponentType.InstantiationButton, + ComponentType.InstanceInformation, + ComponentType.Link, ComponentType.NavigationBar, + ComponentType.NavigationButtons, + ComponentType.Panel, + ComponentType.Paragraph, + ComponentType.PrintButton, + ComponentType.Summary, + ComponentType.Summary2, ]; + const components = formLayoutsData ? Object.values(formLayoutsData).flatMap((layout) => getAllLayoutComponents(layout, excludedComponents), @@ -134,7 +152,7 @@ export const Summary2Target = ({ target, onChange }: Summary2TargetProps) => { {target.type === 'page' && ( { {target.type === 'component' && ( { diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditTab/OptionListEditor/OptionListEditor.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditTab/OptionListEditor/OptionListEditor.tsx index 2f581ec6407..767d9861a24 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditTab/OptionListEditor/OptionListEditor.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditTab/OptionListEditor/OptionListEditor.tsx @@ -90,7 +90,7 @@ function EditLibraryOptionListEditorModal({ const optionListHasChanged = (options: Option[]): boolean => JSON.stringify(options) !== JSON.stringify(localOptionList); - const handleOptionsChange = (options: Option[]) => { + const handleBlurAny = (options: Option[]) => { if (optionListHasChanged(options)) { updateOptionList({ optionListId: optionsId, optionsList: options }); setLocalOptionList(options); @@ -126,7 +126,7 @@ function EditLibraryOptionListEditorModal({ > @@ -147,7 +147,7 @@ function EditManualOptionListEditorModal({ const modalRef = useRef(null); const editorTexts = useOptionListEditorTexts(); - const handleOptionsChange = (options: Option[]) => { + const handleBlurAny = (options: Option[]) => { if (component.optionsId) { delete component.optionsId; } @@ -175,7 +175,7 @@ function EditManualOptionListEditorModal({ > diff --git a/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItemTitle/FormItemTitle.module.css b/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItemTitle/FormItemTitle.module.css index 2018f7f709c..9ee89c296fc 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItemTitle/FormItemTitle.module.css +++ b/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItemTitle/FormItemTitle.module.css @@ -5,6 +5,8 @@ .label { flex: 1; min-width: 0; + overflow: hidden; + text-overflow: ellipsis; } .root:hover .label { diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.ts b/frontend/packages/ux-editor/src/data/formItemConfig.ts index c22711e41e1..698cb06b6d4 100644 --- a/frontend/packages/ux-editor/src/data/formItemConfig.ts +++ b/frontend/packages/ux-editor/src/data/formItemConfig.ts @@ -472,7 +472,7 @@ export const formItemConfigs: FormItemConfigs = { itemType: LayoutItemType.Component, defaultProperties: { target: { - type: 'component', + type: 'layoutSet', id: '', taskId: '', }, diff --git a/frontend/packages/ux-editor/src/testing/componentMocks.ts b/frontend/packages/ux-editor/src/testing/componentMocks.ts index 886371032d6..aee0cba48de 100644 --- a/frontend/packages/ux-editor/src/testing/componentMocks.ts +++ b/frontend/packages/ux-editor/src/testing/componentMocks.ts @@ -180,7 +180,7 @@ const repeatingGroupContainer: FormContainer = { const summary2Component: FormComponent = { ...commonProps(ComponentType.Summary2), target: { - type: 'component', + type: 'layoutSet', }, };