From b8cbb01fe40bb02db21e0215b4d070405a06d2ae Mon Sep 17 00:00:00 2001 From: wrt95 Date: Mon, 14 Oct 2024 14:36:42 +0200 Subject: [PATCH 01/17] feat: Implementing support for full config of subform component --- frontend/language/src/nb.json | 7 + .../ColumnElement/ColumnElement.module.css | 8 + .../ColumnElement/ColumnElement.test.tsx | 145 ++++++++++++++++++ .../ColumnElement/ColumnElement.tsx | 128 ++++++++++++++++ .../ColumnElement/index.ts | 1 + .../EditSubFormTableColumns.module.css | 9 ++ .../EditSubFormTableColumns.test.tsx | 102 ++++++++++++ .../EditSubFormTableColumns.tsx | 65 ++++++++ .../EditSubFormTableColumns/index.ts | 1 + .../types/TableColumn.ts | 9 ++ .../editSubFormTableColumnsUtils.test.ts | 91 +++++++++++ .../utils/editSubFormTableColumnsUtils.ts | 20 +++ .../EditSubFormTableColumns/utils/index.ts | 1 + .../src/components/Properties/Text.test.tsx | 41 +++++ .../src/components/Properties/Text.tsx | 30 ++-- .../components/config/FormComponentConfig.tsx | 1 + .../ux-editor/src/testing/componentMocks.ts | 9 ++ 17 files changed, 652 insertions(+), 16 deletions(-) create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/index.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/types/TableColumn.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index b1ec7114244..4f4e722606a 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1661,6 +1661,13 @@ "ux_editor.properties_panel.options.codelist_switch_to_custom": "Bytt til egendefinert kodeliste", "ux_editor.properties_panel.options.codelist_switch_to_static": "Bytt til statisk kodeliste", "ux_editor.properties_panel.options.use_code_list_label": "Bruk kodeliste", + "ux_editor.properties_panel.subform_table_columns.add_column": "Legg til kolonne", + "ux_editor.properties_panel.subform_table_columns.cell_content_default_label": "Default", + "ux_editor.properties_panel.subform_table_columns.cell_content_query_label": "Query", + "ux_editor.properties_panel.subform_table_columns.column_header": "Kolonne {{columnNumber}}", + "ux_editor.properties_panel.subform_table_columns.delete_column": "Slett kolonne {{columnNumber}}", + "ux_editor.properties_panel.subform_table_columns.header_content_label": "Header Content", + "ux_editor.properties_panel.subform_table_columns.heading": "Valg for tabell", "ux_editor.properties_panel.texts.loading": "Laster inn tekster", "ux_editor.properties_panel.texts.no_properties": "Det er ingen tekster å konfigurere for denne komponenten.", "ux_editor.properties_panel.texts.sub_title_images": "Valg for bilde", diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.module.css b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.module.css new file mode 100644 index 00000000000..82a871787d8 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.module.css @@ -0,0 +1,8 @@ +.wrapper { + margin-top: var(--fds-spacing-4); +} + +.headerWrapper { + display: flex; + justify-content: space-between; +} diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.test.tsx new file mode 100644 index 00000000000..76f64c3f5b9 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { ColumnElement, type ColumnElementProps } from './ColumnElement'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { renderWithProviders } from 'dashboard/testing/mocks'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import userEvent from '@testing-library/user-event'; +import { type TableColumn } from '../types/TableColumn'; + +const headerContentMock: string = 'Header'; +const cellContentQueryMock: string = 'Query'; +const cellContentDefaultMock: string = 'Default'; +const columnNumberMock: number = 1; + +const mockTableColumn: TableColumn = { + headerContent: headerContentMock, + cellContent: { + query: cellContentQueryMock, + default: cellContentDefaultMock, + }, +}; + +const defaultProps: ColumnElementProps = { + tableColumn: mockTableColumn, + columnNumber: columnNumberMock, + onDeleteColumn: jest.fn(), + onEdit: jest.fn(), +}; + +describe('ColumnElement', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call onEdit with updated header content when header text field is blurred', async () => { + const onEditMock = jest.fn(); + + const user = userEvent.setup(); + renderColumnElement({ + onEdit: onEditMock, + }); + + const headerInputbutton = screen.getByRole('button', { + name: `${textMock('ux_editor.properties_panel.subform_table_columns.header_content_label')}: ${headerContentMock}`, + }); + await user.click(headerInputbutton); + + const headerInputfield = screen.getByLabelText( + textMock('ux_editor.properties_panel.subform_table_columns.header_content_label'), + ); + const newValue: string = 'a'; + await user.type(headerInputfield, newValue); + await user.tab(); + + expect(onEditMock).toHaveBeenCalledTimes(1); + expect(onEditMock).toHaveBeenCalledWith({ + ...mockTableColumn, + headerContent: `${headerContentMock}${newValue}`, + }); + }); + + it('should call onEdit with updated query content when query text field is blurred', async () => { + const onEditMock = jest.fn(); + + const user = userEvent.setup(); + renderColumnElement({ + onEdit: onEditMock, + }); + + const queryInputbutton = screen.getByRole('button', { + name: `${textMock('ux_editor.properties_panel.subform_table_columns.cell_content_query_label')}: ${cellContentQueryMock}`, + }); + await user.click(queryInputbutton); + + const queryInputfield = screen.getByLabelText( + textMock('ux_editor.properties_panel.subform_table_columns.cell_content_query_label'), + ); + const newValue: string = 'a'; + await user.type(queryInputfield, newValue); + await user.tab(); + + expect(onEditMock).toHaveBeenCalledTimes(1); + expect(onEditMock).toHaveBeenCalledWith({ + ...mockTableColumn, + cellContent: { ...mockTableColumn.cellContent, query: `${cellContentQueryMock}${newValue}` }, + }); + }); + + it('should call onEdit with updated default content when default text field is blurred', async () => { + const onEditMock = jest.fn(); + + const user = userEvent.setup(); + renderColumnElement({ + onEdit: onEditMock, + }); + + const defaultInputbutton = screen.getByRole('button', { + name: `${textMock('ux_editor.properties_panel.subform_table_columns.cell_content_default_label')}: ${cellContentDefaultMock}`, + }); + await user.click(defaultInputbutton); + + const defaultInputfield = screen.getByLabelText( + textMock('ux_editor.properties_panel.subform_table_columns.cell_content_default_label'), + ); + const newValue: string = 'a'; + await user.type(defaultInputfield, newValue); + await user.tab(); + + expect(onEditMock).toHaveBeenCalledTimes(1); + expect(onEditMock).toHaveBeenCalledWith({ + ...mockTableColumn, + cellContent: { + ...mockTableColumn.cellContent, + default: `${cellContentDefaultMock}${newValue}`, + }, + }); + }); + + it('should call onDeleteColumn when delete button is clicked', async () => { + const onDeleteColumnMock = jest.fn(); + + const user = userEvent.setup(); + renderColumnElement({ + onDeleteColumn: onDeleteColumnMock, + }); + + const deleteButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.subform_table_columns.delete_column', { + columnNumber: columnNumberMock, + }), + }); + await user.click(deleteButton); + + expect(onDeleteColumnMock).toHaveBeenCalledTimes(1); + }); +}); + +const renderColumnElement = (props: Partial = {}) => { + const queryClient = createQueryClientMock(); + return renderWithProviders(, { + ...queriesMock, + queryClient, + }); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx new file mode 100644 index 00000000000..cf5f4518832 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx @@ -0,0 +1,128 @@ +import React, { type ReactElement, type ChangeEvent } from 'react'; +import classes from './ColumnElement.module.css'; +import { type TableColumn } from '../types/TableColumn'; +import { useTranslation } from 'react-i18next'; +import { + StudioButton, + StudioLabelAsParagraph, + StudioToggleableTextfield, +} from '@studio/components'; +import { KeyVerticalFillIcon, TrashFillIcon } from '@studio/icons'; + +export type ColumnElementProps = { + tableColumn: TableColumn; + columnNumber: number; + onDeleteColumn: () => void; + onEdit: (tableColumn: TableColumn) => void; +}; + +export const ColumnElement = ({ + tableColumn, + columnNumber, + onDeleteColumn, + onEdit, +}: ColumnElementProps): ReactElement => { + const { t } = useTranslation(); + + const handleEditHeaderContent = (event: ChangeEvent) => { + onEdit({ ...tableColumn, headerContent: event.target.value }); + }; + + const handleEditQuery = (event: ChangeEvent) => { + onEdit({ + ...tableColumn, + cellContent: { ...tableColumn.cellContent, query: event.target.value }, + }); + }; + + const handleEditDefault = (event: ChangeEvent) => { + onEdit({ + ...tableColumn, + cellContent: { ...tableColumn.cellContent, default: event.target.value }, + }); + }; + return ( +
+ + + + +
+ ); +}; + +type TableColumnHeaderProps = { + columnNumber: number; + onDeleteColumn: () => void; +}; + +const TableColumnHeader = ({ + columnNumber, + onDeleteColumn, +}: TableColumnHeaderProps): ReactElement => { + const { t } = useTranslation(); + + return ( +
+ + {t('ux_editor.properties_panel.subform_table_columns.column_header', { columnNumber })} + + } + title={t('ux_editor.properties_panel.subform_table_columns.delete_column', { + columnNumber, + })} + onClick={onDeleteColumn} + color='danger' + variant='secondary' + /> +
+ ); +}; + +type TableColumnToggleableTextfieldProps = { + label: string; + value: string; + onBlur: (event: ChangeEvent) => void; + required?: boolean; +}; + +const TableColumnToggleableTextfield = ({ + label, + value, + onBlur, + required = false, +}: TableColumnToggleableTextfieldProps): ReactElement => { + return ( + , + label, + value, + size: 'sm', + required, + onBlur, + }} + viewProps={{ + children: ( + + {label}: {value} + + ), + variant: 'tertiary', + }} + /> + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/index.ts new file mode 100644 index 00000000000..9677694e4d2 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/index.ts @@ -0,0 +1 @@ +export { ColumnElement } from './ColumnElement'; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css new file mode 100644 index 00000000000..e869e81a93b --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css @@ -0,0 +1,9 @@ +.wrapper { + border-top: 1px solid var(--fds-semantic-border-divider-default); + margin-inline: var(--fds-spacing-2); + padding: var(--fds-spacing-3); +} + +.addColumnButton { + margin-top: var(--fds-spacing-3); +} diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx new file mode 100644 index 00000000000..53a8b7e3b4b --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { + EditSubFormTableColumns, + type EditSubFormTableColumnsProps, +} from './EditSubFormTableColumns'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { renderWithProviders } from 'dashboard/testing/mocks'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import userEvent from '@testing-library/user-event'; +import { ComponentType } from 'app-shared/types/ComponentType'; +import { componentMocks } from '@altinn/ux-editor/testing/componentMocks'; + +const subFormComponentMock = componentMocks[ComponentType.SubForm]; + +const defaultProps: EditSubFormTableColumnsProps = { + component: subFormComponentMock, + handleComponentChange: jest.fn(), +}; + +describe('EditSubFormTableColumns', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call handleComponentChange when a new column is added', async () => { + const handleComponentChangeMock = jest.fn(); + const user = userEvent.setup(); + + renderEditSubFormTableColumns({ + handleComponentChange: handleComponentChangeMock, + }); + + const addColumnButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.subform_table_columns.add_column'), + }); + + await user.click(addColumnButton); + + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + const updatedComponent = handleComponentChangeMock.mock.calls[0][0]; + expect(updatedComponent.tableColumns.length).toBe(2); + }); + + it('should call handleComponentChange when a column is edited', async () => { + const handleComponentChangeMock = jest.fn(); + const user = userEvent.setup(); + + renderEditSubFormTableColumns({ + handleComponentChange: handleComponentChangeMock, + }); + + const headerInputbutton = screen.getByRole('button', { + name: `${textMock('ux_editor.properties_panel.subform_table_columns.header_content_label')}: ${subFormComponentMock.tableColumns[0].headerContent}`, + }); + + await user.click(headerInputbutton); + + const headerInputfield = screen.getByLabelText( + textMock('ux_editor.properties_panel.subform_table_columns.header_content_label'), + ); + + const newValue = 'Updated Header'; + await user.clear(headerInputfield); + await user.type(headerInputfield, newValue); + await user.tab(); + + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + const updatedComponent = handleComponentChangeMock.mock.calls[0][0]; + expect(updatedComponent.tableColumns[0].headerContent).toBe(newValue); + }); + + it('should call handleComponentChange when a column is deleted', async () => { + const handleComponentChangeMock = jest.fn(); + const user = userEvent.setup(); + + renderEditSubFormTableColumns({ + handleComponentChange: handleComponentChangeMock, + }); + + const deleteButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.subform_table_columns.delete_column', { + columnNumber: 1, + }), + }); + + await user.click(deleteButton); + + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + const updatedComponent = handleComponentChangeMock.mock.calls[0][0]; + expect(updatedComponent.tableColumns.length).toBe(0); + }); +}); + +const renderEditSubFormTableColumns = (props: Partial = {}) => { + const queryClient = createQueryClientMock(); + return renderWithProviders(, { + ...queriesMock, + queryClient, + }); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx new file mode 100644 index 00000000000..1e6f6cd718c --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx @@ -0,0 +1,65 @@ +import React, { type ReactElement } from 'react'; +import classes from './EditSubFormTableColumns.module.css'; +import { StudioButton, StudioHeading } from '@studio/components'; +import { useTranslation } from 'react-i18next'; +import { type IGenericEditComponent } from '../../config/componentConfig'; +import { type ComponentType } from 'app-shared/types/ComponentType'; +import { type TableColumn } from './types/TableColumn'; +import { filterOutTableColumn, updateComponentWithSubform } from './utils'; +import { useUniqueKeys } from '@studio/hooks'; +import { ColumnElement } from './ColumnElement'; + +export type EditSubFormTableColumnsProps = IGenericEditComponent; + +export const EditSubFormTableColumns = ({ + component, + handleComponentChange, +}: EditSubFormTableColumnsProps): ReactElement => { + const { t } = useTranslation(); + + const tableColumns: TableColumn[] = component?.tableColumns ?? []; + const { getUniqueKey, addUniqueKey, removeUniqueKey } = useUniqueKeys({ + numberOfKeys: tableColumns.length, + }); + + const handleAddColumn = () => { + addUniqueKey(); + const updatedComponent = updateComponentWithSubform(component, [ + { headerContent: '', cellContent: { query: '', default: '' } }, + ]); + handleComponentChange(updatedComponent); + }; + + const deleteColumn = (tableColumnToRemove: TableColumn, index: number) => { + const updatedColumns: TableColumn[] = filterOutTableColumn(tableColumns, tableColumnToRemove); + removeUniqueKey(index); + handleComponentChange({ ...component, tableColumns: updatedColumns }); + }; + + const editColumn = (tableColumn: TableColumn, position: number) => { + const updatedColumns = [...tableColumns]; + updatedColumns[position] = tableColumn; + handleComponentChange({ ...component, tableColumns: updatedColumns }); + }; + + return ( +
+ + {t('ux_editor.properties_panel.subform_table_columns.heading')} + + {tableColumns.length > 0 && + tableColumns.map((tableColum: TableColumn, index: number) => ( + deleteColumn(tableColum, index)} + onEdit={(updatedTableColumn: TableColumn) => editColumn(updatedTableColumn, index)} + /> + ))} + + {t('ux_editor.properties_panel.subform_table_columns.add_column')} + +
+ ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts new file mode 100644 index 00000000000..4c9bc5c96da --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts @@ -0,0 +1 @@ +export { EditSubFormTableColumns } from './EditSubFormTableColumns'; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/types/TableColumn.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/types/TableColumn.ts new file mode 100644 index 00000000000..b12584e7e28 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/types/TableColumn.ts @@ -0,0 +1,9 @@ +type TableColumnCellContent = { + query: string; + default?: string; +}; + +export type TableColumn = { + headerContent: string; + cellContent: TableColumnCellContent; +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts new file mode 100644 index 00000000000..11ef2891b3e --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts @@ -0,0 +1,91 @@ +import { updateComponentWithSubform, filterOutTableColumn } from './editSubFormTableColumnsUtils'; +import { type FormItem } from '@altinn/ux-editor/types/FormItem'; +import { ComponentType } from 'app-shared/types/ComponentType'; +import { type TableColumn } from '../types/TableColumn'; +import { componentMocks } from '../../../../testing/componentMocks'; + +// Mock data for testing +const mockTableColumn1: TableColumn = { + headerContent: 'Header 1', + cellContent: { query: 'query 1', default: 'default 1' }, +}; +const mockTableColumn2: TableColumn = { + headerContent: 'Header 2', + cellContent: { query: 'query 2' }, +}; +const mockTableColumn3: TableColumn = { + headerContent: 'Header 3', + cellContent: { query: 'query 3', default: 'default 3' }, +}; + +const subFormComponentMock = componentMocks[ComponentType.SubForm]; + +describe('editSubFormTableColumnsUtils', () => { + describe('updateComponentWithSubform', () => { + it('should add table columns to the component', () => { + const tableColumnsToAdd = [mockTableColumn2, mockTableColumn3]; + + const updatedComponent = updateComponentWithSubform(subFormComponentMock, tableColumnsToAdd); + + expect(updatedComponent.tableColumns).toEqual([ + subFormComponentMock.tableColumns[0], + mockTableColumn2, + mockTableColumn3, + ]); + }); + + it('should handle case where the component has no initial tableColumns', () => { + const componentWithoutColumns: FormItem = { + ...subFormComponentMock, + tableColumns: undefined, + }; + + const tableColumnsToAdd = [mockTableColumn2]; + + const updatedComponent = updateComponentWithSubform( + componentWithoutColumns, + tableColumnsToAdd, + ); + + expect(updatedComponent.tableColumns).toEqual([mockTableColumn2]); + }); + + it('should return the same component if tableColumnsToAdd is an empty array', () => { + const updatedComponent = updateComponentWithSubform(subFormComponentMock, []); + + expect(updatedComponent.tableColumns).toEqual([subFormComponentMock.tableColumns[0]]); + }); + }); + + describe('filterOutTableColumn', () => { + it('should filter out the specified table column', () => { + const tableColumns = [mockTableColumn1, mockTableColumn2, mockTableColumn3]; + + const updatedTableColumns = filterOutTableColumn(tableColumns, mockTableColumn2); + + expect(updatedTableColumns).toEqual([mockTableColumn1, mockTableColumn3]); + }); + + it('should return the same array if tableColumnToRemove is not found', () => { + const tableColumns = [mockTableColumn1, mockTableColumn3]; + + const updatedTableColumns = filterOutTableColumn(tableColumns, mockTableColumn2); + + expect(updatedTableColumns).toEqual(tableColumns); + }); + + it('should return an empty array if the only column is removed', () => { + const tableColumns = [mockTableColumn1]; + + const updatedTableColumns = filterOutTableColumn(tableColumns, mockTableColumn1); + + expect(updatedTableColumns).toEqual([]); + }); + + it('should return the same array if it is empty', () => { + const updatedTableColumns = filterOutTableColumn([], mockTableColumn1); + + expect(updatedTableColumns).toEqual([]); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts new file mode 100644 index 00000000000..fe0f6144b1c --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts @@ -0,0 +1,20 @@ +import { type FormItem } from '@altinn/ux-editor/types/FormItem'; +import { type ComponentType } from 'app-shared/types/ComponentType'; +import { type TableColumn } from '../types/TableColumn'; + +export const updateComponentWithSubform = ( + component: FormItem, + tableColumnsToAdd: TableColumn[], +): FormItem => { + return { + ...component, + tableColumns: [...(component?.tableColumns ?? []), ...tableColumnsToAdd], + }; +}; + +export const filterOutTableColumn = ( + tableColumns: TableColumn[], + tableColumnToRemove: TableColumn, +): TableColumn[] => { + return tableColumns.filter((tableColumn: TableColumn) => tableColumn !== tableColumnToRemove); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts new file mode 100644 index 00000000000..25f7e5966eb --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts @@ -0,0 +1 @@ +export { updateComponentWithSubform, filterOutTableColumn } from './editSubFormTableColumnsUtils'; diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx index c1e4cfaab63..b282ed82f6c 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx @@ -296,6 +296,47 @@ describe('TextTab', () => { expect(formItemContextProviderMock.handleUpdate).toHaveBeenCalled(); }); }); + + it('should render subform tabel section if component is subform', () => { + render({ + props: { + ...props, + formItem: { + ...componentMocks[ComponentType.SubForm], + }, + }, + }); + const tabelHeading = screen.getByRole('heading', { + name: textMock('ux_editor.properties_panel.subform_table_columns.heading'), + level: 2, + }); + const addColumnButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.subform_table_columns.add_column'), + }); + expect(tabelHeading).toBeInTheDocument(); + expect(addColumnButton).toBeInTheDocument(); + }); + + it('should call handleUpdate when handleComponentChange is triggered from EditSubFormTableColumns', async () => { + const user = userEvent.setup(); + + render({ + props: { + ...props, + formItem: { + ...componentMocks[ComponentType.SubForm], + }, + }, + }); + const addColumnButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.subform_table_columns.add_column'), + }); + await user.click(addColumnButton); + + await waitFor(() => { + expect(formItemContextProviderMock.handleUpdate).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.tsx index 69139b61c08..b284b298970 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Text.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Text.tsx @@ -6,12 +6,14 @@ import { EditTextResourceBindings } from '../config/editModal/EditTextResourceBi import { useComponentSchemaQuery } from '../../hooks/queries/useComponentSchemaQuery'; import { StudioSpinner } from '@studio/components'; import { EditOptions } from '../config/editModal/EditOptions'; -import type { FormComponentBase } from '../../types/FormComponent'; +import type { FormComponent, FormComponentBase } from '../../types/FormComponent'; import type { ComponentType } from 'app-shared/types/ComponentType'; import type { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecificConfig'; import { useAppContext } from '../../hooks'; import { EditImage } from '../config/editModal/EditImage'; import classes from './Text.module.css'; +import { EditSubFormTableColumns } from './EditSubFormTableColumns'; +import { type FormContainer } from '@altinn/ux-editor/types/FormContainer'; export const Text = () => { const { formItemId: formId, formItem: form, handleUpdate, debounceSave } = useFormItemContext(); @@ -20,6 +22,11 @@ export const Text = () => { const { data: schema } = useComponentSchemaQuery(form.type); const { selectedFormLayoutName } = useAppContext(); + const handleComponentChange = async (updatedComponent: FormContainer | FormComponent) => { + handleUpdate(updatedComponent); + debounceSave(formId, updatedComponent); + }; + if (!schema) { return ( { {schema.properties.textResourceBindings?.properties && ( { - handleUpdate(updatedComponent); - debounceSave(formId, updatedComponent); - }} + handleComponentChange={handleComponentChange} textResourceBindingKeys={Object.keys(schema.properties.textResourceBindings.properties)} editFormId={formId} layoutName={selectedFormLayoutName} @@ -59,10 +63,7 @@ export const Text = () => { | (FormComponentBase & ComponentSpecificConfig) } - handleComponentChange={async (updatedComponent) => { - handleUpdate(updatedComponent); - debounceSave(formId, updatedComponent); - }} + handleComponentChange={handleComponentChange} editFormId={formId} layoutName={selectedFormLayoutName} renderOptions={{ @@ -75,15 +76,12 @@ export const Text = () => { {t('ux_editor.properties_panel.texts.sub_title_images')} - { - handleUpdate(updatedComponent); - debounceSave(formId, updatedComponent); - }} - /> + )} + {form.type === 'SubForm' && ( + + )} ); }; diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx index d187ec1e3cb..664b84cdd1f 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx @@ -53,6 +53,7 @@ export const FormComponentConfig = ({ 'children', 'dataTypeIds', 'target', + 'tableColumns', ]; const booleanPropertyKeys: string[] = getSupportedPropertyKeysForPropertyType( diff --git a/frontend/packages/ux-editor/src/testing/componentMocks.ts b/frontend/packages/ux-editor/src/testing/componentMocks.ts index 2160eb3f9cb..770a4d7fbd3 100644 --- a/frontend/packages/ux-editor/src/testing/componentMocks.ts +++ b/frontend/packages/ux-editor/src/testing/componentMocks.ts @@ -67,6 +67,15 @@ const textareaComponent: FormComponent = { }; const subFormComponent: FormComponent = { ...commonProps(ComponentType.SubForm), + tableColumns: [ + { + headerContent: 'header content', + cellContent: { + query: 'query', + default: 'default', + }, + }, + ], }; const fileUploadComponent: FormComponent = { ...commonProps(ComponentType.FileUpload), From 0ba8449cde3f8816f740e16774747123deaf4a91 Mon Sep 17 00:00:00 2001 From: wrt95 Date: Mon, 14 Oct 2024 14:49:03 +0200 Subject: [PATCH 02/17] adding required --- .../EditSubFormTableColumns/ColumnElement/ColumnElement.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx index cf5f4518832..738c6ddf938 100644 --- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx @@ -48,11 +48,13 @@ export const ColumnElement = ({ label={t('ux_editor.properties_panel.subform_table_columns.header_content_label')} value={tableColumn.headerContent} onBlur={handleEditHeaderContent} + required={true} /> Date: Mon, 14 Oct 2024 14:58:53 +0200 Subject: [PATCH 03/17] adding another test --- .../EditSubFormTableColumns.test.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx index 53a8b7e3b4b..59928aaf55f 100644 --- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx @@ -24,7 +24,27 @@ describe('EditSubFormTableColumns', () => { jest.clearAllMocks(); }); - it('should call handleComponentChange when a new column is added', async () => { + it('should call handleComponentChange when a new column is added when tableColumns initially are empty ', async () => { + const handleComponentChangeMock = jest.fn(); + const user = userEvent.setup(); + + renderEditSubFormTableColumns({ + component: { ...subFormComponentMock, tableColumns: undefined }, + handleComponentChange: handleComponentChangeMock, + }); + + const addColumnButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.subform_table_columns.add_column'), + }); + + await user.click(addColumnButton); + + expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); + const updatedComponent = handleComponentChangeMock.mock.calls[0][0]; + expect(updatedComponent.tableColumns.length).toBe(1); + }); + + it('should call handleComponentChange when a new column is added when tableColumns has a value', async () => { const handleComponentChangeMock = jest.fn(); const user = userEvent.setup(); From c2669dc88f42123ef32c743ad3258b5c43293247 Mon Sep 17 00:00:00 2001 From: wrt95 Date: Wed, 16 Oct 2024 13:09:27 +0200 Subject: [PATCH 04/17] fixing comment from PR --- .../packages/ux-editor/src/components/Properties/Text.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.tsx index b284b298970..523ad896a84 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Text.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Text.tsx @@ -7,7 +7,7 @@ import { useComponentSchemaQuery } from '../../hooks/queries/useComponentSchemaQ import { StudioSpinner } from '@studio/components'; import { EditOptions } from '../config/editModal/EditOptions'; import type { FormComponent, FormComponentBase } from '../../types/FormComponent'; -import type { ComponentType } from 'app-shared/types/ComponentType'; +import { ComponentType } from 'app-shared/types/ComponentType'; import type { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecificConfig'; import { useAppContext } from '../../hooks'; import { EditImage } from '../config/editModal/EditImage'; @@ -71,7 +71,7 @@ export const Text = () => { }} /> )} - {form.type === 'Image' && ( + {form.type === ComponentType.Image && ( <> {t('ux_editor.properties_panel.texts.sub_title_images')} @@ -79,7 +79,7 @@ export const Text = () => { )} - {form.type === 'SubForm' && ( + {form.type === ComponentType.SubForm && ( )} From b31dafffc12d1c2e49d5ca7743369c56d1d5ab99 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 17 Oct 2024 11:30:18 +0200 Subject: [PATCH 05/17] chore: Add StudioAlert component --- .../src/components/StudioAlert/StudioAlert.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx diff --git a/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx new file mode 100644 index 00000000000..18be0b7087f --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx @@ -0,0 +1,16 @@ +import React, { forwardRef } from 'react'; +import { Alert, type AlertProps } from '@digdir/designsystemet-react'; + +type StudioAlertProps = Omit & { + size?: 'sm' | 'md' | 'lg'; +}; + +const StudioAlert = forwardRef( + ({ size = 'sm', ...rest }, ref) => { + return ; + }, +); + +StudioAlert.displayName = 'StudioAlert'; + +export { StudioAlert }; From 451746efdf131dee45f114292e0be05e50627be6 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 17 Oct 2024 11:55:52 +0200 Subject: [PATCH 06/17] Remove size prop override --- .../src/components/StudioAlert/StudioAlert.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx index 18be0b7087f..8a57be4c53c 100644 --- a/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx +++ b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.tsx @@ -1,9 +1,7 @@ import React, { forwardRef } from 'react'; import { Alert, type AlertProps } from '@digdir/designsystemet-react'; -type StudioAlertProps = Omit & { - size?: 'sm' | 'md' | 'lg'; -}; +export type StudioAlertProps = AlertProps; const StudioAlert = forwardRef( ({ size = 'sm', ...rest }, ref) => { From 310ca6458c0e695e46a4d73619ee98a1b891c78f Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 17 Oct 2024 11:55:58 +0200 Subject: [PATCH 07/17] Add tests --- .../components/StudioAlert/StudioAlert.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx diff --git a/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx new file mode 100644 index 00000000000..37055784060 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import type { StudioAlertProps } from './StudioAlert'; +import { StudioAlert } from './StudioAlert'; + +describe('StudioAlert', () => { + it('should render the component', () => { + renderTestAlert({ severity: 'danger' }); + const studioAlert = screen.getByText('Feil'); + expect(studioAlert).toBeInTheDocument(); + }); +}); + +const renderTestAlert = (props: Partial = {}) => { + render(); +}; From 0ed30d851efa1e7d4b506c984ddcf814c70a35b9 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 17 Oct 2024 11:56:12 +0200 Subject: [PATCH 08/17] Fix file extension --- .../src/components/StudioAlert/{index.tsx => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/libs/studio-components/src/components/StudioAlert/{index.tsx => index.ts} (100%) diff --git a/frontend/libs/studio-components/src/components/StudioAlert/index.tsx b/frontend/libs/studio-components/src/components/StudioAlert/index.ts similarity index 100% rename from frontend/libs/studio-components/src/components/StudioAlert/index.tsx rename to frontend/libs/studio-components/src/components/StudioAlert/index.ts From ba4d06abc9193159accbb0d580796fe63bb43ab8 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 17 Oct 2024 13:23:09 +0200 Subject: [PATCH 09/17] Add SubFormMissingContentWarning Component --- .../SubFormMissingContentWarning.module.css | 4 ++ .../SubFormMissingContentWarning.test.tsx | 46 +++++++++++++++++++ .../SubFormMissingContentWarning.tsx | 45 ++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.module.css create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.tsx diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.module.css b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.module.css new file mode 100644 index 00000000000..36cb113d802 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.module.css @@ -0,0 +1,4 @@ +.redirectButton { + margin-top: var(--fds-spacing-3); + max-width: var(--fds-sizing-30); +} diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.test.tsx new file mode 100644 index 00000000000..244a63905d6 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.test.tsx @@ -0,0 +1,46 @@ +import { renderWithProviders } from '@altinn/ux-editor/testing/mocks'; +import { SubFormMissingContentWarning } from './SubFormMissingContentWarning'; +import React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { textMock } from '@studio/testing/mocks/i18nMock'; + +const setSelectedFormLayoutName = jest.fn(); +const setSelectedFormLayoutSetName = jest.fn(); +jest.mock('@altinn/ux-editor/hooks', () => ({ + useAppContext: () => ({ + setSelectedFormLayoutName, + setSelectedFormLayoutSetName, + }), +})); + +describe('SubFormMissingContentWarning', () => { + it('renders without crashing', () => { + renderWithProviders(); + expect( + screen.getByText( + textMock('ux_editor.component_properties.subform.layout_set_is_missing_content_heading'), + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + textMock('ux_editor.component_properties.subform.layout_set_is_missing_content_paragraph'), + ), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: textMock('top_menu.create') })).toBeInTheDocument(); + }); + it('calls redirect/state change functions on redirect button click', async () => { + const user = userEvent.setup(); + const subFormLayoutSetName = 'test'; + renderWithProviders( + , + ); + + await user.click(screen.getByRole('button', { name: textMock('top_menu.create') })); + + expect(setSelectedFormLayoutName).toHaveBeenCalledTimes(1); + expect(setSelectedFormLayoutName).toHaveBeenCalledWith(undefined); + expect(setSelectedFormLayoutSetName).toHaveBeenCalledTimes(1); + expect(setSelectedFormLayoutSetName).toHaveBeenCalledWith(subFormLayoutSetName); + }); +}); diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.tsx new file mode 100644 index 00000000000..486670c3320 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/SubFormMissingContentWarning/SubFormMissingContentWarning.tsx @@ -0,0 +1,45 @@ +import { PencilIcon } from '@studio/icons'; +import { StudioAlert, StudioButton, StudioHeading, StudioParagraph } from '@studio/components'; +import type { ReactElement } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAppContext } from '@altinn/ux-editor/hooks'; +import classes from './SubFormMissingContentWarning.module.css'; + +type SubFormMissingContentWarningProps = { + subFormLayoutSetName: string; +}; + +export const SubFormMissingContentWarning = ({ + subFormLayoutSetName, +}: SubFormMissingContentWarningProps): ReactElement => { + const { setSelectedFormLayoutName, setSelectedFormLayoutSetName } = useAppContext(); + const { t } = useTranslation(); + + const handleOnRedirectClick = (): void => { + setSelectedFormLayoutSetName(subFormLayoutSetName); + setSelectedFormLayoutName(undefined); + }; + + return ( + + + {t('ux_editor.component_properties.subform.layout_set_is_missing_content_heading')} + + + {t('ux_editor.component_properties.subform.layout_set_is_missing_content_paragraph')} + + } + iconPlacement='left' + disabled={!subFormLayoutSetName} + className={classes.redirectButton} + > + {t('top_menu.create')} + + + ); +}; From 7ae5b1e767a1930c823a3be621603befe710a624 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Mon, 21 Oct 2024 12:11:03 +0200 Subject: [PATCH 10/17] Change test to use img role and custom text --- .../StudioAlert/StudioAlert.test.tsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx index 37055784060..02428b2fea3 100644 --- a/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioAlert/StudioAlert.test.tsx @@ -1,16 +1,34 @@ -import React from 'react'; +import React, { type ForwardedRef } from 'react'; import { render, screen } from '@testing-library/react'; import type { StudioAlertProps } from './StudioAlert'; import { StudioAlert } from './StudioAlert'; +import { testRefForwarding } from '../../test-utils/testRefForwarding'; +import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending'; +import { testCustomAttributes } from '../../test-utils/testCustomAttributes'; describe('StudioAlert', () => { - it('should render the component', () => { - renderTestAlert({ severity: 'danger' }); - const studioAlert = screen.getByText('Feil'); + it('should render the component with icon', () => { + renderTestAlert({ iconTitle: 'test-icon-title' }); + const studioAlert = screen.getByRole('img', { name: 'test-icon-title' }); expect(studioAlert).toBeInTheDocument(); }); + + it('should support forwarding the ref', () => { + testRefForwarding((ref) => renderTestAlert({}, ref)); + }); + + it('should append classname to root', () => { + testRootClassNameAppending((className) => renderTestAlert({ className })); + }); + + it('should allow custom attributes', () => { + testCustomAttributes((customAttributes) => renderTestAlert({ ...customAttributes })); + }); }); -const renderTestAlert = (props: Partial = {}) => { - render(); +const renderTestAlert = ( + props: Partial = {}, + ref?: ForwardedRef, +) => { + return render(); }; From 5f355e362b61a1cb3a650a23164f5041bf9d988b Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Mon, 21 Oct 2024 12:55:16 +0200 Subject: [PATCH 11/17] Fix StudioAlert export --- .../libs/studio-components/src/components/StudioAlert/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/libs/studio-components/src/components/StudioAlert/index.ts b/frontend/libs/studio-components/src/components/StudioAlert/index.ts index 2946aec9c00..4558f3979fb 100644 --- a/frontend/libs/studio-components/src/components/StudioAlert/index.ts +++ b/frontend/libs/studio-components/src/components/StudioAlert/index.ts @@ -1 +1 @@ -export { Alert as StudioAlert } from '@digdir/designsystemet-react'; +export { StudioAlert } from './StudioAlert'; From fd3a6728f941436775e5dd457e6da79dbf453444 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 24 Oct 2024 10:00:13 +0200 Subject: [PATCH 12/17] feat(subform): texts to inform user invalid subform layout (#13834) Co-authored-by: JamalAlabdullah <90609090+JamalAlabdullah@users.noreply.github.com> --- frontend/language/src/nb.json | 2 + .../EditSubFormTableColumns.module.css | 4 ++ .../EditSubFormTableColumns.test.tsx | 46 +++++++++++--- .../EditSubFormTableColumns.tsx | 34 +++++++++-- .../hooks/useSubFormLayoutValidation.test.tsx | 61 +++++++++++++++++++ .../hooks/useSubFormLayoutValidation.ts | 13 ++++ .../src/components/Properties/Text.test.tsx | 25 -------- .../src/hooks/queries/useFormLayoutsQuery.ts | 1 + 8 files changed, 149 insertions(+), 37 deletions(-) create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.test.tsx create mode 100644 frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.ts diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 5f5c8167699..060407f4818 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1295,6 +1295,8 @@ "ux_editor.component_properties.subform.choose_layout_set": "Velg sidegruppe...", "ux_editor.component_properties.subform.choose_layout_set_label": "Velg sidegruppe å knytte til underskjema", "ux_editor.component_properties.subform.go_to_layout_set": "Gå til utforming av underskjemaet", + "ux_editor.component_properties.subform.layout_set_is_missing_content_heading": "Underskjemaet ditt mangler innhold.", + "ux_editor.component_properties.subform.layout_set_is_missing_content_paragraph": "Denne tabellen bruker underskjemaet for å hente feltene og tekstene som skal vises i tabellen. Velg Utform underskjemaet for å legge inn innhold.", "ux_editor.component_properties.subform.no_layout_sets_acting_as_subform": "Det finnes ingen sidegrupper i løsningen som kan brukes som et underskjema", "ux_editor.component_properties.subform.selected_layout_set_label": "Underskjema", "ux_editor.component_properties.subform.selected_layout_set_title": "Endre underskjemakobling til {{subform}}", diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css index e869e81a93b..b498d5a8497 100644 --- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css @@ -4,6 +4,10 @@ padding: var(--fds-spacing-3); } +.content-wrapper { + margin-top: var(--fds-spacing-4); +} + .addColumnButton { margin-top: var(--fds-spacing-3); } diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx index 59928aaf55f..de33f8357f2 100644 --- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx @@ -5,15 +5,20 @@ import { type EditSubFormTableColumnsProps, } from './EditSubFormTableColumns'; import { textMock } from '@studio/testing/mocks/i18nMock'; -import { renderWithProviders } from 'dashboard/testing/mocks'; import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import { queriesMock } from 'app-shared/mocks/queriesMock'; import userEvent from '@testing-library/user-event'; import { ComponentType } from 'app-shared/types/ComponentType'; import { componentMocks } from '@altinn/ux-editor/testing/componentMocks'; +import { renderWithProviders } from '@altinn/ux-editor/testing/mocks'; const subFormComponentMock = componentMocks[ComponentType.SubForm]; +const mockSubFormLayoutValidation = jest.fn(); +jest.mock('./hooks/useSubFormLayoutValidation', () => ({ + useSubFormLayoutValidation: () => mockSubFormLayoutValidation(), +})); + const defaultProps: EditSubFormTableColumnsProps = { component: subFormComponentMock, handleComponentChange: jest.fn(), @@ -29,8 +34,10 @@ describe('EditSubFormTableColumns', () => { const user = userEvent.setup(); renderEditSubFormTableColumns({ - component: { ...subFormComponentMock, tableColumns: undefined }, - handleComponentChange: handleComponentChangeMock, + props: { + component: { ...subFormComponentMock, tableColumns: undefined }, + handleComponentChange: handleComponentChangeMock, + }, }); const addColumnButton = screen.getByRole('button', { @@ -49,7 +56,7 @@ describe('EditSubFormTableColumns', () => { const user = userEvent.setup(); renderEditSubFormTableColumns({ - handleComponentChange: handleComponentChangeMock, + props: { handleComponentChange: handleComponentChangeMock }, }); const addColumnButton = screen.getByRole('button', { @@ -68,7 +75,7 @@ describe('EditSubFormTableColumns', () => { const user = userEvent.setup(); renderEditSubFormTableColumns({ - handleComponentChange: handleComponentChangeMock, + props: { handleComponentChange: handleComponentChangeMock }, }); const headerInputbutton = screen.getByRole('button', { @@ -96,7 +103,7 @@ describe('EditSubFormTableColumns', () => { const user = userEvent.setup(); renderEditSubFormTableColumns({ - handleComponentChange: handleComponentChangeMock, + props: { handleComponentChange: handleComponentChangeMock }, }); const deleteButton = screen.getByRole('button', { @@ -111,9 +118,34 @@ describe('EditSubFormTableColumns', () => { const updatedComponent = handleComponentChangeMock.mock.calls[0][0]; expect(updatedComponent.tableColumns.length).toBe(0); }); + + it('should show warning if subform validation is false', () => { + renderEditSubFormTableColumns({ isSubFormLayoutConfigured: false }); + expect( + screen.getByText( + textMock('ux_editor.component_properties.subform.layout_set_is_missing_content_heading'), + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + textMock('ux_editor.component_properties.subform.layout_set_is_missing_content_paragraph'), + ), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: textMock('top_menu.create') })).toBeInTheDocument(); + }); }); -const renderEditSubFormTableColumns = (props: Partial = {}) => { +type renderEditSubFormTableColumnsParameters = { + props?: Partial; + isSubFormLayoutConfigured?: boolean; +}; + +const renderEditSubFormTableColumns = ( + { props, isSubFormLayoutConfigured }: renderEditSubFormTableColumnsParameters = { + isSubFormLayoutConfigured: true, + }, +) => { + mockSubFormLayoutValidation.mockReturnValue(isSubFormLayoutConfigured); const queryClient = createQueryClientMock(); return renderWithProviders(, { ...queriesMock, diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx index 1e6f6cd718c..33d6a05164f 100644 --- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx @@ -1,4 +1,4 @@ -import React, { type ReactElement } from 'react'; +import React, { type ReactElement, type ReactNode } from 'react'; import classes from './EditSubFormTableColumns.module.css'; import { StudioButton, StudioHeading } from '@studio/components'; import { useTranslation } from 'react-i18next'; @@ -8,6 +8,8 @@ import { type TableColumn } from './types/TableColumn'; import { filterOutTableColumn, updateComponentWithSubform } from './utils'; import { useUniqueKeys } from '@studio/hooks'; import { ColumnElement } from './ColumnElement'; +import { useSubFormLayoutValidation } from './hooks/useSubFormLayoutValidation'; +import { SubFormMissingContentWarning } from './SubFormMissingContentWarning/SubFormMissingContentWarning'; export type EditSubFormTableColumnsProps = IGenericEditComponent; @@ -16,6 +18,7 @@ export const EditSubFormTableColumns = ({ handleComponentChange, }: EditSubFormTableColumnsProps): ReactElement => { const { t } = useTranslation(); + var subFormLayoutIsConfigured = useSubFormLayoutValidation(component.layoutSet); const tableColumns: TableColumn[] = component?.tableColumns ?? []; const { getUniqueKey, addUniqueKey, removeUniqueKey } = useUniqueKeys({ @@ -42,11 +45,16 @@ export const EditSubFormTableColumns = ({ handleComponentChange({ ...component, tableColumns: updatedColumns }); }; + if (subFormLayoutIsConfigured === false) { + return ( + + + + ); + } + return ( -
- - {t('ux_editor.properties_panel.subform_table_columns.heading')} - + {tableColumns.length > 0 && tableColumns.map((tableColum: TableColumn, index: number) => ( {t('ux_editor.properties_panel.subform_table_columns.add_column')} + + ); +}; + +type EditSubFormTableColumnsWrapperProps = { + children: ReactNode; +}; + +const EditSubFormTableColumnsWrapper = ({ children }: EditSubFormTableColumnsWrapperProps) => { + const { t } = useTranslation(); + return ( +
+ + {t('ux_editor.properties_panel.subform_table_columns.heading')} + +
{children}
); }; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.test.tsx new file mode 100644 index 00000000000..7536810cc83 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.test.tsx @@ -0,0 +1,61 @@ +import { useSubFormLayoutValidation } from './useSubFormLayoutValidation'; +import { renderHookWithProviders } from '@altinn/ux-editor/testing/mocks'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { app, org } from '@studio/testing/testids'; +import type { IFormLayouts } from '@altinn/ux-editor/types/global'; +import { ComponentType } from 'app-shared/types/ComponentType'; + +const emptyLayout: IFormLayouts = { + page1: { + order: { + section1: ['component2'], + }, + components: {}, + containers: {}, + customRootProperties: {}, + customDataProperties: {}, + }, +}; + +const nonEmptyLayout: IFormLayouts = { + ...emptyLayout, + page1: { + ...emptyLayout.page1, + components: { + component2: { + type: ComponentType.Input, + id: 'component2', + itemType: 'COMPONENT', + dataModelBindings: { simpleBinding: 'simpleBinding' }, + }, + }, + }, +}; + +describe('useSubFormLayoutValidation', () => { + it('should return true if form layout has components', () => { + const { result } = renderHook({ + layout: nonEmptyLayout, + }); + expect(result.current).toBe(true); + }); + it('should return false if form layout has no components', () => { + const { result } = renderHook({ + layout: emptyLayout, + }); + expect(result.current).toBe(false); + }); +}); + +type renderHookArgs = { + layout: IFormLayouts; +}; + +const renderHook = ({ layout }: renderHookArgs) => { + const queryClient = createQueryClientMock(); + queryClient.setQueryData([QueryKey.FormLayouts, org, app, ''], layout); + return renderHookWithProviders(() => useSubFormLayoutValidation(''), { + queryClient: queryClient, + }); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.ts new file mode 100644 index 00000000000..8532db7f046 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/useSubFormLayoutValidation.ts @@ -0,0 +1,13 @@ +import { useFormLayoutsQuery } from '@altinn/ux-editor/hooks/queries/useFormLayoutsQuery'; +import { getAllLayoutComponents } from '@altinn/ux-editor/utils/formLayoutUtils'; +import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; + +export const useSubFormLayoutValidation = (subFormLayoutSetName: string): boolean => { + const { org, app } = useStudioEnvironmentParams(); + const { data: formLayout } = useFormLayoutsQuery(org, app, subFormLayoutSetName); + + if (formLayout) { + return Object.values(formLayout).some((value) => getAllLayoutComponents(value).length > 0); + } + return false; +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx index b282ed82f6c..462ec978e21 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx @@ -310,32 +310,7 @@ describe('TextTab', () => { name: textMock('ux_editor.properties_panel.subform_table_columns.heading'), level: 2, }); - const addColumnButton = screen.getByRole('button', { - name: textMock('ux_editor.properties_panel.subform_table_columns.add_column'), - }); expect(tabelHeading).toBeInTheDocument(); - expect(addColumnButton).toBeInTheDocument(); - }); - - it('should call handleUpdate when handleComponentChange is triggered from EditSubFormTableColumns', async () => { - const user = userEvent.setup(); - - render({ - props: { - ...props, - formItem: { - ...componentMocks[ComponentType.SubForm], - }, - }, - }); - const addColumnButton = screen.getByRole('button', { - name: textMock('ux_editor.properties_panel.subform_table_columns.add_column'), - }); - await user.click(addColumnButton); - - await waitFor(() => { - expect(formItemContextProviderMock.handleUpdate).toHaveBeenCalledTimes(1); - }); }); }); }); diff --git a/frontend/packages/ux-editor/src/hooks/queries/useFormLayoutsQuery.ts b/frontend/packages/ux-editor/src/hooks/queries/useFormLayoutsQuery.ts index ced75496baf..4f8a0e5f9f2 100644 --- a/frontend/packages/ux-editor/src/hooks/queries/useFormLayoutsQuery.ts +++ b/frontend/packages/ux-editor/src/hooks/queries/useFormLayoutsQuery.ts @@ -17,6 +17,7 @@ export const useFormLayoutsQuery = ( getFormLayouts(org, app, layoutSetName).then((formLayouts) => { return convertExternalLayoutsToInternalFormat(formLayouts); }), + enabled: Boolean(layoutSetName), staleTime: Infinity, }); }; From b2bf825c9a6ad3384be50de4aa7b56eda174d888 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 30 Oct 2024 09:41:49 +0100 Subject: [PATCH 13/17] Fix Subform naming --- .../ux-editor/src/AppContext.test.tsx | 5 ++++- .../packages/ux-editor/src/AppContext.tsx | 2 +- .../Elements/LayoutSetsContainer.test.tsx | 2 +- .../Elements/LayoutSetsContainer.tsx | 2 +- .../SubformMissingContentWarning.module.css} | 0 .../SubformMissingContentWarning.test.tsx} | 12 +++++----- .../SubformMissingContentWarning.tsx} | 16 +++++++------- ...sx => useSubformLayoutValidation.test.tsx} | 6 ++--- ...ation.ts => useSubformLayoutValidation.ts} | 4 ++-- .../EditSubformTableColumns.test.tsx | 16 +++++++------- .../EditSubformTableColumns.tsx | 22 +++++++++---------- .../CreateNewSubformLayoutSet.test.tsx | 12 +++++----- .../CreateNewSubformLayoutSet.tsx | 20 ++++++++--------- .../EditLayoutSet/EditLayoutSet.tsx | 6 ++--- .../EditLayoutSetForSubform.test.tsx | 6 ++--- .../EditLayoutSetForSubform.tsx | 6 ++--- .../RedirectToLayoutSet.test.tsx | 2 +- .../RedirectToLayoutSet.tsx | 3 ++- .../ux-editor/src/testing/appContextMock.ts | 2 +- 19 files changed, 74 insertions(+), 70 deletions(-) rename frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/{SubFormMissingContentWarning/SubFormMissingContentWarning.module.css => SubformMissingContentWarning/SubformMissingContentWarning.module.css} (100%) rename frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/{SubFormMissingContentWarning/SubFormMissingContentWarning.test.tsx => SubformMissingContentWarning/SubformMissingContentWarning.test.tsx} (83%) rename frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/{SubFormMissingContentWarning/SubFormMissingContentWarning.tsx => SubformMissingContentWarning/SubformMissingContentWarning.tsx} (75%) rename frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/{useSubFormLayoutValidation.test.tsx => useSubformLayoutValidation.test.tsx} (89%) rename frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/hooks/{useSubFormLayoutValidation.ts => useSubformLayoutValidation.ts} (86%) diff --git a/frontend/packages/ux-editor/src/AppContext.test.tsx b/frontend/packages/ux-editor/src/AppContext.test.tsx index 10f7fc5ffa9..e5b47eecc31 100644 --- a/frontend/packages/ux-editor/src/AppContext.test.tsx +++ b/frontend/packages/ux-editor/src/AppContext.test.tsx @@ -93,7 +93,10 @@ describe('AppContext', () => { it('sets selectedFormLayoutSetName correctly', async () => { renderAppContext( - ({ selectedFormLayoutSetName, setSelectedFormLayoutSetName }: AppContextProps) => ( + ({ + selectedFormLayoutSetName, + setSelectedformLayoutSetName: setSelectedFormLayoutSetName, + }: AppContextProps) => ( <>