-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add editing mode to task cards
Allow editing of layoutset name and datamodelbinding on task/layout cards commit-id:4f2b585e
- Loading branch information
Showing
5 changed files
with
319 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
frontend/packages/ux-editor/src/components/TaskNavigation/TaskCardEditing.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
122 changes: 122 additions & 0 deletions
122
frontend/packages/ux-editor/src/components/TaskNavigation/TaskCardEditing.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import React from 'react'; | ||
import type { LayoutSetModel } from 'app-shared/types/api/dto/LayoutSetModel'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { act, screen, waitFor } from '@testing-library/react'; | ||
Check failure on line 4 in frontend/packages/ux-editor/src/components/TaskNavigation/TaskCardEditing.test.tsx
|
||
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) => { | ||
console.log('updateProcessDataTypesMutation'); | ||
console.log(params); | ||
console.log(options); | ||
options.onSettled(); | ||
}); | ||
const updateLayoutSetIdMutation = jest.fn().mockImplementation((params, options) => { | ||
console.log('updateLayoutSetIdMutation'); | ||
console.log(params); | ||
console.log(options); | ||
options.onSettled(); | ||
}); | ||
jest.mock('app-development/hooks/mutations/useUpdateProcessDataTypesMutation', () => ({ | ||
useUpdateProcessDataTypesMutation: () => ({ mutate: updateProcessDataTypesMutation }), | ||
})); | ||
jest.mock('app-development/hooks/mutations/useUpdateLayoutSetIdMutation', () => ({ | ||
useUpdateLayoutSetIdMutation: () => ({ mutate: updateLayoutSetIdMutation }), | ||
})); | ||
|
||
describe('taskCard', () => { | ||
it('should render with disabled save button without changes', () => { | ||
render(); | ||
expect(screen.getByRole('button', { name: /general.save/ })).toBeDisabled(); | ||
}); | ||
|
||
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 }); | ||
await user.clear( | ||
screen.getByRole('textbox', { name: /ux_editor.component_properties.layoutSet/ }), | ||
); | ||
const newLayoutSetId = 'CoolLayoutName'; | ||
await user.type( | ||
screen.getByRole('textbox', { name: /ux_editor.component_properties.layoutSet/ }), | ||
newLayoutSetId, | ||
); | ||
await user.click(screen.getByRole('button', { name: /general.save/ })); | ||
expect(updateLayoutSetIdMutation).toHaveBeenCalledTimes(1); | ||
expect(updateLayoutSetIdMutation).toHaveBeenCalledWith( | ||
{ | ||
layoutSetIdToUpdate: mockLayoutSet.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 }); | ||
await act(async () => { | ||
await user.selectOptions( | ||
screen.getByRole('combobox', { name: /ux_editor.modal_properties_data_model_binding/ }), | ||
'unuseddatamodel', | ||
); | ||
}); | ||
await user.click(screen.getByRole('button', { name: /general.save/ })); | ||
expect(updateProcessDataTypesMutation).toHaveBeenCalledTimes(1); | ||
Check failure on line 79 in frontend/packages/ux-editor/src/components/TaskNavigation/TaskCardEditing.test.tsx
|
||
expect(updateProcessDataTypesMutation).toHaveBeenCalledWith( | ||
{ | ||
layoutSetIdToUpdate: mockLayoutSet.id, | ||
newLayoutSetId: '', | ||
}, | ||
expect.anything(), | ||
); | ||
expect(onClose).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should show validation error when inputting invalid id', async () => { | ||
const user = userEvent.setup(); | ||
const onClose = jest.fn(); | ||
render({ onClose }); | ||
await user.type( | ||
screen.getByRole('textbox', { name: /ux_editor.component_properties.layoutSet/ }), | ||
' test4! &', | ||
); | ||
expect( | ||
screen.getByRole('textbox', { name: /ux_editor.component_properties.layoutSet/ }), | ||
).toBeInvalid(); | ||
expect(screen.getByRole('button', { name: /general.save/ })).toBeDisabled(); | ||
}); | ||
}); | ||
|
||
const mockLayoutSet: LayoutSetModel = { | ||
id: 'test', | ||
dataType: 'datamodell123', | ||
type: 'subform', | ||
task: { id: null, type: null }, | ||
}; | ||
|
||
const render = (props?: Partial<TaskCardEditingProps>) => { | ||
const queryClient = createQueryClientMock(); | ||
queryClient.setQueryData( | ||
[QueryKey.AppMetadataModelIds, org, app, true], | ||
['datamodell123', 'unuseddatamodel'], | ||
); | ||
renderWithProviders( | ||
<TaskCardEditing layoutSetModel={mockLayoutSet} onClose={jest.fn()} {...props} />, | ||
{ queryClient }, | ||
); | ||
}; |
121 changes: 121 additions & 0 deletions
121
frontend/packages/ux-editor/src/components/TaskNavigation/TaskCardEditing.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { | ||
StudioButton, | ||
StudioCard, | ||
StudioHeading, | ||
StudioNativeSelect, | ||
StudioSpinner, | ||
StudioTextfield, | ||
} from '@studio/components'; | ||
import { useUpdateLayoutSetIdMutation } from 'app-development/hooks/mutations/useUpdateLayoutSetIdMutation'; | ||
import { useUpdateProcessDataTypesMutation } from 'app-development/hooks/mutations/useUpdateProcessDataTypesMutation'; | ||
import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMetadataModelIdsQuery'; | ||
import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery'; | ||
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; | ||
import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName'; | ||
import type { LayoutSetModel } from 'app-shared/types/api/dto/LayoutSetModel'; | ||
import React, { type ChangeEvent } from 'react'; | ||
import classes from './TaskCardEditing.module.css'; | ||
import { getLayoutSetTypeTranslationKey } from 'app-shared/utils/layoutSetsUtils'; | ||
import { useTranslation } from 'react-i18next'; | ||
|
||
export type TaskCardEditingProps = { | ||
layoutSetModel: LayoutSetModel; | ||
onClose: () => void; | ||
}; | ||
|
||
export const TaskCardEditing = ({ layoutSetModel, onClose }: TaskCardEditingProps) => { | ||
const { org, app } = useStudioEnvironmentParams(); | ||
const { t } = useTranslation(); | ||
|
||
const { mutate: updateProcessDataType, isPending: updateProcessDataTypePending } = | ||
useUpdateProcessDataTypesMutation(org, app); | ||
const { mutate: mutateLayoutSetId, isPending: mutateLayoutSetIdPending } = | ||
useUpdateLayoutSetIdMutation(org, app); | ||
const { validateLayoutSetName } = useValidateLayoutSetName(); | ||
const { data: dataModels } = useAppMetadataModelIdsQuery(org, app, true); | ||
const { data: layoutSets } = useLayoutSetsQuery(org, app); | ||
|
||
const taskName = getLayoutSetTypeTranslationKey(layoutSetModel); | ||
const [id, setId] = React.useState(layoutSetModel.id); | ||
const [dataType, setDataType] = React.useState(layoutSetModel.dataType); | ||
|
||
const idChanged = id !== layoutSetModel.id; | ||
const dataTypeChanged = dataType !== layoutSetModel.dataType; | ||
const fieldChanged = idChanged || dataTypeChanged; | ||
|
||
const idValidationError = validateLayoutSetName(id, layoutSets, layoutSetModel.id); | ||
const pendingMutation = updateProcessDataTypePending || mutateLayoutSetIdPending; | ||
const disableSaveButton = !fieldChanged || Boolean(idValidationError) || pendingMutation; | ||
|
||
const onSettled = () => { | ||
if (!pendingMutation) onClose(); | ||
}; | ||
|
||
const saveChanges = () => { | ||
if (idChanged) { | ||
mutateLayoutSetId( | ||
{ layoutSetIdToUpdate: layoutSetModel.id, newLayoutSetId: id }, | ||
{ onSettled }, | ||
); | ||
} | ||
if (dataTypeChanged) { | ||
// TODO: implement subform datamodel change, maybe a common mutation | ||
updateProcessDataType( | ||
{ | ||
newDataTypes: [dataType], | ||
connectedTaskId: layoutSetModel.task?.id, | ||
}, | ||
{ onSettled }, | ||
); | ||
} | ||
}; | ||
|
||
return ( | ||
<StudioCard className={classes.card}> | ||
<StudioCard.Header> | ||
<StudioHeading size='xs'>{t(taskName)}</StudioHeading> | ||
</StudioCard.Header> | ||
<StudioCard.Content className={classes.cardContent}> | ||
<StudioTextfield | ||
label={t('ux_editor.component_properties.layoutSet')} | ||
value={id} | ||
error={idValidationError} | ||
onKeyUp={(event) => { | ||
if (event.key === 'Enter' && !disableSaveButton) saveChanges(); | ||
}} | ||
onChange={(event: ChangeEvent<HTMLInputElement>) => setId(event.target.value)} | ||
></StudioTextfield> | ||
<StudioNativeSelect | ||
label={t('ux_editor.modal_properties_data_model_binding')} | ||
size='sm' | ||
disabled={layoutSetModel.type === 'subform'} | ||
value={dataType} | ||
onChange={(event) => setDataType(event.target.value)} | ||
> | ||
{/* TODO: filter or validate this */} | ||
<option value=''>Ingen datamodell</option> | ||
{layoutSetModel.dataType && ( | ||
<option value={layoutSetModel.dataType}>{layoutSetModel.dataType}</option> | ||
)} | ||
{dataModels?.map((dataModel) => ( | ||
<option key={dataModel} value={dataModel}> | ||
{dataModel} | ||
</option> | ||
))} | ||
</StudioNativeSelect> | ||
<div className={classes.btnGroup}> | ||
<StudioButton | ||
disabled={disableSaveButton} | ||
onClick={() => saveChanges()} | ||
variant='primary' | ||
> | ||
{pendingMutation ? <StudioSpinner size='xs' spinnerTitle='' /> : t('general.save')} | ||
</StudioButton> | ||
<StudioButton disabled={pendingMutation} onClick={() => onClose()} variant='secondary'> | ||
{t('general.close')} | ||
</StudioButton> | ||
</div> | ||
</StudioCard.Content> | ||
</StudioCard> | ||
); | ||
}; |