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 main config section with header behind feature flag #14702

Merged
merged 9 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/packages/shared/src/utils/featureToggleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum FeatureFlag {
ComponentConfigBeta = 'componentConfigBeta',
ExportForm = 'exportForm',
Maskinporten = 'maskinporten',
MainConfig = 'mainConfig',
OptionListEditor = 'optionListEditor',
ShouldOverrideAppLibCheck = 'shouldOverrideAppLibCheck',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,98 +1,54 @@
import React from 'react';
import type { FormItem } from '@altinn/ux-editor/types/FormItem';
import { ComponentType } from 'app-shared/types/ComponentType';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../../../testing/mocks';
import { ComponentMainConfig } from './ComponentMainConfig';
import type { FormItem } from '../../../types/FormItem';
import { ComponentType } from 'app-shared/types/ComponentType';
import userEvent from '@testing-library/user-event';
import { component1Mock } from '@altinn/ux-editor/testing/layoutMock';
import { addFeatureFlagToLocalStorage, FeatureFlag } from 'app-shared/utils/featureToggleUtils';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { typedLocalStorage } from '@studio/pure-functions';
import { renderWithProviders } from '../../../testing/mocks';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { QueryKey } from 'app-shared/types/QueryKey';
import { app, org } from '@studio/testing/testids';
import {
layoutSet1NameMock,
layoutSet2NameMock,
layoutSetsExtendedMock,
layoutSetsMock,
} from '../../../testing/layoutSetsMock';
import { layout1NameMock, layoutMock } from '../../../testing/layoutMock';
import { layoutSetsExtendedMock } from '@altinn/ux-editor/testing/layoutSetsMock';

const summary2Component: FormItem = {
const summary2ComponentMock: FormItem = {
id: '0',
type: ComponentType.Summary2,
itemType: 'COMPONENT',
target: {},
};

describe('ComponentMainConfig', () => {
describe('Summary2', () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
typedLocalStorage.removeItem('featureFlags');
});

it('should render summary2 config', async () => {
const user = userEvent.setup();
render(summary2Component);
expect(summary2AccordionButton()).toBeInTheDocument();
await user.click(summary2AccordionButton());
expect(summary2AddOverrideButton()).toBeInTheDocument();
});
it('should render summary2 config when the component type matches', async () => {
renderComponentMainConfig(summary2ComponentMock);

it('should display overrides', async () => {
const user = userEvent.setup();
const summary2ComponentWithOverrides = {
...summary2Component,
overrides: [{ componentId: '0' }],
};
render(summary2ComponentWithOverrides);
await user.click(summary2AccordionButton());
expect(summary2CollapsedButton(1)).toBeInTheDocument();
});
const targetHeader = screen.getByText(textMock('ux_editor.component_properties.target'));
expect(targetHeader).toBeInTheDocument();
});

it('should call handleComponentChange when adding overrides', async () => {
const user = userEvent.setup();
render(summary2Component);
await user.click(summary2AccordionButton());
await user.click(summary2AddOverrideButton());
expect(handleComponentChange).toHaveBeenCalledTimes(1);
});
it('should render header config when feature flag is set, but the type does not match', async () => {
addFeatureFlagToLocalStorage(FeatureFlag.MainConfig);
renderComponentMainConfig(component1Mock);

it('should call handleComponentChange when changing target', async () => {
const user = userEvent.setup();
render(summary2Component);
await user.selectOptions(summary2TargetLayoutSet(), layoutSet2NameMock);
expect(handleComponentChange).toHaveBeenCalledTimes(1);
});
const sectionHeader = textMock('ux_editor.component_properties.main_configuration');
const headerMainConfig = screen.getByText(sectionHeader);
expect(headerMainConfig).toBeInTheDocument();
});
});

const summary2AccordionButton = () =>
screen.getByRole('button', { name: /ux_editor.component_properties.summary.override.title/ });
const summary2AddOverrideButton = () =>
screen.getByRole('button', { name: /ux_editor.component_properties.summary.add_override/ });
const summary2CollapsedButton = (n: number) =>
screen.getByRole('button', {
name: new RegExp(`ux_editor.component_properties.summary.overrides.nth.*:${n}}`),
});

const summary2TargetLayoutSet = () =>
screen.getByRole('combobox', { name: /ux_editor.component_properties.target_layoutSet_id/ });

const handleComponentChange = jest.fn();
const render = (component: FormItem) => {
const renderComponentMainConfig = (component: FormItem) => {
const handleComponentChange = jest.fn();
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.FormLayouts, org, app, layoutSet1NameMock], {
[layout1NameMock]: layoutMock,
});
queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock);
queryClient.setQueryData([QueryKey.LayoutSetsExtended, org, app], layoutSetsExtendedMock);
renderWithProviders(
return renderWithProviders(
<ComponentMainConfig component={component} handleComponentChange={handleComponentChange} />,
{
queryClient,
appContextProps: {
selectedFormLayoutSetName: layoutSet1NameMock,
selectedFormLayoutName: layout1NameMock,
},
},
{ queryClient },
);
};
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import React from 'react';
import type { FormItem } from '../../../types/FormItem';
import classes from './ComponentMainConfig.module.css';
import { ComponentType } from 'app-shared/types/ComponentType';
import { Accordion } from '@digdir/designsystemet-react';
import { useTranslation } from 'react-i18next';
import { Summary2Override } from '../../config/componentSpecificContent/Summary2/Override/Summary2Override';
import type {
Summary2OverrideConfig,
Summary2TargetConfig,
} from 'app-shared/types/ComponentSpecificConfig';
import { Summary2Target } from '../../config/componentSpecificContent/Summary2/Summary2Target/Summary2Target';
import { StudioHeading } from '@studio/components';
import { RequiredIndicator } from '../../RequiredIndicator';
import { SummaryMainConfig } from './SpecificMainConfig/SummaryMainConfig';
import { HeaderMainConfig } from './HeaderMainConfig';

export type ComponentMainConfigProps = {
component: FormItem;
Expand All @@ -22,55 +13,12 @@ export const ComponentMainConfig = ({
component,
handleComponentChange,
}: ComponentMainConfigProps) => {
const [accordionOpen, setAccordionOpen] = React.useState<Record<string, boolean>>({});
const { t } = useTranslation();

const handleOverridesChange = (updatedOverrides: Summary2OverrideConfig[]): void => {
const updatedComponent = { ...component } as FormItem<ComponentType.Summary2>;
updatedComponent.overrides = updatedOverrides;
handleComponentChange(updatedComponent);
};

const handleTargetChange = (updatedTarget: Summary2TargetConfig): void => {
const updatedComponent = { ...component } as FormItem<ComponentType.Summary2>;
updatedComponent.target = updatedTarget;
updatedComponent.overrides = [];
handleComponentChange(updatedComponent);
};

return (
<>
{component.type === ComponentType.Summary2 && (
<>
<div className={classes.componentMainConfig}>
<StudioHeading size='2xs'>
{t('ux_editor.component_properties.main_configuration')}
<RequiredIndicator />
</StudioHeading>
<Summary2Target target={component.target} onChange={handleTargetChange} />
</div>
<Accordion color='subtle'>
<Accordion.Item open={accordionOpen['summary2overrides'] === true}>
<Accordion.Header
onHeaderClick={() =>
setAccordionOpen((prev) => {
return { ...prev, summary2overrides: !prev['summary2overrides'] };
})
}
>
{t('ux_editor.component_properties.summary.override.title')}
</Accordion.Header>
<Accordion.Content>
<Summary2Override
target={component.target}
overrides={component.overrides}
onChange={handleOverridesChange}
/>
</Accordion.Content>
</Accordion.Item>
</Accordion>
</>
)}
</>
);
switch (component.type) {
case ComponentType.Summary2:
return (
<SummaryMainConfig component={component} handleComponentChange={handleComponentChange} />
);
default:
return <HeaderMainConfig />;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { StudioHeading } from '@studio/components';
import { RequiredIndicator } from '../../RequiredIndicator';
import classes from './HeaderMainConfig.module.css';
import { useTranslation } from 'react-i18next';

type HeaderMainConfigProps = {
children?: React.ReactNode;
};

export const HeaderMainConfig = ({ children }: Partial<HeaderMainConfigProps>): JSX.Element => {
const { t } = useTranslation();

return (
<div className={classes.componentMainConfig}>
<StudioHeading size='2xs'>
{t('ux_editor.component_properties.main_configuration')}
<RequiredIndicator />
</StudioHeading>
{children}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { FormItem } from '../../../types/FormItem';
import { ComponentType } from 'app-shared/types/ComponentType';
import { EditLayoutSetForSubform } from './EditLayoutSetForSubform';
import { ComponentMainConfig } from './ComponentMainConfig';
import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';

export type PropertiesHeaderProps = {
formItem: FormItem;
Expand Down Expand Up @@ -50,7 +51,10 @@ export const PropertiesHeader = ({
/>
)}
</div>
<ComponentMainConfig component={formItem} handleComponentChange={handleComponentUpdate} />
{(formItem.type === ComponentType.Summary2 ||
shouldDisplayFeature(FeatureFlag.MainConfig)) && (
<ComponentMainConfig component={formItem} handleComponentChange={handleComponentUpdate} />
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../../../../testing/mocks';
import { SummaryMainConfig } from './SummaryMainConfig';
import type { FormItem } from '../../../../types/FormItem';
import { ComponentType } from 'app-shared/types/ComponentType';
import userEvent from '@testing-library/user-event';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { QueryKey } from 'app-shared/types/QueryKey';
import { app, org } from '@studio/testing/testids';
import {
layoutSet1NameMock,
layoutSet2NameMock,
layoutSetsExtendedMock,
layoutSetsMock,
} from '../../../../testing/layoutSetsMock';
import { layout1NameMock, layoutMock } from '../../../../testing/layoutMock';

const summary2Component: FormItem = {
id: '0',
type: ComponentType.Summary2,
itemType: 'COMPONENT',
target: {},
};

describe('ComponentMainConfig', () => {
describe('Summary2', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render summary2 config', async () => {
const user = userEvent.setup();
render(summary2Component);
expect(summary2AccordionButton()).toBeInTheDocument();
await user.click(summary2AccordionButton());
expect(summary2AddOverrideButton()).toBeInTheDocument();
});

it('should display overrides', async () => {
const user = userEvent.setup();
const summary2ComponentWithOverrides = {
...summary2Component,
overrides: [{ componentId: '0' }],
};
render(summary2ComponentWithOverrides);
await user.click(summary2AccordionButton());
expect(summary2CollapsedButton(1)).toBeInTheDocument();
});

it('should call handleComponentChange when adding overrides', async () => {
const user = userEvent.setup();
render(summary2Component);
await user.click(summary2AccordionButton());
await user.click(summary2AddOverrideButton());
expect(handleComponentChange).toHaveBeenCalledTimes(1);
});

it('should call handleComponentChange when changing target', async () => {
const user = userEvent.setup();
render(summary2Component);
await user.selectOptions(summary2TargetLayoutSet(), layoutSet2NameMock);
expect(handleComponentChange).toHaveBeenCalledTimes(1);
});
});
});

const summary2AccordionButton = () =>
screen.getByRole('button', { name: /ux_editor.component_properties.summary.override.title/ });
const summary2AddOverrideButton = () =>
screen.getByRole('button', { name: /ux_editor.component_properties.summary.add_override/ });
const summary2CollapsedButton = (n: number) =>
screen.getByRole('button', {
name: new RegExp(`ux_editor.component_properties.summary.overrides.nth.*:${n}}`),
});

const summary2TargetLayoutSet = () =>
screen.getByRole('combobox', { name: /ux_editor.component_properties.target_layoutSet_id/ });

const handleComponentChange = jest.fn();
const render = (component: FormItem<ComponentType.Summary2>) => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.FormLayouts, org, app, layoutSet1NameMock], {
[layout1NameMock]: layoutMock,
});
queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock);
queryClient.setQueryData([QueryKey.LayoutSetsExtended, org, app], layoutSetsExtendedMock);
renderWithProviders(
<SummaryMainConfig component={component} handleComponentChange={handleComponentChange} />,
{
queryClient,
appContextProps: {
selectedFormLayoutSetName: layoutSet1NameMock,
selectedFormLayoutName: layout1NameMock,
},
},
);
};
Loading