Skip to content

Commit

Permalink
Support multipage groups (#11382)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasEng authored Oct 30, 2023
1 parent 7efa44b commit 3ec58dd
Show file tree
Hide file tree
Showing 51 changed files with 1,287 additions and 755 deletions.
85 changes: 85 additions & 0 deletions frontend/packages/shared/src/types/ComponentSpecificConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ComponentType } from './ComponentType';
import { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import { MapLayer } from 'app-shared/types/MapLayer';
import { FormPanelVariant } from 'app-shared/types/FormPanelVariant';

type Option<T = any> = {
label: string;
value: T;
};

type OptionsComponentBase = {
options?: Option[];
preselectedOptionIndex?: number;
optionsId?: string;
};

type FileUploadComponentBase = {
description: string;
hasCustomFileEndings: boolean;
maxFileSizeInMB: number;
displayMode: string;
maxNumberOfAttachments: number;
minNumberOfAttachments: number;
validFileEndings?: string;
};

export type ComponentSpecificConfig<T extends ComponentType = ComponentType> = {
[ComponentType.Alert]: { severity: 'success' | 'info' | 'warning' | 'danger' };
[ComponentType.Accordion]: {};
[ComponentType.AccordionGroup]: {};
[ComponentType.ActionButton]: {};
[ComponentType.AddressComponent]: { simplified: boolean };
[ComponentType.AttachmentList]: {};
[ComponentType.Button]: { onClickAction: () => void };
[ComponentType.ButtonGroup]: {};
[ComponentType.Checkboxes]: OptionsComponentBase;
[ComponentType.Custom]: { tagName: string; framework: string; [id: string]: any };
[ComponentType.Datepicker]: { timeStamp: boolean };
[ComponentType.Dropdown]: { optionsId: string };
[ComponentType.FileUpload]: FileUploadComponentBase;
[ComponentType.FileUploadWithTag]: FileUploadComponentBase & { optionsId: string };
[ComponentType.Grid]: {};
[ComponentType.Group]: {
maxCount?: number;
edit?: {
multiPage?: boolean;
mode?: string;
};
};
[ComponentType.Header]: { size: string };
[ComponentType.IFrame]: {};
[ComponentType.Image]: {
image?: {
src?: KeyValuePairs<string>;
align?: string | null;
width?: string;
};
};
[ComponentType.Input]: { disabled?: boolean };
[ComponentType.InstanceInformation]: {};
[ComponentType.InstantiationButton]: {};
[ComponentType.Likert]: {};
[ComponentType.Link]: {};
[ComponentType.List]: {};
[ComponentType.Map]: {
centerLocation: {
latitude: number;
longitude: number;
};
zoom: number;
layers?: MapLayer[];
};
[ComponentType.MultipleSelect]: {};
[ComponentType.NavigationBar]: {};
[ComponentType.NavigationButtons]: { showSaveButton?: boolean; showPrev?: boolean };
[ComponentType.Panel]: {
variant: FormPanelVariant;
showIcon: boolean;
};
[ComponentType.Paragraph]: {};
[ComponentType.PrintButton]: {};
[ComponentType.RadioButtons]: OptionsComponentBase;
[ComponentType.Summary]: {};
[ComponentType.TextArea]: {};
}[T];
5 changes: 5 additions & 0 deletions frontend/packages/shared/src/types/FormPanelVariant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum FormPanelVariant {
Info = 'info',
Warning = 'warning',
Success = 'success',
}
5 changes: 5 additions & 0 deletions frontend/packages/shared/src/types/MapLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MapLayer {
url: string;
attribution?: string;
subdomains?: string[];
}
16 changes: 12 additions & 4 deletions frontend/packages/shared/src/types/api/FormLayoutsResponse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import { ComponentType } from 'app-shared/types/ComponentType';
import { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecificConfig';

export type FormLayoutsResponse = KeyValuePairs<ExternalFormLayout>;

Expand All @@ -15,8 +16,15 @@ export interface ExternalData {
[key: string]: any;
}

export interface ExternalComponent {
type ExternalComponentBase<T extends ComponentType = ComponentType> = {
id: string;
type: ComponentType;
[key: string]: any; // Todo: Set type here
}
type: T;
dataModelBindings?: KeyValuePairs<string>;
textResourceBindings?: KeyValuePairs<string>;
[key: string]: any;
};

export type ExternalComponent<T extends ComponentType = ComponentType> = {
[componentType in ComponentType]: ExternalComponentBase<componentType> &
ComponentSpecificConfig<componentType>;
}[T];
29 changes: 28 additions & 1 deletion frontend/packages/shared/src/utils/objectUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { areObjectsEqual } from 'app-shared/utils/objectUtils';
import { areObjectsEqual, mapByProperty } from 'app-shared/utils/objectUtils';

describe('objectUtils', () => {
describe('areObjectsEqual', () => {
Expand All @@ -15,4 +15,31 @@ describe('objectUtils', () => {
expect(areObjectsEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 })).toBe(false);
});
});

describe('mapByProperty', () => {
const property = 'id';
const value1 = 'value1';
const value2 = 'value2';
const value3 = 'value3';
const object1 = { [property]: value1 };
const object2 = { [property]: value2, otherProperty: 'Some irrelevant value' };
const object3 = { [property]: value3, otherProperty: 'Another irrelevant value' };

it('Maps an array of objects to a key-value pair object, where the key is the value of the property', () => {
const objectList = [object1, object2, object3];
expect(mapByProperty(objectList, property)).toEqual({
[value1]: object1,
[value2]: object2,
[value3]: object3,
});
});

it('Throws an error if the values of the given property are not unique', () => {
const object4 = { [property]: value1 };
const objectList = [object1, object2, object3, object4];
const expectedError =
'The values of the given property in the mapByProperty function should be unique.';
expect(() => mapByProperty(objectList, property)).toThrowError(expectedError);
});
});
});
24 changes: 23 additions & 1 deletion frontend/packages/shared/src/utils/objectUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import { areItemsUnique } from 'app-shared/utils/arrayUtils';

/**
* Checks if two objects are equal (shallow comparison).
* @param obj1 The first object.
Expand All @@ -12,4 +15,23 @@ export const areObjectsEqual = <T extends object>(obj1: T, obj2: T): boolean =>
}
}
return true;
}
};

/**
* Maps an array of objects to a key-value pair object, where the key is the value of the given property.
* Requires that the values of the given property are unique.
* @param objectList
* @param property
*/
export const mapByProperty = <T extends object>(
objectList: T[],
property: keyof T,
): KeyValuePairs<T> => {
const keys = objectList.map((object) => object[property]);
if (!areItemsUnique(keys)) {
throw new Error(
'The values of the given property in the mapByProperty function should be unique.',
);
}
return Object.fromEntries(objectList.map((object) => [object[property], object]));
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FormField } from '../../../FormField';
import { useText } from '../../../../hooks';
import { stringToArray, arrayToString } from '../../../../utils/stringUtils';
import classes from './MapComponent.module.css';
import type { FormMapLayer } from '../../../../types/FormComponent';
import type { MapLayer } from 'app-shared/types/MapLayer';

export const MapComponent = ({
component,
Expand Down Expand Up @@ -117,7 +117,7 @@ const AddMapLayer = ({ component, handleComponentChange }: AddMapLayerProps): JS
});
};

const updateLayer = (index: number, subdomains: string[]): FormMapLayer[] => {
const updateLayer = (index: number, subdomains: string[]): MapLayer[] => {
return [
...component.layers.slice(0, index),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import React from 'react';
import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PanelComponent } from './PanelComponent';
import { FormPanelComponent, FormPanelVariant } from '../../../../types/FormComponent';
import { FormComponent } from '../../../../types/FormComponent';
import { renderHookWithMockStore, renderWithMockStore } from '../../../../testing/mocks';
import { useLayoutSchemaQuery } from '../../../../hooks/queries/useLayoutSchemaQuery';
import { ComponentType } from 'app-shared/types/ComponentType';
import { useFormLayoutsQuery } from '../../../../hooks/queries/useFormLayoutsQuery';
import { useFormLayoutSettingsQuery } from '../../../../hooks/queries/useFormLayoutSettingsQuery';
import { textMock } from '../../../../../../../testing/mocks/i18nMock';
import { FormPanelVariant } from 'app-shared/types/FormPanelVariant';

// Test data:
const org = 'org';
const app = 'app';
const selectedLayoutSet = 'test-layout-set';

const component: FormPanelComponent = {
const component: FormComponent<ComponentType.Panel> = {
id: '',
itemType: 'COMPONENT',
type: ComponentType.Panel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Switch, Select } from '@digdir/design-system-react';
import type { IGenericEditComponent } from '../../componentConfig';
import { useText } from '../../../../hooks';
import { EditTextResourceBinding } from '../../editModal/EditTextResourceBinding';
import { FormPanelVariant } from '../../../../types/FormComponent';
import { FormPanelVariant } from 'app-shared/types/FormPanelVariant';
import { FormField } from '../../../FormField';

export const PanelComponent = ({ component, handleComponentChange }: IGenericEditComponent) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { externalLayoutToInternal } from './externalLayoutToInternal';
import {
externalLayoutWithMultiPageGroup,
internalLayoutWithMultiPageGroup,
} from '../../testing/layoutWithMultiPageGroupMocks';
import { createEmptyLayout } from '../../utils/formLayoutUtils';
import { ExternalFormLayout } from 'app-shared/types/api';
import { IInternalLayout } from '../../types/global';
import { layoutSchemaUrl } from 'app-shared/cdn-paths';

describe('externalLayoutToInternal', () => {
it('Converts an external layout to an internal layout', () => {
const result = externalLayoutToInternal(externalLayoutWithMultiPageGroup);
expect(result).toEqual(internalLayoutWithMultiPageGroup);
});

it('Returns an empty layout if the external layout is null', () => {
const result = externalLayoutToInternal(null);
expect(result).toEqual(createEmptyLayout());
});

it('Returns an empty layout with custom properties when the "data" property is null', () => {
const customProperty1 = 'test1';
const customProperty2 = 'test2';
const externalLayout: ExternalFormLayout = {
$schema: layoutSchemaUrl(),
data: null,
customProperty1,
customProperty2,
};
const expectedResult: IInternalLayout = {
...createEmptyLayout(),
customRootProperties: {
customProperty1,
customProperty2,
},
};
const result = externalLayoutToInternal(externalLayout);
expect(result).toEqual(expectedResult);
});

it('Returns an empty layout with custom properties when the "layout" property within the "data" property is null', () => {
const rootCustomProperty1 = 'test1';
const rootCustomProperty2 = 'test2';
const dataCustomProperty1 = 'test3';
const dataCustomProperty2 = 'test4';
const externalLayout: ExternalFormLayout = {
$schema: layoutSchemaUrl(),
data: {
layout: null,
dataCustomProperty1,
dataCustomProperty2,
},
rootCustomProperty1,
rootCustomProperty2,
};
const expectedResult: IInternalLayout = {
...createEmptyLayout(),
customRootProperties: {
rootCustomProperty1,
rootCustomProperty2,
},
customDataProperties: {
dataCustomProperty1,
dataCustomProperty2,
},
};
const result = externalLayoutToInternal(externalLayout);
expect(result).toEqual(expectedResult);
});
});
Loading

0 comments on commit 3ec58dd

Please sign in to comment.