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: add codeListEditor in config options #13953

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion frontend/dashboard/components/RepoList/RepoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const RepoList = ({
},
{
accessor: 'description',
heading: t('dashboard.description'),
heading: t('general.description'),
sortable: true,
},
{
Expand Down
12 changes: 10 additions & 2 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@
"dashboard.created_by": "Opprettet av",
"dashboard.creating_your_service": "Oppretter appen din",
"dashboard.data_models": "Datamodeller",
"dashboard.description": "Beskrivelse",
"dashboard.edit_app": "Endre {{appName}} i Studio",
"dashboard.error_getting_organization_data.message": "Det oppsto en feil da vi skulle hente de organisasjonene som trengs for å kjøre appen.",
"dashboard.error_getting_organization_data.title": "Kunne ikke laste inn organisasjoner",
Expand Down Expand Up @@ -260,6 +259,7 @@
"general.date_time_format": "{{date}} kl. {{time}}",
"general.delete": "Slett",
"general.delete_item": "Slett {{item}}",
"general.description": "Beskrivelse",
"general.edit": "Endre",
"general.empty_string": "Tom tekst",
"general.error_message": "Det har oppstått en feil. Hvis problemet fortsetter, <a>ta kontakt med oss</a>.",
Expand All @@ -275,6 +275,7 @@
"general.loading": "Laster...",
"general.next": "Neste",
"general.no_options": "Ingen alternativer tilgjengelige",
"general.option": "Alternativ",
"general.options": "Alternativer",
"general.page": "Side",
"general.page_error_message": "Vi vet ikke helt hva, men <a>ta kontakt med oss</a>, så graver vi i det sammen.",
Expand Down Expand Up @@ -1456,8 +1457,15 @@
"ux_editor.modal_header_type_helper": "Velg titteltype",
"ux_editor.modal_new_option": "Legg til alternativ",
"ux_editor.modal_properties_add_radio_button_options": "Hvordan vil du legge til radioknapper?",
"ux_editor.modal_properties_code_list_delete_item": "Slett alternativ {{number}}",
"ux_editor.modal_properties_code_list_empty": "Kodelisten er tom.",
"ux_editor.modal_properties_code_list_helper": "Velg kodeliste",
"ux_editor.modal_properties_code_list_id": "Kodeliste-ID",
"ux_editor.modal_properties_code_list_item_description": "Beskrivelse for alternativ {{number}}",
"ux_editor.modal_properties_code_list_item_help_text": "Hjelpetekst for alternativ {{number}}",
"ux_editor.modal_properties_code_list_item_label": "Ledetekst for alternativ {{number}}",
"ux_editor.modal_properties_code_list_item_value": "Verdi for alternativ {{number}}",
"ux_editor.modal_properties_code_list_open_editor": "Åpne redigeringsverktøy",
"ux_editor.modal_properties_code_list_read_more": "<0 href=\"{{optionsDocs}}\" >Les mer om kodelister</0>",
"ux_editor.modal_properties_code_list_read_more_dynamic": "<0 href=\"{{optionsDocs}}\" >Les mer om dynamiske kodelister</0>",
"ux_editor.modal_properties_code_list_read_more_static": "<0 href=\"{{optionsDocs}}\" >Les mer om statiske kodelister</0>",
Expand Down Expand Up @@ -1612,7 +1620,7 @@
"ux_editor.options.codelist_create_info.step4": "Skriv inn kodelisten i tekstfeltet midt på siden. Kodelisten må være i JSON-format.",
"ux_editor.options.codelist_create_info.step5": "Velg \"Commit endringer\".",
"ux_editor.options.codelist_create_info.step6": "Du er nå ferdig i Gitea for denne gang. Gå tilbake til Altinn Studio-fanen, eller klikk på Altinn-logoen øverst til venstre i Gitea for å komme tilbake til Altinn Studio.",
"ux_editor.options.codelist_only": "Denne komponenten støtter kun oppsett med kodelister.",
"ux_editor.options.codelist_only": "Denne komponenten støtter kun oppsett med predefinerte kodelister.",
"ux_editor.options.codelist_referenceId.description": "Her kan du legge til en referanse-ID til en dynamisk kodeliste som er satt opp i koden.",
"ux_editor.options.codelist_referenceId.description_details": "Du bruker dynamiske kodelister for å tilpasse alternativer for brukerne. Det kan for eksempel være tilpasninger ut fra geografisk plassering, eller valg brukeren gjør tidligere i skjemaet.",
"ux_editor.options.codelist_upload_info.heading": "Steg for å laste opp kodelister manuelt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function StudioCodeListEditorRow({
<StudioInputTable.Row>
<TextfieldCell
label={texts.itemValue(number)}
value={item.value}
value={item.value as string}
onChange={handleValueChange}
/>
<TextfieldCell
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type CodeListItem = {
export type CodeListItem<T extends string | boolean | number = string | boolean | number> = {
description?: string;
helpText?: string;
label: string;
value: string;
value: T;
};
7 changes: 5 additions & 2 deletions frontend/packages/shared/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
altinn2DelegationsMigrationPath,
imagePath,
addImagePath,
optionListPath,
optionListUploadPath,
optionListUpdatePath,
} from 'app-shared/api/paths';
import type { AddLanguagePayload } from 'app-shared/types/api/AddLanguagePayload';
import type { AddRepoParams } from 'app-shared/types/api';
Expand All @@ -65,6 +66,7 @@ import type { PipelineDeployment } from 'app-shared/types/api/PipelineDeployment
import type { AddLayoutSetResponse } from 'app-shared/types/api/AddLayoutSetResponse';
import type { DataTypesChange } from 'app-shared/types/api/DataTypesChange';
import type { FormLayoutRequest } from 'app-shared/types/api/FormLayoutRequest';
import type { Option } from 'app-shared/types/Option';

const headers = {
Accept: 'application/json',
Expand Down Expand Up @@ -114,7 +116,8 @@ export const updateAppPolicy = (org: string, app: string, payload: Policy) => pu
export const updateAppMetadata = (org: string, app: string, payload: ApplicationMetadata) => put(appMetadataPath(org, app), payload);
export const updateAppConfig = (org: string, app: string, payload: AppConfig) => post(serviceConfigPath(org, app), payload);
export const uploadDataModel = (org: string, app: string, form: FormData) => post<void, FormData>(dataModelsUploadPath(org, app), form, { headers: { 'Content-Type': 'multipart/form-data' } });
export const uploadOptionList = (org: string, app: string, payload: FormData) => post<void, FormData>(optionListPath(org, app), payload, { headers: { 'Content-Type': 'multipart/form-data' } });
export const uploadOptionList = (org: string, app: string, payload: FormData) => post<void, FormData>(optionListUploadPath(org, app), payload, { headers: { 'Content-Type': 'multipart/form-data' } });
export const updateOptionList = (org: string, app: string, optionsListId: string, payload: Option[]) => put<void, Option[]>(optionListUpdatePath(org, app, optionsListId), payload);
export const upsertTextResources = (org: string, app: string, language: string, payload: ITextResourcesObjectFormat) => put<ITextResourcesObjectFormat>(textResourcesPath(org, app, language), payload);

// Resourceadm
Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export const dataModelAddXsdFromRepoPath = (org, app, filePath) => `${basePath}/
export const ruleHandlerPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/rule-handler?${s({ layoutSetName })}`; // Get, Post
export const widgetSettingsPath = (org, app) => `${basePath}/${org}/${app}/app-development/widget-settings`; // Get
export const optionListsPath = (org, app) => `${basePath}/${org}/${app}/options/option-lists`; // Get
export const optionListPath = (org, app) => `${basePath}/${org}/${app}/options/upload/`; // Post
export const optionListIdsPath = (org, app) => `${basePath}/${org}/${app}/app-development/option-list-ids`; // Get
export const optionListUpdatePath = (org, app, optionsListId) => `${basePath}/${org}/${app}/options/${optionsListId}`; // Put
export const optionListUploadPath = (org, app) => `${basePath}/${org}/${app}/options/upload`; // Post
export const ruleConfigPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/rule-config?${s({ layoutSetName })}`; // Get, Post
export const appMetadataModelIdsPath = (org, app, onlyUnReferenced) => `${basePath}/${org}/${app}/app-development/model-ids?${s({ onlyUnReferenced })}`; // Get
export const dataModelMetadataPath = (org, app, layoutSetName, dataModelName) => `${basePath}/${org}/${app}/app-development/model-metadata?${s({ layoutSetName })}&${s({ dataModelName })}`; // Get
Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import type { FormLayoutsResponseV3 } from 'app-shared/types/api/FormLayoutsResp
import type { Policy } from 'app-shared/types/Policy';
import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse';
import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse';
import type { Option } from 'app-shared/types/Option';

export const getAppMetadataModelIds = (org: string, app: string, onlyUnReferenced: boolean) => get<string[]>(appMetadataModelIdsPath(org, app, onlyUnReferenced));
export const getAppReleases = (owner: string, app: string) => get<AppReleasesResponse>(releasesPath(owner, app, 'Descending'));
Expand All @@ -102,7 +103,7 @@ export const getImageFileNames = (owner: string, app: string) => get<string[]>(g
export const getInstanceIdForPreview = (owner: string, app: string) => get<string>(instanceIdForPreviewPath(owner, app));
export const getLayoutNames = (owner: string, app: string) => get<string[]>(layoutNamesPath(owner, app));
export const getLayoutSets = (owner: string, app: string) => get<LayoutSets>(layoutSetsPath(owner, app));
export const getOptionLists = (owner: string, app: string) => get<string[]>(optionListsPath(owner, app));
export const getOptionLists = (owner: string, app: string) => get<Map<string, Option[]>>(optionListsPath(owner, app));
export const getOptionListIds = (owner: string, app: string) => get<string[]>(optionListIdsPath(owner, app));
export const getOrgList = () => get<OrgList>(orgListUrl());
export const getOrganizations = () => get<Organization[]>(orgsListPath());
Expand Down
6 changes: 5 additions & 1 deletion frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import type { FormLayoutsResponseV3 } from 'app-shared/types/api/FormLayoutsResp
import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse';
import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse';
import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse';
import type { Option } from 'app-shared/types/Option';

export const queriesMock: ServicesContextProps = {
// Queries
Expand Down Expand Up @@ -102,7 +103,9 @@ export const queriesMock: ServicesContextProps = {
getLayoutNames: jest.fn().mockImplementation(() => Promise.resolve<string[]>([])),
getLayoutSets: jest.fn().mockImplementation(() => Promise.resolve<LayoutSets>(layoutSets)),
getOptionListIds: jest.fn().mockImplementation(() => Promise.resolve<string[]>([])),
getOptionLists: jest.fn().mockImplementation(() => Promise.resolve<string[]>([])),
getOptionLists: jest
.fn()
.mockImplementation(() => Promise.resolve<Map<string, Option[]>>(new Map<string, Option[]>())),
getOrgList: jest.fn().mockImplementation(() => Promise.resolve<OrgList>(orgList)),
getOrganizations: jest.fn().mockImplementation(() => Promise.resolve<Organization[]>([])),
getRepoMetadata: jest.fn().mockImplementation(() => Promise.resolve<Repository>(repository)),
Expand Down Expand Up @@ -211,6 +214,7 @@ export const queriesMock: ServicesContextProps = {
updateAppPolicy: jest.fn().mockImplementation(() => Promise.resolve()),
updateAppMetadata: jest.fn().mockImplementation(() => Promise.resolve()),
updateAppConfig: jest.fn().mockImplementation(() => Promise.resolve()),
updateOptionList: jest.fn().mockImplementation(() => Promise.resolve()),
uploadDataModel: jest.fn().mockImplementation(() => Promise.resolve<JsonSchema>({})),
uploadOptionList: jest.fn().mockImplementation(() => Promise.resolve()),
upsertTextResources: jest
Expand Down
10 changes: 4 additions & 6 deletions frontend/packages/shared/src/types/Option.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export type Option<T extends string | boolean | number = string | boolean | number> = {
label: string;
value: T;
description?: string;
helpText?: string;
};
import type { CodeListItem } from '@studio/components';

export type Option<T extends string | boolean | number = string | boolean | number> =
CodeListItem<T>;
3 changes: 2 additions & 1 deletion frontend/packages/shared/src/utils/featureToggleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export type SupportedFeatureFlags =
| 'multipleDataModelsPerTask'
| 'exportForm'
| 'subform'
| 'summary2';
| 'summary2'
| 'codeListEditor';

/*
* Please add all the features that you want to be toggle on by default here.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.manualTabModal[open] {
max-width: unset;
width: min(80vw, 64rem);
}

.modalTrigger {
margin-top: var(--fds-spacing-2);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments here:

  1. We only need to pass queries and the queryclient if we are modifying them. They will be defined in the renderWithProviders if they are not passed 🤗 As this test is now it does not look like it is modifying the queryClients cache so we dont need to pass it.
  2. I see that you await for the spinner to be removed. I see two different approaches here;
    2.1: we can set the optionList directly on the cache if we anyway always will wait for the spinner to removed before executing the tests.
    2.2: we can actually test that the spinner is there when it suppose to be. There are also two ways to implement that. Either you can set the cache in the renderMethod if data is passed to the renderMethod. Or you can use adapt the queries and have the waitForSpinnerTobeRemoved in a separete renderMethod. We have examples of both approcahes around the codebase. Let me know if you need help to find it 🤗

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import { ComponentType } from 'app-shared/types/ComponentType';
import type { FormComponent } from '../../../../../types/FormComponent';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { CodeListEditor } from './CodeListEditor';
import { textMock } from '@studio/testing/mocks/i18nMock';
import userEvent, { type UserEvent } from '@testing-library/user-event';
import { componentMocks } from '@altinn/ux-editor/testing/componentMocks';
import type { Option } from 'app-shared/types/Option';
import { renderWithProviders } from '@altinn/ux-editor/testing/mocks';

// Test data:
const mockComponent: FormComponent<ComponentType.Dropdown> = componentMocks[ComponentType.Dropdown];
mockComponent.optionsId = 'test';

const optionsList = new Map([
[
'text',
[{ value: 'test', label: 'label text', description: 'description', helpText: 'help text' }],
],
['number', [{ value: 2, label: 'label number' }]],
['boolean', [{ value: true, label: 'label boolean' }]],
]);
const queriesMock = {
getOptionLists: jest
.fn()
.mockImplementation(() => Promise.resolve<Map<string, Option[]>>(optionsList)),
};
const queryClientMock = createQueryClientMock();

describe('CodeListTableEditor', () => {
afterEach(() => {
queryClientMock.clear();
});

it('should render the component', async () => {
await renderCodeListTableEditor();

expect(
screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_open_editor'),
}),
).toBeInTheDocument();
});

it('should open Dialog', async () => {
const user = userEvent.setup();
await renderCodeListTableEditor();
await openModal(user);

expect(screen.getByRole('dialog')).toBeInTheDocument();
});

it('should close Dialog', async () => {
const user = userEvent.setup();
await renderCodeListTableEditor();
await openModal(user);

await user.click(screen.getByRole('button', { name: 'close modal' })); // Todo: Replace "close modal" with defaultDialogProps.closeButtonTitle when https://github.com/digdir/designsystemet/issues/2195 is fixed

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('should call handClose when closing Dialog', async () => {
const user = userEvent.setup();
const doReloadPreview = jest.fn();
await renderCodeListTableEditor({ previewContextProps: { doReloadPreview } });
await openModal(user);

await user.click(screen.getByRole('button', { name: 'close modal' })); // Todo: Replace "close modal" with defaultDialogProps.closeButtonTitle when https://github.com/digdir/designsystemet/issues/2195 is fixed

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(doReloadPreview).toHaveBeenCalledTimes(1);
});
});

const openModal = async (user: UserEvent) => {
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_open_editor'),
});
await user.click(btnOpen);
};

const renderCodeListTableEditor = async ({ previewContextProps = {} } = {}) => {
const view = renderWithProviders(
<CodeListEditor
component={{
...mockComponent,
}}
/>,
{
queries: queriesMock,
queryClient: queryClientMock,
previewContextProps: previewContextProps,
},
);
await waitForElementToBeRemoved(screen.queryByTestId('studio-spinner-test-id'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you decide to keep the waitForElementToBeRemoved method we should use the text on it instead of test Id I think 🤔

return view;
};
Loading