Skip to content

Commit

Permalink
feat: add editing mode to task cards
Browse files Browse the repository at this point in the history
Allow editing of layoutset name and datamodelbinding on task/layout
cards

commit-id:4f2b585e
  • Loading branch information
Jondyr committed Mar 11, 2025
1 parent 2d71fa3 commit 632f893
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 11 deletions.
1 change: 1 addition & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1948,6 +1948,7 @@
"ux_editor.sync_error_settings_component_id": "En feil oppsto under synkronisering av komponentID i filen 'Settings.json'. Vennligst forsikre deg om at 'Settings.json' kun inneholder gyldig JSON-struktur og prøv igjen.",
"ux_editor.task_card.datamodel": "Datamodell: ",
"ux_editor.task_card.edit": "Endre oppgave",
"ux_editor.task_card.edit.confirm_data_model_change": "Er du sikker på at du vil bytte datamodell?\nFelt i skjemaet ditt kan få ugyldige knytninger hvis du gjør det. Du får feilmeldinger for felt som mister knytningen til nåværende datamodell.",
"ux_editor.task_card.ux_editor": "Utform",
"ux_editor.text_resource_binding_add_description": "Legg til beskrivelse",
"ux_editor.text_resource_binding_add_help": "Legg til hjelpetekst",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('taskCard', () => {
render();
await user.click(screen.getByTestId(studioIconCardPopoverTrigger));
expect(screen.getByRole('button', { name: /general.delete/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /ux_editor.task_card.edit/ })).toBeInTheDocument();
});

it('should display datatype id', async () => {
Expand Down Expand Up @@ -50,6 +51,37 @@ describe('taskCard', () => {
expect(queriesMock.deleteLayoutSet).toHaveBeenCalledTimes(1);
expect(queriesMock.deleteLayoutSet).toHaveBeenCalledWith(org, app, 'test');
});

it('should open edit mode when clicking edit button', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByTestId(studioIconCardPopoverTrigger));
await user.click(screen.getByRole('button', { name: /ux_editor.task_card.edit/ }));

expect(screen.getByRole('button', { name: /general.save/ })).toBeInTheDocument();
});

it('should exit save mode when closing', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByTestId(studioIconCardPopoverTrigger));
await user.click(screen.getByRole('button', { name: /ux_editor.task_card.edit/ }));
await user.click(screen.getByRole('button', { name: /general.close/ }));
expect(screen.queryByRole('button', { name: /general.save/ })).not.toBeInTheDocument();
});

it('should exit save mode when saving', async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByTestId(studioIconCardPopoverTrigger));
await user.click(screen.getByRole('button', { name: /ux_editor.task_card.edit/ }));
await user.type(
screen.getByRole('textbox', { name: /ux_editor.component_properties.layoutSet/ }),
'test',
);
await user.click(screen.getByRole('button', { name: /general.save/ }));
expect(screen.queryByRole('button', { name: /general.save/ })).not.toBeInTheDocument();
});
});

const render = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';
import React, { useState, type MouseEvent } from 'react';
import type { LayoutSetModel } from 'app-shared/types/api/dto/LayoutSetModel';
import { StudioIconCard } from '@studio/components/src/components/StudioIconCard/StudioIconCard';
import { PencilIcon } from '@studio/icons';
import { getLayoutSetTypeTranslationKey } from 'app-shared/utils/layoutSetsUtils';
import { useTranslation } from 'react-i18next';
import { StudioButton, StudioDeleteButton, StudioParagraph } from '@studio/components';
import { useLayoutSetIcon } from '../../hooks/useLayoutSetIcon';
import { useDeleteLayoutSetMutation } from 'app-development/hooks/mutations/useDeleteLayoutSetMutation';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { TaskCardEditing } from './TaskCardEditing';

type TaskCardProps = {
layoutSetModel: LayoutSetModel;
Expand All @@ -20,18 +22,36 @@ export const TaskCard = ({ layoutSetModel }: TaskCardProps) => {
const taskName = getLayoutSetTypeTranslationKey(layoutSetModel);
const taskIcon = useLayoutSetIcon(layoutSetModel);

const contextButtons = layoutSetModel.type === 'subform' && (
<StudioDeleteButton
variant='tertiary'
confirmMessage={t('ux_editor.delete.subform.confirm')}
onDelete={() => {
deleteLayoutSet({ layoutSetIdToUpdate: layoutSetModel.id });
}}
>
{t('general.delete')}
</StudioDeleteButton>
const [editing, setEditing] = useState(false);

const contextButtons = (
<>
<StudioButton
variant='tertiary'
onClick={(_: MouseEvent<HTMLButtonElement>) => {
setEditing(true);
}}
>
<PencilIcon /> {t('ux_editor.task_card.edit')}
</StudioButton>
{layoutSetModel.type === 'subform' && (
<StudioDeleteButton
variant='tertiary'
confirmMessage={t('ux_editor.delete.subform.confirm')}
onDelete={() => {
deleteLayoutSet({ layoutSetIdToUpdate: layoutSetModel.id });
}}
>
{t('general.delete')}
</StudioDeleteButton>
)}
</>
);

if (editing) {
return <TaskCardEditing layoutSetModel={layoutSetModel} onClose={() => setEditing(false)} />;
}

return (
<StudioIconCard
icon={taskIcon.icon}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.card {
width: 300px;
min-height: 250px;
}

.cardContent {
padding: var(--fds-spacing-2);
}

.btnGroup {
display: flex;
gap: var(--fds-spacing-2);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from 'react';
import type { LayoutSetModel } from 'app-shared/types/api/dto/LayoutSetModel';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../../testing/mocks';
import { TaskCardEditing, type TaskCardEditingProps } from './TaskCardEditing';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { QueryKey } from 'app-shared/types/QueryKey';
import { app, org } from '@studio/testing/testids';

const updateProcessDataTypesMutation = jest.fn().mockImplementation((params, options) => {
options.onSettled();
});
const updateLayoutSetIdMutation = jest.fn().mockImplementation((params, options) => {
options.onSettled();
});
jest.mock('app-development/hooks/mutations/useUpdateProcessDataTypesMutation', () => ({
useUpdateProcessDataTypesMutation: () => ({ mutate: updateProcessDataTypesMutation }),
}));
jest.mock('app-development/hooks/mutations/useUpdateLayoutSetIdMutation', () => ({
useUpdateLayoutSetIdMutation: () => ({ mutate: updateLayoutSetIdMutation }),
}));

const datamodels = ['datamodell123', 'unuseddatamodel'];
const subformLayoutSet: LayoutSetModel = {
id: 'test',
dataType: datamodels[0],
type: 'subform',
task: null,
};

const customReceiptLayoutSet: LayoutSetModel = {
id: 'test',
dataType: datamodels[0],
type: '',
task: { id: 'CustomReceipt', type: 'CustomReceipt' },
};

describe('taskCard', () => {
let confirmSpy: jest.SpyInstance;
beforeAll(() => {
confirmSpy = jest.spyOn(window, 'confirm');
confirmSpy.mockImplementation(jest.fn(() => true));
});

afterAll(() => {
confirmSpy.mockRestore();
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should render with disabled save button without changes', () => {
render();
expect(screen.getByRole('button', { name: /general.save/ })).toBeDisabled();
});

it('should show alert when changing data model', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render({ onClose, layoutSetModel: customReceiptLayoutSet });

await user.selectOptions(dataModelBindingCombobox(), datamodels[1]);
expect(confirmSpy.getMockImplementation()).toHaveBeenCalledTimes(0);
await user.click(screen.getByRole('button', { name: /general.save/ }));

expect(onClose).toHaveBeenCalledTimes(1);
expect(confirmSpy.getMockImplementation()).toHaveBeenCalledTimes(1);
});

it('should not show alert when not changing data model', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render({ onClose, layoutSetModel: customReceiptLayoutSet });

await user.type(layoutSetNameTextbox(), 'test');
await user.click(screen.getByRole('button', { name: /general.save/ }));
});

it('should call onClose when clicking close button', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render({ onClose });

await user.click(screen.getByRole('button', { name: /general.close/ }));

expect(onClose).toHaveBeenCalledTimes(1);
});

it('should call updateLayoutSetidMutation when layout set id is changed', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render({ onClose, layoutSetModel: subformLayoutSet });

await user.clear(layoutSetNameTextbox());
const newLayoutSetId = 'CoolLayoutName';
await user.type(layoutSetNameTextbox(), newLayoutSetId);
await user.click(screen.getByRole('button', { name: /general.save/ }));

expect(updateLayoutSetIdMutation).toHaveBeenCalledTimes(1);
expect(updateLayoutSetIdMutation).toHaveBeenCalledWith(
{
layoutSetIdToUpdate: subformLayoutSet.id,
newLayoutSetId: newLayoutSetId,
},
expect.anything(),
);
expect(onClose).toHaveBeenCalledTimes(1);
});

it('should call updateProcessDataTypesMutation when datamodel id is changed', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render({ onClose, layoutSetModel: customReceiptLayoutSet });

await user.selectOptions(dataModelBindingCombobox(), datamodels[1]);
await user.click(screen.getByRole('button', { name: /general.save/ }));

expect(updateProcessDataTypesMutation).toHaveBeenCalledTimes(1);
expect(updateProcessDataTypesMutation).toHaveBeenCalledWith(
{
connectedTaskId: customReceiptLayoutSet.task?.id,
newDataTypes: [datamodels[1]],
},
expect.anything(),
);
expect(onClose).toHaveBeenCalledTimes(1);
expect(confirmSpy.getMockImplementation()).toHaveBeenCalledTimes(1);
});

it('should show validation error when inputting invalid id', async () => {
const user = userEvent.setup();
const invalidLayoutName = ' test4! &';
render();

await user.type(layoutSetNameTextbox(), invalidLayoutName);

expect(layoutSetNameTextbox()).toBeInvalid();
expect(screen.getByRole('button', { name: /general.save/ })).toBeDisabled();
});
});

const render = (props?: Partial<TaskCardEditingProps>) => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.AppMetadataModelIds, org, app, true], datamodels);
renderWithProviders(
<TaskCardEditing layoutSetModel={subformLayoutSet} onClose={jest.fn()} {...props} />,
{ queryClient },
);
};

const layoutSetNameTextbox = (): Element =>
screen.getByRole('textbox', { name: /ux_editor.component_properties.layoutSet/ });

const dataModelBindingCombobox = (): Element =>
screen.getByRole('combobox', { name: /ux_editor.modal_properties_data_model_binding/ });
Loading

0 comments on commit 632f893

Please sign in to comment.