Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implementing support for full config of subform component #13782

Merged
merged 9 commits into from
Oct 18, 2024
7 changes: 7 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.wrapper {
margin-top: var(--fds-spacing-4);
}

.headerWrapper {
display: flex;
justify-content: space-between;
}
Original file line number Diff line number Diff line change
@@ -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<ColumnElementProps> = {}) => {
const queryClient = createQueryClientMock();
return renderWithProviders(<ColumnElement {...defaultProps} {...props} />, {
...queriesMock,
queryClient,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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<HTMLInputElement>) => {
onEdit({ ...tableColumn, headerContent: event.target.value });
};

const handleEditQuery = (event: ChangeEvent<HTMLInputElement>) => {
onEdit({
...tableColumn,
cellContent: { ...tableColumn.cellContent, query: event.target.value },
});
};

const handleEditDefault = (event: ChangeEvent<HTMLInputElement>) => {
onEdit({
...tableColumn,
cellContent: { ...tableColumn.cellContent, default: event.target.value },
});
};
return (
<div className={classes.wrapper}>
<TableColumnHeader columnNumber={columnNumber} onDeleteColumn={onDeleteColumn} />
<TableColumnToggleableTextfield
label={t('ux_editor.properties_panel.subform_table_columns.header_content_label')}
value={tableColumn.headerContent}
onBlur={handleEditHeaderContent}
required={true}
/>
<TableColumnToggleableTextfield
label={t('ux_editor.properties_panel.subform_table_columns.cell_content_query_label')}
value={tableColumn.cellContent.query}
onBlur={handleEditQuery}
required={true}
/>
<TableColumnToggleableTextfield
label={t('ux_editor.properties_panel.subform_table_columns.cell_content_default_label')}
value={tableColumn.cellContent.default}
onBlur={handleEditDefault}
/>
</div>
);
};

type TableColumnHeaderProps = {
columnNumber: number;
onDeleteColumn: () => void;
};

const TableColumnHeader = ({
columnNumber,
onDeleteColumn,
}: TableColumnHeaderProps): ReactElement => {
const { t } = useTranslation();

return (
<div className={classes.headerWrapper}>
<StudioLabelAsParagraph size='sm'>
{t('ux_editor.properties_panel.subform_table_columns.column_header', { columnNumber })}
</StudioLabelAsParagraph>
<StudioButton
icon={<TrashFillIcon />}
title={t('ux_editor.properties_panel.subform_table_columns.delete_column', {
columnNumber,
})}
onClick={onDeleteColumn}
color='danger'
variant='secondary'
/>
</div>
);
};

type TableColumnToggleableTextfieldProps = {
label: string;
value: string;
onBlur: (event: ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
};

const TableColumnToggleableTextfield = ({
label,
value,
onBlur,
required = false,
}: TableColumnToggleableTextfieldProps): ReactElement => {
return (
<StudioToggleableTextfield
inputProps={{
icon: <KeyVerticalFillIcon />,
label,
value,
size: 'sm',
required,
onBlur,
}}
viewProps={{
children: (
<span>
<b>{label}:</b> {value}
</span>
),
variant: 'tertiary',
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ColumnElement } from './ColumnElement';
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading