Skip to content

Commit

Permalink
feat: add main config section with header behind feature flag (#14702)
Browse files Browse the repository at this point in the history
Co-authored-by: JamalAlabdullah <[email protected]>
  • Loading branch information
lassopicasso and JamalAlabdullah authored Feb 19, 2025
1 parent f2104d9 commit 937fecd
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 135 deletions.
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

0 comments on commit 937fecd

Please sign in to comment.