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

Shifting complexity in expressions/hierarchy #943

Merged
merged 9 commits into from
Feb 24, 2023
10 changes: 7 additions & 3 deletions src/__mocks__/formLayoutGroupMock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { ExprUnresolved } from 'src/features/expressions/types';
import type { ILayoutGroup } from 'src/layout/Group/types';

export function getFormLayoutGroupMock(customMock?: Partial<ILayoutGroup>, children?: string[]): ILayoutGroup {
const mockLayoutGroup: ILayoutGroup = {
export function getFormLayoutGroupMock(
customMock?: Partial<ExprUnresolved<ILayoutGroup>>,
children?: string[],
): ExprUnresolved<ILayoutGroup> {
const mockLayoutGroup: ExprUnresolved<ILayoutGroup> = {
id: 'container-closed-id',
type: 'Group',
children: children || ['field1', 'field2', 'field3', 'field4'],
Expand All @@ -16,7 +20,7 @@ export function getFormLayoutGroupMock(customMock?: Partial<ILayoutGroup>, child
};
}

export function getMultiPageGroupMock(): ILayoutGroup {
export function getMultiPageGroupMock(): ExprUnresolved<ILayoutGroup> {
return {
type: 'Group',
id: 'multipageGroup',
Expand Down
12 changes: 5 additions & 7 deletions src/components/message/ErrorReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { renderLayoutComponent } from 'src/features/form/containers/Form';
import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice';
import { getLanguageFromKey, getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { AsciiUnitSeparator } from 'src/utils/attachment';
import { nodesInLayouts } from 'src/utils/layout/hierarchy';
import { useExprContext } from 'src/utils/layout/ExprContext';
import { getMappedErrors, getUnmappedErrors } from 'src/utils/validation/validation';
import type { ILayout } from 'src/layout/layout';
import type { AnyChildNode } from 'src/utils/layout/hierarchy.types';
import type { LayoutNode } from 'src/utils/layout/hierarchy';
import type { FlatError } from 'src/utils/validation/validation';

export interface IErrorReportProps {
Expand Down Expand Up @@ -56,12 +56,11 @@ export const ErrorReport = ({ components }: IErrorReportProps) => {
const classes = useStyles();
const dispatch = useAppDispatch();
const currentView = useAppSelector((state) => state.formLayout.uiConfig.currentView);
const layouts = useAppSelector((state) => state.formLayout.layouts);
const repeatingGroups = useAppSelector((state) => state.formLayout.uiConfig.repeatingGroups);
const [errorsMapped, errorsUnmapped] = useAppSelector((state) => [
getMappedErrors(state.formValidations.validations),
getUnmappedErrors(state.formValidations.validations),
]);
const nodes = useExprContext();
const language = useAppSelector((state) => state.language.language);
const hasErrors = errorsUnmapped.length > 0 || errorsMapped.length > 0;

Expand All @@ -82,14 +81,13 @@ export const ErrorReport = ({ components }: IErrorReportProps) => {
);
}

const nodes = nodesInLayouts(layouts, error.layout, repeatingGroups);
const componentNode = nodes.findById(error.componentId);
const componentNode = nodes?.findById(error.componentId);

// Iterate over parent repeating groups
componentNode?.parents().forEach((parentNode, i, allParents) => {
const parent = parentNode.item;
if (parent?.type == 'Group' && parent.edit?.mode !== 'likert' && parent.maxCount && parent.maxCount > 1) {
const childNode = i == 0 ? componentNode : (allParents[i - 1] as AnyChildNode<'unresolved'>);
const childNode = i == 0 ? componentNode : (allParents[i - 1] as LayoutNode);

// Go to correct multiPage page if necessary
if (parent.edit?.multiPage && 'multiPageIndex' in childNode.item) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/summary/SummaryComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SummaryComponent } from 'src/components/summary/SummaryComponent';
import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice';
import { renderWithProviders } from 'src/testUtils';
import type { ISummaryComponent } from 'src/components/summary/SummaryComponent';
import type { ExprUnresolved } from 'src/features/expressions/types';
import type { ILayoutState } from 'src/features/form/layout/formLayoutSlice';
import type { ILayoutComponent } from 'src/layout/layout';
import type { IValidations } from 'src/types';
Expand All @@ -28,7 +29,7 @@ describe('SummaryComponent', () => {
textResourceBindings: {},
children: [],
maxCount: 10,
} as ILayoutComponent),
} as ExprUnresolved<ILayoutComponent>),
),
],
},
Expand Down
16 changes: 10 additions & 6 deletions src/components/summary/SummaryComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import {
} from 'src/utils/formComponentUtils';
import { useResolvedNode } from 'src/utils/layout/ExprContext';
import { getTextFromAppOrDefault } from 'src/utils/textResource';
import type { ComponentExceptGroupAndSummary, ILayoutComponent, RenderableGenericComponent } from 'src/layout/layout';
import type { ExprUnresolved } from 'src/features/expressions/types';
import type { ILayoutCompSummary } from 'src/layout/Summary/types';
import type { IComponentValidations, IRuntimeState } from 'src/types';

export interface ISummaryComponent extends Omit<ILayoutCompSummary, 'type'> {
export interface ISummaryComponent extends Omit<ExprUnresolved<ILayoutCompSummary>, 'type'> {
formData?: any;
}

Expand Down Expand Up @@ -79,7 +79,7 @@ export function SummaryComponent(_props: ISummaryComponent) {
const attachments = useAppSelector((state: IRuntimeState) => state.attachments.attachments);
const formComponent = useResolvedNode(componentRef)?.item;
const summaryComponent = useResolvedNode(id)?.item;
const layoutComponent = getLayoutComponentObject(formComponent?.type as ComponentExceptGroupAndSummary);
const layoutComponent = getLayoutComponentObject(formComponent?.type);
const formComponentLegacy = useAppSelector(
(state) =>
(state.formLayout.layouts &&
Expand Down Expand Up @@ -121,7 +121,7 @@ export function SummaryComponent(_props: ISummaryComponent) {
getDisplayFormDataForComponent(
state.formData.formData,
attachments,
formComponent as ILayoutComponent,
formComponent,
state.textResources.resources,
state.optionState.options,
state.formLayout.uiConfig.repeatingGroups,
Expand Down Expand Up @@ -192,9 +192,13 @@ export function SummaryComponent(_props: ISummaryComponent) {
)}
/>
);
} else if (layoutComponent?.getComponentType() === ComponentType.Presentation) {
} else if (
layoutComponent?.getComponentType() === ComponentType.Presentation &&
formComponentLegacy.type !== 'Summary' &&
formComponentLegacy.type !== 'Group'
) {
Comment on lines +196 to +199
Copy link
Member

Choose a reason for hiding this comment

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

I don't think checking that the type is not Summary or Group is necessary here as they do not have a ComponentType, so they would fail because null !== ComponentType.Presentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're probably right! I did that to appease TypeScript, although it's obvious that the extra checks would always pass. I'll look over it once more, hopefully after you merge #940 and I do one more pass over this thing. Thanks for letting me know!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Checked now, and it's still needed. TypeScript doesn't know that layoutComponent and formComponentLegacy is supposed to be the same thing (in many ways), so checking the type of layoutComponent does not narrow the type of formComponentLegacy (which is used in the implementation below). So these checks will make sure to narrow the type of formComponentLegacy so that GenericComponent can render it.

// Render non-input components as normal
return <GenericComponent {...(formComponentLegacy as RenderableGenericComponent)} />;
return <GenericComponent {...formComponentLegacy} />;
}

const displayGrid = display && display.useComponentGrid ? formComponent?.grid : grid;
Expand Down
3 changes: 2 additions & 1 deletion src/components/summary/SummaryComponentSwitch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AttachmentSummaryComponent } from 'src/layout/FileUpload/AttachmentSumm
import { AttachmentWithTagSummaryComponent } from 'src/layout/FileUploadWithTag/AttachmentWithTagSummaryComponent';
import { MapComponentSummary } from 'src/layout/Map/MapComponentSummary';
import { useResolvedNode } from 'src/utils/layout/ExprContext';
import type { ExprUnresolved } from 'src/features/expressions/types';
import type { ILayoutGroup } from 'src/layout/Group/types';
import type { ILayoutComponent } from 'src/layout/layout';
import type { ILayoutCompSummary } from 'src/layout/Summary/types';
Expand All @@ -17,7 +18,7 @@ export interface ISummaryComponentSwitch extends Omit<ILayoutCompSummary, 'type'
onChangeClick: () => void;
changeText: string | null;
};
formComponent?: ILayoutComponent | ILayoutGroup;
formComponent?: ExprUnresolved<ILayoutComponent | ILayoutGroup>;
hasValidationMessages?: boolean;
label?: JSX.Element | JSX.Element[] | null | undefined;
formData?: any;
Expand Down
18 changes: 7 additions & 11 deletions src/components/summary/SummaryGroupComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import { AltinnAppTheme } from 'src/theme/altinnAppTheme';
import { getDisplayFormDataForComponent, getFormDataForComponentInRepeatingGroup } from 'src/utils/formComponentUtils';
import { useResolvedNode } from 'src/utils/layout/ExprContext';
import { getTextFromAppOrDefault } from 'src/utils/textResource';
import type { ExprUnresolved } from 'src/features/expressions/types';
import type { ComponentFromSummary } from 'src/features/form/containers/DisplayGroupContainer';
import type { ILayoutGroup } from 'src/layout/Group/types';
import type { ComponentExceptGroupAndSummary, ILayoutComponent } from 'src/layout/layout';
import type { SummaryDisplayProperties } from 'src/layout/Summary/types';
import type { IRuntimeState } from 'src/types';
import type { AnyNode } from 'src/utils/layout/hierarchy.types';
import type { LayoutNode } from 'src/utils/layout/hierarchy';

export interface ISummaryGroupComponent {
pageRef?: string;
Expand Down Expand Up @@ -89,7 +89,7 @@ export function SummaryGroupComponent({
const node = useResolvedNode(componentRef);
const textResourceBindings = node?.item.textResourceBindings;

const removeExcludedChildren = (n: AnyNode<'resolved'>) =>
const removeExcludedChildren = (n: LayoutNode) =>
!excludedChildren ||
(!excludedChildren.includes(n.item.id) && !excludedChildren.includes(`${n.item.baseComponentId}`));

Expand Down Expand Up @@ -141,11 +141,7 @@ export function SummaryGroupComponent({
const childSummaryComponents = node
.children(undefined, row.index)
.filter(removeExcludedChildren)
.filter(
(node) =>
getLayoutComponentObject(node.item.type as ComponentExceptGroupAndSummary)?.getComponentType() ===
ComponentType.Form,
)
.filter((node) => getLayoutComponentObject(node.item.type)?.getComponentType() === ComponentType.Form)
.map((n) => {
if (n.isHidden(hiddenFields)) {
return;
Expand All @@ -154,7 +150,7 @@ export function SummaryGroupComponent({
const formDataForComponent = getDisplayFormDataForComponent(
formData,
attachments,
n.item as ILayoutComponent,
n.item,
textResources,
options,
repeatingGroups,
Expand Down Expand Up @@ -196,7 +192,7 @@ export function SummaryGroupComponent({
const groupContainer = {
...node.item,
children: [],
} as ILayoutGroup;
} as ExprUnresolved<ILayoutGroup>;

const childSummaryComponents: ComponentFromSummary[] = [];
node
Expand All @@ -214,7 +210,7 @@ export function SummaryGroupComponent({
formDataForComponent = getFormDataForComponentInRepeatingGroup(
formData,
attachments,
n.item as ILayoutComponent,
n.item,
row.index,
groupDataModelBinding,
textResources,
Expand Down
12 changes: 6 additions & 6 deletions src/features/expressions/ExprContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import dot from 'dot-object';

import { ExprRuntimeError, NodeNotFound, NodeNotFoundWithoutContext } from 'src/features/expressions/errors';
import { prettyErrors, prettyErrorsToConsole } from 'src/features/expressions/prettyErrors';
import type { BaseValue, ExprConfig, Expression } from 'src/features/expressions/types';
import type { ExprConfig, Expression } from 'src/features/expressions/types';
import type { IFormData } from 'src/features/form/data';
import type { IApplicationSettings, IInstanceContext } from 'src/types/shared';
import type { LayoutNode, LayoutRootNode } from 'src/utils/layout/hierarchy';
import type { LayoutNode, LayoutPage } from 'src/utils/layout/hierarchy';

export interface ContextDataSources {
instanceContext: IInstanceContext | null;
Expand All @@ -15,7 +15,7 @@ export interface ContextDataSources {
}

export interface PrettyErrorsOptions {
config?: ExprConfig<BaseValue>;
config?: ExprConfig;
introText?: string;
}

Expand All @@ -28,7 +28,7 @@ export class ExprContext {

private constructor(
public expr: Expression,
public node: LayoutNode<any> | LayoutRootNode<any> | NodeNotFoundWithoutContext,
public node: LayoutNode | LayoutPage | NodeNotFoundWithoutContext,
public dataSources: ContextDataSources,
) {}

Expand All @@ -37,7 +37,7 @@ export class ExprContext {
*/
public static withBlankPath(
expr: Expression,
node: LayoutNode<any> | LayoutRootNode<any> | NodeNotFoundWithoutContext,
node: LayoutNode | LayoutPage | NodeNotFoundWithoutContext,
dataSources: ContextDataSources,
): ExprContext {
return new ExprContext(expr, node, dataSources);
Expand All @@ -57,7 +57,7 @@ export class ExprContext {
/**
* Utility function used to get the LayoutNode for this context, or fail if the node was not found
*/
public failWithoutNode(): LayoutNode<any> | LayoutRootNode<any> {
public failWithoutNode(): LayoutNode | LayoutPage {
if (this.node instanceof NodeNotFoundWithoutContext) {
throw new NodeNotFound(this, this.node);
}
Expand Down
9 changes: 5 additions & 4 deletions src/features/expressions/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { NodeNotFoundWithoutContext } from 'src/features/expressions/errors';
import { CONFIG_FOR_ALL_VALUES_IN_OBJ, evalExpr, evalExprInObj } from 'src/features/expressions/index';
import { ExprVal } from 'src/features/expressions/types';
import type { ContextDataSources } from 'src/features/expressions/ExprContext';
import type { ExprConfig } from 'src/features/expressions/types';

describe('Expressions', () => {
it('should return default value if expression evaluates to null', () => {
const config: ExprConfig<'string'> = {
returnType: 'string',
const config: ExprConfig<ExprVal.String> = {
returnType: ExprVal.String,
defaultValue: 'hello world',
resolvePerRow: false,
};
Expand Down Expand Up @@ -60,12 +61,12 @@ describe('Expressions', () => {
input: { obj: ['instanceContext', 'whatever1'], other: ['instanceContext', 'whatever2'] },
config: {
[CONFIG_FOR_ALL_VALUES_IN_OBJ]: {
returnType: 'string',
returnType: ExprVal.String,
defaultValue: 'some-default-result',
resolvePerRow: false,
},
obj: {
returnType: 'string',
returnType: ExprVal.String,
defaultValue: 'default-for-this-one',
resolvePerRow: false,
},
Expand Down
Loading