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 10 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.12644% with 5 lines in your changes missing coverage. Please review.

Project coverage is 95.34%. Comparing base (460ceec) to head (85fa21e).
Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
...EditOptionChoice/EditOptionList/EditOptionList.tsx 93.87% 3 Missing ⚠️
...itOptionList/OptionListEditor/OptionListEditor.tsx 97.72% 0 Missing and 1 partial ⚠️
...ig/editModal/EditOptions/OptionTabs/OptionTabs.tsx 96.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #14137      +/-   ##
==========================================
+ Coverage   95.32%   95.34%   +0.01%     
==========================================
  Files        1780     1786       +6     
  Lines       23159    23292     +133     
  Branches     2689     2711      +22     
==========================================
+ Hits        22077    22208     +131     
- Misses        835      836       +1     
- Partials      247      248       +1     

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


🚨 Try these New Features:

@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

Discovered this bug when having duplicate values:

Spiller.inn.2024-11-26.153820.mp4

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