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: merge select and edit tabs in config #14137

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

Konrad-Simso
Copy link
Contributor

@Konrad-Simso Konrad-Simso commented Nov 22, 2024

Description

The PR contains the following:

  • Merge of CodeList and Manual tabs in config
  • onChange -> onBlur for StudioCodeListEditor
  • Update names for text resources in nb.json, codelist -> code_list

It's possible to split it into multiple PRs to make Review & Testing easier, but i'll leave that up to the person doing review.

Video of current design

PR.13685.mp4

Duplicated files

There are a few duplicate files and functions in this PR. These have been marked with Todo: Remove comments, or are in a seperate folder. The duplicates have been created to make it easier to remove old code once we're removing the feature flag optionListEditor.

Localtions of duplicate code:

  • OptionListEditor-v1 folder
  • OptionTabs.tsx
  • OptionUtils.ts

Related Issue(s)

Verification

  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)

…while developing StudioCodeListEditor behind a featureFlag.
# Conflicts:
#	frontend/packages/ux-editor/src/components/config/editModal/EditOptions/EditOptions.test.tsx
#	frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditManualOptionsWithEditor/EditManualOptionsWithEditor.tsx
#	frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/OptionTabs.tsx
#	frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/hooks/useOptionListEditorTexts.ts
@github-actions github-actions bot added area/ui-editor Area: Related to the designer tool for assembling app UI in Altinn Studio. solution/studio/designer Issues related to the Altinn Studio Designer solution. frontend labels Nov 22, 2024
@Konrad-Simso Konrad-Simso linked an issue Nov 22, 2024 that may be closed by this pull request
# Conflicts:
#	frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList-v1/EditOptionList.tsx
Copy link

codecov bot commented Nov 22, 2024

Codecov Report

Attention: Patch coverage is 97.48744% with 5 lines in your changes missing coverage. Please review.

Project coverage is 95.33%. Comparing base (460ceec) to head (96879e1).
Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
.../EditTab/OptionListUploader/OptionListUploader.tsx 91.42% 3 Missing ⚠️
...Tabs/EditTab/OptionListEditor/OptionListEditor.tsx 97.67% 0 Missing and 1 partial ⚠️
...ig/editModal/EditOptions/OptionTabs/OptionTabs.tsx 95.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #14137      +/-   ##
==========================================
+ Coverage   95.32%   95.33%   +0.01%     
==========================================
  Files        1780     1787       +7     
  Lines       23159    23259     +100     
  Branches     2689     2707      +18     
==========================================
+ Hits        22077    22175      +98     
- Misses        835      836       +1     
- Partials      247      248       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@ErlingHauan ErlingHauan added the text/content used for issues that need som text improvements, often by ux label Nov 25, 2024
@Ildest Ildest self-requested a review November 25, 2024 08:48
@@ -1525,6 +1525,10 @@
"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": "Velg fra biblioteket",
"ux_editor.modal_properties_code_list_alert_title": "Du er i ferd med å redigere en kodeliste i biblioteket.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"ux_editor.modal_properties_code_list_alert_title": "Du er i ferd med å redigere en kodeliste i biblioteket.",
"ux_editor.modal_properties_code_list_alert_title": "Du redigerer nå en kodeliste i biblioteket.",

"ux_editor.options.section_heading": "Valg for kodelister",
"ux_editor.options.single": "{{value}} alternativ",
"ux_editor.options.tab_code_list": "Velg kodeliste",
"ux_editor.options.tab_manual": "Sett opp egne alternativer",
"ux_editor.options.tab_referenceId": "Angi referanse-ID",
"ux_editor.options.upload_title": "Last opp din egen",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"ux_editor.options.upload_title": "Last opp din egen",
"ux_editor.options.upload_title": "Last opp egen kodeliste",

Copy link
Contributor

@Ildest Ildest left a comment

Choose a reason for hiding this comment

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

Ser bra ut dette @Konrad-Simso og @ErlingHauan. Har justert litt på et par tekster, men ingen krise om dere er uenige og avviser dem :-D.

queryClientMock.clear();
});
afterEach(() => queryClientMock.clear());

it('should render', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should render', async () => {
it('should render', () => {

},
});
expect(await screen.findByText(textMock('ux_editor.modal_new_option'))).toBeInTheDocument();
it('should render spinner when loading data', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should render spinner when loading data', async () => {
it('should render spinner when loading data', () => {

Comment on lines 9 to 12
.modalTrigger {
width: 50%;
}

Copy link
Contributor

@ErlingHauan ErlingHauan Nov 25, 2024

Choose a reason for hiding this comment

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

I see where you are going with this, but when the column width is made small, the library button becomes a little squashed. I suggest adding min-width: 12rem to the modalTrigger classes, so that the button labels remain on one line.

Before:

before.mp4

After:

after.mp4

).toBeInTheDocument();
});

it('should editOptionsId to blank when removing choice', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should editOptionsId to blank when removing choice', async () => {
it('should change editOptionsId to blank when removing choice', async () => {

Comment on lines 20 to 22
const isOptionChosen =
(component.optionsId !== undefined && component.optionsId !== '') ||
component.options !== undefined;
Copy link
Contributor

@ErlingHauan ErlingHauan Nov 25, 2024

Choose a reason for hiding this comment

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

I think this part could be more clear 🤔
The logic could be wrapped inside variables, and the variable name isOptionChosen can be made more explicit. Maybe something like componentHasOptionList:

Suggested change
const isOptionChosen =
(component.optionsId !== undefined && component.optionsId !== '') ||
component.options !== undefined;
const hasOptionsId = component.optionsId !== undefined && component.optionsId !== '';
const componentHasOptionList = hasOptionsId || component.options;

Or maybe we can even just say:

Suggested change
const isOptionChosen =
(component.optionsId !== undefined && component.optionsId !== '') ||
component.options !== undefined;
const componentHasOptionList = component.optionsId || component.options;

Comment on lines 25 to 29
const shouldDisplayChosenOption = isOptionChosen && chosenOption === true;

return (
<>
{shouldDisplayChosenOption ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to simplify this, by replacing shouldDisplayChosenOption with chosenOption?

Suggested change
const shouldDisplayChosenOption = isOptionChosen && chosenOption === true;
return (
<>
{shouldDisplayChosenOption ? (
return (
<>
{chosenOption ? (

@@ -0,0 +1,3 @@
.modalTrigger {
width: 50%;
Copy link
Contributor

Choose a reason for hiding this comment

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

See the other comment regarding the modalTrigger class 😊

Copy link
Contributor

Choose a reason for hiding this comment

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

In this file, waitFor can be removed.

Comment on lines 23 to 26
it('should render the component', async () => {
renderEditOptionList();
expect(await screen.findByText(textMock('ux_editor.options.upload_title'))).toBeInTheDocument();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

This test case can be done without async:

Suggested change
it('should render the component', async () => {
renderEditOptionList();
expect(await screen.findByText(textMock('ux_editor.options.upload_title'))).toBeInTheDocument();
});
it('should render the component', () => {
renderEditOptionList();
expect(screen.getByText(textMock('ux_editor.options.upload_title'))).toBeInTheDocument();
});

await user.upload(fileInput, file);
}

async function userFindDropDownButton(user: UserEvent) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
async function userFindDropDownButton(user: UserEvent) {
async function userFindDropDownButtonAndClick(user: UserEvent) {

);

await userFindDropDownButton(user);
const choice = screen.getByText(optionListIdsMock[0]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the test would be quicker to read with a more explicit variable name, for example:

Suggested change
const choice = screen.getByText(optionListIdsMock[0]);
const dropdownOption = screen.getByText(optionListIdsMock[0]);

setChosenOption: (value: boolean) => void;
} & Pick<IGenericEditComponent<SelectionComponentType>, 'component' | 'handleComponentChange'>;

function DisplayChosenOption({
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we rename this component?

Suggested change
function DisplayChosenOption({
function SelectedOptionList({

Comment on lines 23 to 91
export function EditOptionList<T extends SelectionComponentType>({
setChosenOption,
component,
handleComponentChange,
}: EditOptionListProps<T>) {
const { t } = useTranslation();
const { org, app } = useStudioEnvironmentParams();
const { data: optionListIds } = useOptionListIdsQuery(org, app);
const { mutate: uploadOptionList } = useAddOptionListMutation(org, app, {
hideDefaultError: (error: AxiosError<ApiError>) => !error.response.data.errorCode,
});

const handleOptionsIdChange = (optionsId: string) => {
if (component.options) {
delete component.options;
}

handleComponentChange({
...component,
optionsId,
});

setChosenOption(true);
};

const onSubmit = (file: File) => {
const fileNameError = findFileNameError(optionListIds, file.name);
if (fileNameError) {
handleInvalidFileName(fileNameError);
} else {
handleUpload(file);
}
};

const handleUpload = (file: File) => {
uploadOptionList(file, {
onSuccess: () => {
handleOptionsIdChange(FileNameUtils.removeExtension(file.name));
toast.success(t('ux_editor.modal_properties_code_list_upload_success'));
},
onError: (error: AxiosError<ApiError>) => {
if (!error.response?.data?.errorCode) {
toast.error(`${t('ux_editor.modal_properties_code_list_upload_generic_error')}`);
}
},
});
};

const handleInvalidFileName = (fileNameError: FileNameError) => {
switch (fileNameError) {
case 'invalidFileName':
return toast.error(t('ux_editor.modal_properties_code_list_filename_error'));
case 'fileExists':
return toast.error(t('ux_editor.modal_properties_code_list_upload_duplicate_error'));
}
};

return (
<>
<OptionListSelector handleOptionsIdChange={handleOptionsIdChange} />
<StudioFileUploader
accept='.json'
variant={'tertiary'}
uploaderButtonText={t('ux_editor.options.upload_title')}
onSubmit={onSubmit}
/>
</>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can the selector and the file uploader be separated into two different components?

Suggestion:
handleOptionsIdChange could be moved into OptionListSelector, and a new component OptionListUploader could wrap around StudioFileUploder and its handler functions. This would improve separation of concerns.

Then we wouldn't need EditOptionList anymore, and OptionListSelector and OptionListUploader could be called directly from EditOptionChoice.

EditOptionChoice could then return this:

{chosenOption ? (
        <DisplayChosenOption
          setChosenOption={setChosenOption}
          component={component}
          handleComponentChange={handleComponentChange}
        />
      ) : (
        <div className={classes.optionButtons}>
          <EditManualOptionsWithEditor
            setChosenOption={setChosenOption}
            component={component}
            handleComponentChange={handleComponentChange}
          />
          <OptionListSelector
            setChosenOption={setChosenOption}
            component={component}
            handleComponentChange={handleComponentChange}
          />
          <OptionListUploader
            setChosenOption={setChosenOption}
            component={component}
            handleComponentChange={handleComponentChange}
          />
        </div>

Comment on lines 33 to 35
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
});
Copy link
Contributor

@ErlingHauan ErlingHauan Nov 26, 2024

Choose a reason for hiding this comment

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

Naming suggestion:

Suggested change
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
});
const editOptionsButton = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
});

await renderOptionListEditorAndWaitForSpinnerToBeRemoved();

await openManualModal(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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
await user.click(screen.getByRole('button', { name: 'close modal' })); // Todo: Replace "close modal" with defaultDialogProps.closeButtonTitle when we upgrade to Designsystemet v1

it('should render the open Dialog button', async () => {
await renderOptionListEditorAndWaitForSpinnerToBeRemoved();

const btnOpen = screen.getByRole('button', {
Copy link
Contributor

@ErlingHauan ErlingHauan Nov 26, 2024

Choose a reason for hiding this comment

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

Naming suggestion:

Suggested change
const btnOpen = screen.getByRole('button', {
const editOptionsButton = screen.getByRole('button', {

await renderOptionListEditorAndWaitForSpinnerToBeRemoved();

await openOptionModal(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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
await user.click(screen.getByRole('button', { name: 'close modal' })); // Todo: Replace "close modal" with defaultDialogProps.closeButtonTitle when we upgrade to Designsystemet v1

Comment on lines 184 to 195
const openOptionModal = async (user: UserEvent) => {
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_library'),
});
await user.click(btnOpen);
};
const openManualModal = async (user: UserEvent) => {
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
});
await user.click(btnOpen);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming suggestion:

Suggested change
const openOptionModal = async (user: UserEvent) => {
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_library'),
});
await user.click(btnOpen);
};
const openManualModal = async (user: UserEvent) => {
const btnOpen = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
});
await user.click(btnOpen);
};
const openOptionModal = async (user: UserEvent) => {
const editOptionsButton = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_library'),
});
await user.click(editOptionsButton);
};
const openManualModal = async (user: UserEvent) => {
const editOptionsButton = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
});
await user.click(editOptionsButton);
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Edited to not have user.click inside the functions:

function getOptionModalButton() {
  return screen.getByRole('button', {
    name: textMock('ux_editor.modal_properties_code_list_button_title_library'),
  });
}

function getManualModalButton() {
  return screen.getByRole('button', {
    name: textMock('ux_editor.modal_properties_code_list_button_title_manual'),
  });
}

Comment on lines 50 to 64
return (
<OptionListEditorModal
label={label}
optionsId={optionsId}
optionsList={optionsListMap[optionsId]}
/>
);
}
if (component.options !== undefined) {
return (
<OptionListEditorModalManualOptions
label={label}
component={component}
handleComponentChange={handleComponentChange}
/>
Copy link
Contributor

@ErlingHauan ErlingHauan Nov 26, 2024

Choose a reason for hiding this comment

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

Naming suggestion:

Suggested change
return (
<OptionListEditorModal
label={label}
optionsId={optionsId}
optionsList={optionsListMap[optionsId]}
/>
);
}
if (component.options !== undefined) {
return (
<OptionListEditorModalManualOptions
label={label}
component={component}
handleComponentChange={handleComponentChange}
/>
return (
<LibraryOptionListEditorModal
label={label}
optionsId={optionsId}
optionsList={optionsListMap[optionsId]}
/>
);
}
if (component.options !== undefined) {
return (
<ManualOptionListEditorModal
label={label}
component={component}
handleComponentChange={handleComponentChange}
/>

const { org, app } = useStudioEnvironmentParams();
const { doReloadPreview } = usePreviewContext();
const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app);
const { debounce } = useDebounce({ debounceTimeInMs: AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS });
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need debounce, now that we save onBlur?

describe('EditOptions', () => {
afterEach(() => jest.clearAllMocks());

it('should render component', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should render component', async () => {
it('should render component', () => {

expect(screen.getByText(textMock('ux_editor.options.tab_code_list'))).toBeInTheDocument();
});

it('should show code list input by default when neither options nor optionId are set', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice test case! But async can be removed:

Suggested change
it('should show code list input by default when neither options nor optionId are set', async () => {
it('should show code list input by default when neither options nor optionId are set', () => {

).toBeInTheDocument();
});

it('should show code list tab when component has optionsId defined matching an optionId in optionsID-list', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should show code list tab when component has optionsId defined matching an optionId in optionsID-list', async () => {
it('should show code list tab when component has optionsId defined matching an optionId in optionsID-list', () => {

).toBeInTheDocument();
});

it('should show referenceId tab when component has optionsId defined not matching an optionId in optionsId-list', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should show referenceId tab when component has optionsId defined not matching an optionId in optionsId-list', async () => {
it('should show referenceId tab when component has optionsId defined not matching an optionId in optionsId-list', () => {

).toBeInTheDocument();
});

it('should switch to code list tab when input clicking code list tab', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('should switch to code list tab when input clicking code list tab', async () => {
it('should switch to code list tab when clicking code list tab', async () => {

) : (
<EditManualOptions component={component} handleComponentChange={handleComponentChange} />
<EditOptionChoice component={component} handleComponentChange={handleComponentChange} />
{errorMessage && component.options !== undefined && (
Copy link
Contributor

Choose a reason for hiding this comment

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

component.options !== undefined

Is this because Det må være minst én radioknapp shows up too often?

Maybe we can keep the error message for now (since we are behind feature flag), and update the logic in useComponentErrorMessage/useValidateComponent in a separate issue?

Comment on lines 169 to 172
// Todo: Remove once featureFlag "optionListEditor" is removed.
type RenderManualOptionsV1Props = {
areLayoutOptionsSupported: boolean;
} & Pick<IGenericEditComponent<SelectionComponentType>, 'component' | 'handleComponentChange'>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we will avoid having these "almost duplicate" functions in the same files, if we do a hard separation of the old and new features as suggested in another comment 🤔

Comment on lines 47 to 56
// Copy of function above. Todo: Remove once featureFlag "optionListEditor" is removed.
export function getSelectedOptionsTypeV1(
codeListId: string | undefined,
options: IOption[] | undefined,
optionListIds: string[] = [],
): SelectedOptionsType {
/** It is not permitted for a component to have both options and optionsId set on the same component. */
if (options?.length && codeListId) {
return SelectedOptionsType.Unknown;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can getSelectedOptionsTypeV1 and getSelectedOptionsType be moved next to where they are used? If we can place getSelectedOptionsTypeV1 inside one of the "old" folders, Then we don't need to have different versions next to each other, and the old function will be deleted together with the old functionality.

@ErlingHauan
Copy link
Contributor

ErlingHauan commented Nov 26, 2024

Discovered this bug when having duplicate values:

Spiller.inn.2024-11-26.153820.mp4

Edit: the bug is also in prod.

handleComponentChange,
areLayoutOptionsSupported,
}: RenderManualOptionsProps) => {
function TabWithErrorHandling({ component, handleComponentChange }: TabWithErrorHandlingProps) {
Copy link
Contributor

Choose a reason for hiding this comment

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

One alternative to having TabWithErrorHandling is having the error handling inside EditTab and ManualTab.

Advantages to the alternative:

  • Featureflag conditional will only be used once, in OptionTabs, which is pretty neat
  • Easier to read <StudioTabs.Content>. With TabWithErrorHandling:
    • First we have <SelectTab /> (clear name, we have a good idea what this tab is about)
    • Then we have <TabWithErrorHandling /> (not as clear name; we can't know exactly what tab this is about without looking inside the component)
    • Then we have <ReferenceTab /> (clear name)

Disadvantages:

  • Slightly longer code in EditTab and ManualTab, and some code duplication
  • The CSS class errorMessage might have to be duplicated

What do you think? 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/ui-editor Area: Related to the designer tool for assembling app UI in Altinn Studio. frontend solution/studio/designer Issues related to the Altinn Studio Designer solution. text/content used for issues that need som text improvements, often by ux
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Merge "select codelist" and "edit codelist" views
3 participants