From 976cfaa3962b774ca4b75c15fe9d9fb51dc6f01e Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 18 Dec 2024 16:20:21 +0100 Subject: [PATCH 01/13] Fixing a few minor issues I found in the Text/Number/Date components --- src/layout/Date/Date.tsx | 6 ++++-- src/layout/Date/DateComponent.tsx | 10 ++++++++-- src/layout/Number/Number.tsx | 3 ++- src/layout/Number/NumberComponent.tsx | 11 +++++++++-- src/layout/Text/Text.tsx | 3 ++- src/layout/Text/TextComponent.tsx | 9 +++++++-- 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/layout/Date/Date.tsx b/src/layout/Date/Date.tsx index 64990c5985..68efb9c23f 100644 --- a/src/layout/Date/Date.tsx +++ b/src/layout/Date/Date.tsx @@ -16,7 +16,8 @@ export const Date = ({ value, format, iconUrl, iconAltText, labelId }: DateProps const parsedValue = parseISO(value); let displayData = parsedValue.toDateString(); if (!isValid(parsedValue)) { - displayData = 'Ugyldig format'; + window.logErrorOnce(`Ugyldig datoformat gitt til Date-komponent: "${value}"`); + displayData = ''; } else if (format) { displayData = formatDate(parsedValue, format); } @@ -30,7 +31,8 @@ export const Date = ({ value, format, iconUrl, iconAltText, labelId }: DateProps alt={iconAltText} /> )} - {displayData} + {labelId && {displayData}} + {!labelId && {displayData}} ); }; diff --git a/src/layout/Date/DateComponent.tsx b/src/layout/Date/DateComponent.tsx index ff7c5af5f3..8545d357b1 100644 --- a/src/layout/Date/DateComponent.tsx +++ b/src/layout/Date/DateComponent.tsx @@ -3,6 +3,7 @@ import React from 'react'; import cn from 'classnames'; import { getLabelId } from 'src/components/label/Label'; +import { useLanguage } from 'src/features/language/useLanguage'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import { Date } from 'src/layout/Date/Date'; import classes from 'src/layout/Date/DateComponent.module.css'; @@ -10,7 +11,12 @@ import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; export const DateComponent = ({ node }: PropsFromGenericComponent<'Date'>) => { - const { textResourceBindings, value, icon, direction, format } = useNodeItem(node); + const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); + const direction = useNodeItem(node, (i) => i.direction) ?? 'horizontal'; + const value = useNodeItem(node, (i) => i.value); + const icon = useNodeItem(node, (i) => i.icon); + const format = useNodeItem(node, (i) => i.format); + const { langAsString } = useLanguage(node); if (!textResourceBindings?.title) { return ( @@ -33,7 +39,7 @@ export const DateComponent = ({ node }: PropsFromGenericComponent<'Date'>) => { diff --git a/src/layout/Number/Number.tsx b/src/layout/Number/Number.tsx index e7d42c49dd..92f3c5d214 100644 --- a/src/layout/Number/Number.tsx +++ b/src/layout/Number/Number.tsx @@ -30,7 +30,8 @@ export const Number = ({ value, formatting, iconUrl, iconAltText, labelId, curre alt={iconAltText} /> )} - {displayData} + {labelId && {displayData}} + {!labelId && {displayData}} ); }; diff --git a/src/layout/Number/NumberComponent.tsx b/src/layout/Number/NumberComponent.tsx index e6b05d3234..2d5f552588 100644 --- a/src/layout/Number/NumberComponent.tsx +++ b/src/layout/Number/NumberComponent.tsx @@ -4,6 +4,7 @@ import cn from 'classnames'; import { getLabelId } from 'src/components/label/Label'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; +import { useLanguage } from 'src/features/language/useLanguage'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import { Number } from 'src/layout/Number/Number'; import classes from 'src/layout/Number/NumberComponent.module.css'; @@ -11,8 +12,14 @@ import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; export const NumberComponent = ({ node }: PropsFromGenericComponent<'Number'>) => { - const { textResourceBindings, value, icon, direction, formatting } = useNodeItem(node); + const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); + const value = useNodeItem(node, (i) => i.value); + const icon = useNodeItem(node, (i) => i.icon); + const direction = useNodeItem(node, (i) => i.direction) ?? 'horizontal'; + const formatting = useNodeItem(node, (i) => i.formatting); + const { langAsString } = useLanguage(node); const currentLanguage = useCurrentLanguage(); + if (isNaN(value)) { return null; } @@ -40,7 +47,7 @@ export const NumberComponent = ({ node }: PropsFromGenericComponent<'Number'>) = value={value} currentLanguage={currentLanguage} iconUrl={icon} - iconAltText={textResourceBindings.title} + iconAltText={langAsString(textResourceBindings.title)} labelId={getLabelId(node.id)} formatting={formatting} /> diff --git a/src/layout/Text/Text.tsx b/src/layout/Text/Text.tsx index 686a24ed62..cb61ba1d06 100644 --- a/src/layout/Text/Text.tsx +++ b/src/layout/Text/Text.tsx @@ -18,6 +18,7 @@ export const Text = ({ value, iconUrl, iconAltText, labelId }: TextProps) => ( alt={iconAltText} /> )} - {value} + {labelId && {value}} + {!labelId && {value}} ); diff --git a/src/layout/Text/TextComponent.tsx b/src/layout/Text/TextComponent.tsx index 9a8fcc66be..1886a1fa6c 100644 --- a/src/layout/Text/TextComponent.tsx +++ b/src/layout/Text/TextComponent.tsx @@ -3,6 +3,7 @@ import React from 'react'; import cn from 'classnames'; import { getLabelId } from 'src/components/label/Label'; +import { useLanguage } from 'src/features/language/useLanguage'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import { Text } from 'src/layout/Text/Text'; import classes from 'src/layout/Text/TextComponent.module.css'; @@ -10,7 +11,11 @@ import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; export const TextComponent = ({ node }: PropsFromGenericComponent<'Text'>) => { - const { textResourceBindings, value, icon, direction } = useNodeItem(node); + const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); + const value = useNodeItem(node, (i) => i.value); + const icon = useNodeItem(node, (i) => i.icon); + const direction = useNodeItem(node, (i) => i.direction) ?? 'horizontal'; + const { langAsString } = useLanguage(node); if (!textResourceBindings?.title) { return ; @@ -28,7 +33,7 @@ export const TextComponent = ({ node }: PropsFromGenericComponent<'Text'>) => { From 1657b7040386a1337ccaa75514746e7102e3142a Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 18 Dec 2024 21:31:16 +0100 Subject: [PATCH 02/13] Initial work, copying from other presentation components --- src/features/options/OptionsPlugin.tsx | 14 +++-- src/features/options/StoreOptionsInNode.tsx | 5 +- src/layout/Option/Option.module.css | 18 ++++++ src/layout/Option/OptionComponent.tsx | 67 +++++++++++++++++++++ src/layout/Option/OptionSummary.tsx | 42 +++++++++++++ src/layout/Option/config.ts | 31 ++++++++++ src/layout/Option/index.tsx | 50 +++++++++++++++ 7 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 src/layout/Option/Option.module.css create mode 100644 src/layout/Option/OptionComponent.tsx create mode 100644 src/layout/Option/OptionSummary.tsx create mode 100644 src/layout/Option/config.ts create mode 100644 src/layout/Option/index.tsx diff --git a/src/features/options/OptionsPlugin.tsx b/src/features/options/OptionsPlugin.tsx index 3e11a9280a..20190922c5 100644 --- a/src/features/options/OptionsPlugin.tsx +++ b/src/features/options/OptionsPlugin.tsx @@ -12,6 +12,7 @@ interface Config { componentType: CompTypes; expectedFromExternal: SupportsPreselection extends true ? ISelectionComponentFull : ISelectionComponent; settings: { + allowsEffects?: boolean; supportsPreselection: SupportsPreselection; type: OptionsValueType; }; @@ -22,6 +23,7 @@ interface Config { } interface ExternalConfig { + allowsEffects?: boolean; supportsPreselection: boolean; type: OptionsValueType; } @@ -49,10 +51,6 @@ export class OptionsPlugin extends NodeDefPlugin extends NodeDefPlugin`; + const allowsEffects = this.settings!.allowsEffects ?? true; + + return ` + <${StoreOptionsInNode} + valueType={'${this.settings!.type}'} + allowEffects={${allowsEffects ? 'true' : 'false'}} + />`.trim(); } } diff --git a/src/features/options/StoreOptionsInNode.tsx b/src/features/options/StoreOptionsInNode.tsx index 0191cb3181..1c5dd70b4c 100644 --- a/src/features/options/StoreOptionsInNode.tsx +++ b/src/features/options/StoreOptionsInNode.tsx @@ -19,6 +19,7 @@ import type { LayoutNode } from 'src/utils/layout/LayoutNode'; interface GeneratorOptionProps { valueType: OptionsValueType; + allowEffects: boolean; } export function StoreOptionsInNode(props: GeneratorOptionProps) { @@ -32,7 +33,7 @@ export function StoreOptionsInNode(props: GeneratorOptionProps) { ); } -function StoreOptionsInNodeWorker({ valueType }: GeneratorOptionProps) { +function StoreOptionsInNodeWorker({ valueType, allowEffects }: GeneratorOptionProps) { const item = GeneratorInternal.useIntermediateItem() as CompIntermediate>; const node = GeneratorInternal.useParent() as LayoutNode>; const dataModelBindings = item.dataModelBindings as IDataModelBindingsOptionsSimple | undefined; @@ -55,7 +56,7 @@ function StoreOptionsInNodeWorker({ valueType }: GeneratorOptionProps) { NodesStateQueue.useSetNodeProp({ node, prop: 'options', value: options }, !hasBeenSet && !isFetching); NodesStateQueue.useSetNodeProp({ node, prop: 'isFetchingOptions', value: isFetching }, !hasBeenSet); - if (isFetching || !hasBeenSet) { + if (isFetching || !hasBeenSet || !allowEffects) { // No need to run effects while fetching or if the data has not been set yet return false; } diff --git a/src/layout/Option/Option.module.css b/src/layout/Option/Option.module.css new file mode 100644 index 0000000000..155150cafb --- /dev/null +++ b/src/layout/Option/Option.module.css @@ -0,0 +1,18 @@ +.icon { + width: 24px; + margin-right: 12px; +} + +.optionComponent { + display: flex; + justify-content: space-between; + margin: 0; + width: 100%; +} + +.horizontal { + flex-direction: row; +} +.vertical { + flex-direction: column; +} diff --git a/src/layout/Option/OptionComponent.tsx b/src/layout/Option/OptionComponent.tsx new file mode 100644 index 0000000000..a2a16f5039 --- /dev/null +++ b/src/layout/Option/OptionComponent.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import cn from 'classnames'; + +import { getLabelId } from 'src/components/label/Label'; +import { Lang } from 'src/features/language/Lang'; +import { useLanguage } from 'src/features/language/useLanguage'; +import { useGetOptions } from 'src/features/options/useGetOptions'; +import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; +import classes from 'src/layout/Option/Option.module.css'; +import { useNodeItem } from 'src/utils/layout/useNodeItem'; +import type { PropsFromGenericComponent } from 'src/layout'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; + +export const OptionComponent = ({ node }: PropsFromGenericComponent<'Option'>) => { + const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); + const direction = useNodeItem(node, (i) => i.direction); + + if (!textResourceBindings?.title) { + return ; + } + + return ( + + + + ); +}; + +interface TextProps { + node: LayoutNode<'Option'>; +} + +function Text({ node }: TextProps) { + const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); + const icon = useNodeItem(node, (i) => i.icon); + const value = useNodeItem(node, (i) => i.value); + const { options, isFetching } = useGetOptions(node, 'single'); + const { langAsString } = useLanguage(node); + const selectedOption = options.find((option) => option.value === value); + + if (isFetching) { + return null; + } + + return ( + <> + {icon && textResourceBindings?.title && ( + {langAsString(textResourceBindings.title)} + )} + + + + + ); +} diff --git a/src/layout/Option/OptionSummary.tsx b/src/layout/Option/OptionSummary.tsx new file mode 100644 index 0000000000..390ebb158b --- /dev/null +++ b/src/layout/Option/OptionSummary.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +import { Lang } from 'src/features/language/Lang'; +import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/unifiedValidationsForNode'; +import { validationsOfSeverity } from 'src/features/validation/utils'; +import { SingleValueSummary } from 'src/layout/Summary2/CommonSummaryComponents/SingleValueSummary'; +import { useNodeItem } from 'src/utils/layout/useNodeItem'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; + +type OptionComponentSummaryProps = { + componentNode: LayoutNode<'Option'>; + isCompact?: boolean; + emptyFieldText?: string; +}; + +export const OptionSummary = ({ componentNode, isCompact, emptyFieldText }: OptionComponentSummaryProps) => { + const displayData = componentNode.def.useDisplayData(componentNode); + const validations = useUnifiedValidationsForNode(componentNode); + const errors = validationsOfSeverity(validations, 'error'); + const title = useNodeItem(componentNode, (i) => i.textResourceBindings?.title); + const direction = useNodeItem(componentNode, (i) => i.direction); + const compact = (direction === 'horizontal' && isCompact == undefined) || isCompact; + + return ( + + ) + } + displayData={displayData} + errors={errors} + componentNode={componentNode} + hideEditButton + isCompact={compact} + emptyFieldText={emptyFieldText} + /> + ); +}; diff --git a/src/layout/Option/config.ts b/src/layout/Option/config.ts new file mode 100644 index 0000000000..8ddcaa533b --- /dev/null +++ b/src/layout/Option/config.ts @@ -0,0 +1,31 @@ +import { CG } from 'src/codegen/CG'; +import { ExprVal } from 'src/features/expressions/types'; +import { OptionsPlugin } from 'src/features/options/OptionsPlugin'; +import { CompCategory } from 'src/layout/common'; + +export const Config = new CG.component({ + category: CompCategory.Presentation, + capabilities: { + renderInTable: true, + renderInButtonGroup: false, + renderInAccordion: true, + renderInAccordionGroup: false, + renderInTabs: true, + renderInCards: true, + renderInCardsMedia: false, + }, + functionality: { + customExpressions: true, + }, +}) + .extendTextResources(CG.common('TRBLabel')) + .addPlugin( + new OptionsPlugin({ + supportsPreselection: false, + type: 'single', + allowsEffects: false, + }), + ) + .addProperty(new CG.prop('value', new CG.expr(ExprVal.String))) + .addProperty(new CG.prop('direction', new CG.enum('horizontal', 'vertical').optional({ default: 'horizontal' }))) + .addProperty(new CG.prop('icon', new CG.str().optional().addExample('https://example.com/icon.svg'))); diff --git a/src/layout/Option/index.tsx b/src/layout/Option/index.tsx new file mode 100644 index 0000000000..9185a052ed --- /dev/null +++ b/src/layout/Option/index.tsx @@ -0,0 +1,50 @@ +import React, { forwardRef } from 'react'; +import type { JSX } from 'react'; + +import { useDisplayDataProps } from 'src/features/displayData/useDisplayData'; +import { getSelectedValueToText } from 'src/features/options/getSelectedValueToText'; +import { OptionDef } from 'src/layout/Option/config.def.generated'; +import { OptionComponent } from 'src/layout/Option/OptionComponent'; +import { OptionSummary } from 'src/layout/Option/OptionSummary'; +import type { DisplayDataProps } from 'src/features/displayData'; +import type { PropsFromGenericComponent } from 'src/layout'; +import type { ExprResolver } from 'src/layout/LayoutComponent'; +import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; + +export class Option extends OptionDef { + getDisplayData( + node: LayoutNode<'Option'>, + { nodeDataSelector, optionsSelector, langTools }: DisplayDataProps, + ): string { + const value = nodeDataSelector((picker) => picker(node)?.item?.value, [node]) ?? ''; + const { options } = optionsSelector(node); + return getSelectedValueToText(value, langTools, options) || ''; + } + + useDisplayData(node: LayoutNode<'Option'>): string { + const displayDataProps = useDisplayDataProps(); + return this.getDisplayData(node, displayDataProps); + } + + render = forwardRef>(function LayoutComponentOptionRender(props, _) { + return ; + }); + + renderSummary2(props: Summary2Props<'Option'>): JSX.Element | null { + return ( + + ); + } + + evalExpressions(props: ExprResolver<'Option'>) { + return { + ...this.evalDefaultExpressions(props), + value: props.evalStr(props.item.value, ''), + }; + } +} From cf28db86413552b2742d721a683cdeaa7d23971a Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 8 Jan 2025 16:07:28 +0100 Subject: [PATCH 03/13] Adding nicer css for presenting the help text and description --- src/layout/Option/Option.module.css | 11 +++++++++++ src/layout/Option/OptionComponent.tsx | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/layout/Option/Option.module.css b/src/layout/Option/Option.module.css index 155150cafb..95feca8844 100644 --- a/src/layout/Option/Option.module.css +++ b/src/layout/Option/Option.module.css @@ -16,3 +16,14 @@ .vertical { flex-direction: column; } + +.optionLabelContainer { + display: flex; + column-gap: 8px; + flex-wrap: wrap; + align-items: center; +} + +.optionDescription { + flex: 0 0 100%; +} diff --git a/src/layout/Option/OptionComponent.tsx b/src/layout/Option/OptionComponent.tsx index a2a16f5039..634ea2dd74 100644 --- a/src/layout/Option/OptionComponent.tsx +++ b/src/layout/Option/OptionComponent.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { HelpText } from '@digdir/designsystemet-react'; import cn from 'classnames'; import { getLabelId } from 'src/components/label/Label'; @@ -59,8 +60,21 @@ function Text({ node }: TextProps) { alt={langAsString(textResourceBindings.title)} /> )} - + + {selectedOption?.helpText && ( + + + + )} + {selectedOption?.description && ( + + + + )} ); From 2c04a82ad0ad120da8c0f5728a22f7a0f9c9056c Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 8 Jan 2025 17:00:47 +0100 Subject: [PATCH 04/13] Adding a somewhat simple cypress test --- .../integration/frontend-test/group-pets.ts | 64 +++++++++++++------ test/e2e/pageobjects/app-frontend.ts | 43 +++++++------ test/e2e/support/custom.ts | 6 ++ 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/test/e2e/integration/frontend-test/group-pets.ts b/test/e2e/integration/frontend-test/group-pets.ts index 4c763b5568..95fded44ec 100644 --- a/test/e2e/integration/frontend-test/group-pets.ts +++ b/test/e2e/integration/frontend-test/group-pets.ts @@ -10,12 +10,12 @@ interface PetProps { } function addPet({ species, name, age }: PetProps) { - cy.get(appFrontend.pets.group.editContainer._).should('not.exist'); - cy.get(appFrontend.pets.group.addButton).click(); - cy.dsSelect(appFrontend.pets.group.editContainer.species, species); - cy.get(appFrontend.pets.group.editContainer.name).type(name); - cy.get(appFrontend.pets.group.editContainer.age).type(age.toString()); - cy.get(appFrontend.pets.group.editContainer.saveAndClose).clickAndGone(); + cy.get(appFrontend.pets.group().editContainer._).should('not.exist'); + cy.get(appFrontend.pets.group().addButton).click(); + cy.dsSelect(appFrontend.pets.group().editContainer.species, species); + cy.get(appFrontend.pets.group().editContainer.name).type(name); + cy.get(appFrontend.pets.group().editContainer.age).type(age.toString()); + cy.get(appFrontend.pets.group().editContainer.saveAndClose).clickAndGone(); } function assertPetOrder(pets: PetProps[], callNum: number, editingIndex?: number) { @@ -27,7 +27,7 @@ function assertPetOrder(pets: PetProps[], callNum: number, editingIndex?: number cy.waitUntilNodesReady(); const visibleLength = pets.filter((pet) => pet.visible === true || pet.visible === undefined).length; - cy.get(appFrontend.pets.group.tableRows).should( + cy.get(appFrontend.pets.group().tableRows).should( 'have.length', editingIndex === undefined ? visibleLength : visibleLength + 1, ); @@ -44,11 +44,11 @@ function assertPetOrder(pets: PetProps[], callNum: number, editingIndex?: number return; } const { species, name } = pet; - cy.get(appFrontend.pets.group.tableRow(index).species).should('have.value', species); + cy.get(appFrontend.pets.group().tableRow(index).species).should('have.value', species); if (index === editingIndex) { - cy.get(appFrontend.pets.group.editContainer.name).should('have.value', name); + cy.get(appFrontend.pets.group().editContainer.name).should('have.value', name); } else { - cy.get(appFrontend.pets.group.tableRow(index).name).should('have.text', name); + cy.get(appFrontend.pets.group().tableRow(index).name).should('have.text', name); } }); } @@ -107,13 +107,13 @@ describe('Group (Pets)', () => { assertPetOrder(pets3, 3); // Sort when having opened one row for editing - cy.get(appFrontend.pets.group.tableRow(3).editButton).click(); + cy.get(appFrontend.pets.group().tableRow(3).editButton).click(); assertPetOrder(pets3, 4, 3); - cy.get(appFrontend.pets.group.editContainer.species).should('have.value', 'Fisk'); - cy.get(appFrontend.pets.group.editContainer.name).should('have.value', 'Siri Spinat'); - cy.get(appFrontend.pets.group.editContainer.sortOrder).should('have.value', 'Alder (9-1)'); - cy.dsSelect(appFrontend.pets.group.editContainer.sortOrder, 'Art (Å-A)'); - cy.get(appFrontend.pets.group.editContainer.sortButton).click(); + cy.get(appFrontend.pets.group().editContainer.species).should('have.value', 'Fisk'); + cy.get(appFrontend.pets.group().editContainer.name).should('have.value', 'Siri Spinat'); + cy.get(appFrontend.pets.group().editContainer.sortOrder).should('have.value', 'Alder (9-1)'); + cy.dsSelect(appFrontend.pets.group().editContainer.sortOrder, 'Art (Å-A)'); + cy.get(appFrontend.pets.group().editContainer.sortButton).click(); const pets4 = structuredClone(pets3) .sort((a, b) => { @@ -125,9 +125,9 @@ describe('Group (Pets)', () => { assertPetOrder(pets4, 5, 5); // We should still be editing the same row - cy.get(appFrontend.pets.group.editContainer.species).should('have.value', 'Fisk'); - cy.get(appFrontend.pets.group.editContainer.name).should('have.value', 'Siri Spinat'); - cy.get(appFrontend.pets.group.editContainer.saveAndClose).clickAndGone(); + cy.get(appFrontend.pets.group().editContainer.species).should('have.value', 'Fisk'); + cy.get(appFrontend.pets.group().editContainer.name).should('have.value', 'Siri Spinat'); + cy.get(appFrontend.pets.group().editContainer.saveAndClose).clickAndGone(); // Hiding one row via checkboxes cy.get(appFrontend.pets.hide._).findByRole('checkbox', { name: 'Fisk med navn Siri Spinat (3 år)' }).check(); @@ -147,7 +147,7 @@ describe('Group (Pets)', () => { cy.snapshot('pets'); - cy.get(appFrontend.pets.group.tableRow(1).deleteButton).click(); + cy.get(appFrontend.pets.group().tableRow(1).deleteButton).click(); const pets7 = structuredClone(pets6); const deletedIndex = pets7.findIndex((pet) => pet.name === 'Birte Blomkål' && pet.age === 2); pets7.splice(deletedIndex, 1); @@ -157,6 +157,30 @@ describe('Group (Pets)', () => { cy.get(appFrontend.errorReport).should('not.exist'); }); + it('should handle switching to using the Option-component in a table', () => { + cy.goto('group'); + cy.gotoNavPage('Kjæledyr'); + cy.get(appFrontend.pets.decisionPanel.autoPetsButton).click(); + cy.get(appFrontend.pets.useOptions).findByRole('radio', { name: /Ja/i }).check(); + cy.get(appFrontend.pets.group(true).tableRow(0).species).should('not.exist'); + cy.get(appFrontend.pets.group(true).tableRow(0).speciesOption).should('contain.text', 'Hund'); // Label + cy.get(appFrontend.pets.group(true).tableRow(0).speciesOption).should( + 'contain.text', + 'Hunder er menneskets beste venn, sier mange. Andre liker katter bedre.', // Help text + ); + cy.get(appFrontend.pets.group(true).tableRow(0).speciesOption).should( + 'contain.text', + 'Pelskledd og lojal mot mennesker', // Description + ); + cy.get(appFrontend.pets.group(true).tableRow(0).editButton).click(); + cy.dsSelect(appFrontend.pets.group().editContainer.species, 'Katt'); + cy.get(appFrontend.pets.group(true).tableRow(0).speciesOption).should('contain.text', 'Katt'); + cy.testWcag(); + cy.gotoNavPage('option-comp'); + cy.get('#header-collection1').should('be.visible'); + cy.testWcag(); + }); + it('innerGrid should be ignored when editing in table', () => { cy.interceptLayout('group', (comp) => { if (comp.id === 'pets' && comp.type === 'RepeatingGroup') { diff --git a/test/e2e/pageobjects/app-frontend.ts b/test/e2e/pageobjects/app-frontend.ts index 2c788ef892..8aea50aa12 100644 --- a/test/e2e/pageobjects/app-frontend.ts +++ b/test/e2e/pageobjects/app-frontend.ts @@ -261,30 +261,35 @@ export class AppFrontend { autoPetsButton: '#custom-button-generatePets', autoFarmAnimalsButton: '#custom-button-generateWholeFarm', }, - group: { - table: '#group-pets-table-body', - tableRows: '#group-pets-table-body tr', - tableRow: (idx: number) => ({ - species: `#pet-species-${idx}`, - name: `#group-pets-table-body tr[data-row-num=${idx}] td:nth-child(2)`, - editButton: `#group-pets-table-body tr[data-row-num=${idx}] button:contains("Rediger")`, - deleteButton: `#group-pets-table-body tr[data-row-num=${idx}] button:contains("Slett")`, - }), - addButton: '#add-button-pets', - editContainer: { - _: '[data-testid=group-edit-container]', - species: '[data-testid=group-edit-container] [id^="pet-species"]', - name: '[data-testid=group-edit-container] [id^="pet-name"]', - age: '[data-testid=group-edit-container] [id^="pet-age"]', - sortOrder: '[data-testid=group-edit-container] [id^="futureSortOrder-inside"]', - sortButton: '[data-testid=group-edit-container] [id^="custom-button-sortOrderButton-inside"]', - saveAndClose: '[data-testid=group-edit-container] #save-button-pets', - }, + group: (withOptionComponent = false) => { + const id = withOptionComponent ? 'pets-with-option' : 'pets'; + return { + table: `#group-${id}-table-body`, + tableRows: `#group-${id}-table-body tr`, + tableRow: (idx: number) => ({ + species: `#pet-species-${idx}`, // Only in non-option mode + speciesOption: `#form-content-pet-species-option-${idx}`, // Only in option mode + name: `#group-${id}-table-body tr[data-row-num=${idx}] td:nth-child(2)`, + editButton: `#group-${id}-table-body tr[data-row-num=${idx}] button:contains("Rediger")`, + deleteButton: `#group-${id}-table-body tr[data-row-num=${idx}] button:contains("Slett")`, + }), + addButton: `#add-button-${id}`, + editContainer: { + _: '[data-testid=group-edit-container]', + species: '[data-testid=group-edit-container] [id^="pet-species"]', + name: '[data-testid=group-edit-container] [id^="pet-name"]', + age: '[data-testid=group-edit-container] [id^="pet-age"]', + sortOrder: '[data-testid=group-edit-container] [id^="futureSortOrder-inside"]', + sortButton: '[data-testid=group-edit-container] [id^="custom-button-sortOrderButton-inside"]', + saveAndClose: '[data-testid=group-edit-container] #save-button-pets', + }, + }; }, hide: { _: '#form-content-hiddenPets', all: '#form-content-hiddenPets input[type=checkbox]', }, + useOptions: '#form-content-useOptionComponent', sortOutside: { sortOrder: '#futureSortOrder-outside', sortButton: '#custom-button-sortOrderButton-outside', diff --git a/test/e2e/support/custom.ts b/test/e2e/support/custom.ts index 271c86eeb5..abb147e8f3 100644 --- a/test/e2e/support/custom.ts +++ b/test/e2e/support/custom.ts @@ -167,6 +167,12 @@ const knownWcagViolations: KnownViolation[] = [ id: 'color-contrast', nodeLength: 1, }, + { + spec: 'frontend-test/group-pets.ts', + test: 'should handle switching to using the Option-component in a table', + id: 'color-contrast', // Delete button in the RepeatingGroup on rows with errors + nodeLength: 2, + }, { spec: 'frontend-test/hide-row-in-group.ts', test: 'should be possible to hide rows when "Endre fra" is greater or equals to [...]', From e7752405614c9e9c8f9032b9bdcc0ef7e0e4f08a Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 9 Jan 2025 13:02:25 +0100 Subject: [PATCH 05/13] Fixes for things I forgot: Adding displayValue test, also handling missing label in Option component (like I've made sure to do in Text, Date and Number) --- .../functions/displayValue/option.json | 41 +++++++++++++++++++ src/layout/Option/OptionComponent.tsx | 18 +++++--- 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/features/expressions/shared-tests/functions/displayValue/option.json diff --git a/src/features/expressions/shared-tests/functions/displayValue/option.json b/src/features/expressions/shared-tests/functions/displayValue/option.json new file mode 100644 index 0000000000..a333bfc127 --- /dev/null +++ b/src/features/expressions/shared-tests/functions/displayValue/option.json @@ -0,0 +1,41 @@ +{ + "name": "Display value of Text component", + "expression": [ + "displayValue", + "valg" + ], + "context": { + "component": "valg", + "currentLayout": "Page" + }, + "expects": "Pål", + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "valg", + "type": "Option", + "value": ["dataModel", "Skjema.Valg"], + "options": [ + { + "value": "1", + "label": "Per" + }, + { + "value": "2", + "label": "Pål" + } + ] + } + ] + } + } + }, + "dataModel": { + "Skjema": { + "Valg": "2" + } + } +} diff --git a/src/layout/Option/OptionComponent.tsx b/src/layout/Option/OptionComponent.tsx index 634ea2dd74..5804bbf651 100644 --- a/src/layout/Option/OptionComponent.tsx +++ b/src/layout/Option/OptionComponent.tsx @@ -18,7 +18,12 @@ export const OptionComponent = ({ node }: PropsFromGenericComponent<'Option'>) = const direction = useNodeItem(node, (i) => i.direction); if (!textResourceBindings?.title) { - return ; + return ( + + ); } return ( @@ -30,23 +35,26 @@ export const OptionComponent = ({ node }: PropsFromGenericComponent<'Option'>) = className: cn(classes.optionComponent, direction === 'vertical' ? classes.vertical : classes.horizontal), }} > - + ); }; interface TextProps { node: LayoutNode<'Option'>; + usingLabel: boolean; } -function Text({ node }: TextProps) { +function Text({ node, usingLabel }: TextProps) { const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); const icon = useNodeItem(node, (i) => i.icon); const value = useNodeItem(node, (i) => i.value); const { options, isFetching } = useGetOptions(node, 'single'); const { langAsString } = useLanguage(node); const selectedOption = options.find((option) => option.value === value); - if (isFetching) { return null; } @@ -61,7 +69,7 @@ function Text({ node }: TextProps) { /> )} From a5088ce70b826bb255164d6784dbf5bb88d7893b Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 9 Jan 2025 13:02:50 +0100 Subject: [PATCH 06/13] Fixing all-process-steps now that we have new data in the data model --- test/e2e/integration/frontend-test/all-process-steps.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/integration/frontend-test/all-process-steps.ts b/test/e2e/integration/frontend-test/all-process-steps.ts index 32b16a9169..258dfcb266 100644 --- a/test/e2e/integration/frontend-test/all-process-steps.ts +++ b/test/e2e/integration/frontend-test/all-process-steps.ts @@ -409,6 +409,7 @@ const knownDataModels: { [key: string]: unknown } = { ShiftingOptions: null, FilteredOptions: null, LinkedHidden: null, + PetsUseOptionComponent: null, }, 'nested-group': { skjemanummer: 1603, From 3987d45067bf13027d82683361c6de5663683a72 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 9 Jan 2025 13:29:05 +0100 Subject: [PATCH 07/13] Adding description and helpText to make sure those don't show up in displayValue --- .../shared-tests/functions/displayValue/option.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/features/expressions/shared-tests/functions/displayValue/option.json b/src/features/expressions/shared-tests/functions/displayValue/option.json index a333bfc127..4ecfb941dd 100644 --- a/src/features/expressions/shared-tests/functions/displayValue/option.json +++ b/src/features/expressions/shared-tests/functions/displayValue/option.json @@ -21,11 +21,15 @@ "options": [ { "value": "1", - "label": "Per" + "label": "Per", + "description": "Hello world", + "helpText": "This is a help text" }, { "value": "2", - "label": "Pål" + "label": "Pål", + "description": "Hello world", + "helpText": "This is a help text" } ] } From dfed9581bff2ff9cd2d3808b8b76f550601c6a8d Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 9 Jan 2025 13:30:06 +0100 Subject: [PATCH 08/13] Simplifying and making sure not to output the Heading for a GroupSummary when there is no heading (this empty header was an accessibility error in Wave) --- src/layout/Group/GroupSummary.tsx | 60 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/layout/Group/GroupSummary.tsx b/src/layout/Group/GroupSummary.tsx index 9682be8c0a..3116b70962 100644 --- a/src/layout/Group/GroupSummary.tsx +++ b/src/layout/Group/GroupSummary.tsx @@ -33,18 +33,6 @@ function getHeadingLevel(hierarchyLevel: number): HeadingLevel { } } -const ChildComponents = ({ componentNode, hierarchyLevel, summaryOverride }: GroupComponentSummaryProps) => { - const childComponents = useNodeItem(componentNode, (i) => i.childComponents); - return childComponents.map((childId) => ( - - )); -}; - function ChildComponent({ id, hierarchyLevel, @@ -84,32 +72,40 @@ export const GroupSummary = ({ componentNode, hierarchyLevel = 0, summaryOverrid const isNestedGroup = hierarchyLevel > 0; const dataTestId = hierarchyLevel > 0 ? `summary-group-component-${hierarchyLevel}` : 'summary-group-component'; + const childComponents = useNodeItem(componentNode, (i) => i.childComponents); return (
- - - - - - + {summaryTitle || title ? ( + + + + ) : null} + {childComponents.length ? ( + + {childComponents.map((childId) => ( + + ))} + + ) : null}
); }; From 58e6a533f54dd645f24f5817447f9ee43012d4c3 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 9 Jan 2025 13:30:37 +0100 Subject: [PATCH 09/13] Summary2 says 'target.type' defaults to 'component', but it actually defaulted to show nothing --- src/layout/Summary2/SummaryComponent2/SummaryComponent2.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/layout/Summary2/SummaryComponent2/SummaryComponent2.tsx b/src/layout/Summary2/SummaryComponent2/SummaryComponent2.tsx index 238acd1ccb..6392ea5f2e 100644 --- a/src/layout/Summary2/SummaryComponent2/SummaryComponent2.tsx +++ b/src/layout/Summary2/SummaryComponent2/SummaryComponent2.tsx @@ -30,9 +30,8 @@ function SummaryBody({ target }: SummaryBodyProps) { return ; } - if (target.type === 'component') { - return ; - } + // Component is the default + return ; } export function SummaryComponent2({ summaryNode }: ISummaryComponent2) { From f8585f9cc60e07288eeb106c98068fae64eace78 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Fri, 24 Jan 2025 13:03:26 +0100 Subject: [PATCH 10/13] Fixing name in shared test --- .../shared-tests/functions/displayValue/option.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/features/expressions/shared-tests/functions/displayValue/option.json b/src/features/expressions/shared-tests/functions/displayValue/option.json index 4ecfb941dd..43e94b8cb1 100644 --- a/src/features/expressions/shared-tests/functions/displayValue/option.json +++ b/src/features/expressions/shared-tests/functions/displayValue/option.json @@ -1,9 +1,6 @@ { - "name": "Display value of Text component", - "expression": [ - "displayValue", - "valg" - ], + "name": "Display value of Option component", + "expression": ["displayValue", "valg"], "context": { "component": "valg", "currentLayout": "Page" From 49c05296705720538a9eba9b0615635981decb83 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Fri, 24 Jan 2025 13:20:33 +0100 Subject: [PATCH 11/13] Whoops, these now need to have a certain file name --- .../functions/displayValue/{option.json => type-Option.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/features/expressions/shared-tests/functions/displayValue/{option.json => type-Option.json} (100%) diff --git a/src/features/expressions/shared-tests/functions/displayValue/option.json b/src/features/expressions/shared-tests/functions/displayValue/type-Option.json similarity index 100% rename from src/features/expressions/shared-tests/functions/displayValue/option.json rename to src/features/expressions/shared-tests/functions/displayValue/type-Option.json From 96cb1a5630adb99c900df85b039af73f544301c5 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Fri, 24 Jan 2025 13:21:04 +0100 Subject: [PATCH 12/13] This test failed somehow? I thought this would default to be null, but maybe not --- test/e2e/integration/frontend-test/all-process-steps.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/integration/frontend-test/all-process-steps.ts b/test/e2e/integration/frontend-test/all-process-steps.ts index 258dfcb266..32b16a9169 100644 --- a/test/e2e/integration/frontend-test/all-process-steps.ts +++ b/test/e2e/integration/frontend-test/all-process-steps.ts @@ -409,7 +409,6 @@ const knownDataModels: { [key: string]: unknown } = { ShiftingOptions: null, FilteredOptions: null, LinkedHidden: null, - PetsUseOptionComponent: null, }, 'nested-group': { skjemanummer: 1603, From d6ddf41c4f5a1a816d02bf38b04e516bb381768c Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Fri, 24 Jan 2025 13:47:14 +0100 Subject: [PATCH 13/13] [no ci] Ok, the failing test last time was because of a late deploy to tt02 I guess --- test/e2e/integration/frontend-test/all-process-steps.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/integration/frontend-test/all-process-steps.ts b/test/e2e/integration/frontend-test/all-process-steps.ts index 32b16a9169..258dfcb266 100644 --- a/test/e2e/integration/frontend-test/all-process-steps.ts +++ b/test/e2e/integration/frontend-test/all-process-steps.ts @@ -409,6 +409,7 @@ const knownDataModels: { [key: string]: unknown } = { ShiftingOptions: null, FilteredOptions: null, LinkedHidden: null, + PetsUseOptionComponent: null, }, 'nested-group': { skjemanummer: 1603,