diff --git a/src/app-components/Label/Label.tsx b/src/app-components/Label/Label.tsx index 3e082dc60e..4fd71bd289 100644 --- a/src/app-components/Label/Label.tsx +++ b/src/app-components/Label/Label.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import type { JSX, PropsWithChildren, ReactElement } from 'react'; import { Label as DesignsystemetLabel } from '@digdir/designsystemet-react'; @@ -22,19 +22,22 @@ type LabelProps = { style?: DesignsystemetLabelProps['style']; }; -export function Label({ - label, - htmlFor, - required, - requiredIndicator, - optionalIndicator, - help, - description, - className, - grid, - style, - children, -}: PropsWithChildren) { +export const Label = forwardRef>(function Label( + { + label, + htmlFor, + required, + requiredIndicator, + optionalIndicator, + help, + description, + className, + grid, + style, + children, + }, + ref, +) { if (!label) { return children; } @@ -52,6 +55,7 @@ export function Label({ ); -} +}); diff --git a/src/components/label/LabelContent.tsx b/src/components/label/LabelContent.tsx index 964f2eff7a..4bf89cfc5e 100644 --- a/src/components/label/LabelContent.tsx +++ b/src/components/label/LabelContent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import cn from 'classnames'; @@ -23,16 +23,10 @@ export type LabelContentProps = Readonly<{ labelSettings?: ILabelSettings; }> & { className?: string }; -export function LabelContent({ - componentId, - label, - description, - required, - readOnly, - help, - labelSettings, - className, -}: LabelContentProps) { +export const LabelContent = forwardRef(function LabelContent( + { componentId, label, description, required, readOnly, help, labelSettings, className }, + ref, +) { const { overrideDisplay } = useFormComponentCtx() ?? {}; const { elementAsString } = useLanguage(); @@ -41,7 +35,10 @@ export function LabelContent({ } return ( - + {typeof label === 'string' ? : label} @@ -70,4 +67,4 @@ export function LabelContent({ )} ); -} +}); diff --git a/src/features/alertOnChange/DeleteWarningPopover.module.css b/src/features/alertOnChange/DeleteWarningPopover.module.css index 73cb5cde9e..eb3a485043 100644 --- a/src/features/alertOnChange/DeleteWarningPopover.module.css +++ b/src/features/alertOnChange/DeleteWarningPopover.module.css @@ -1,3 +1,6 @@ +.popoverContent { + z-index: 1700; +} .popoverButtonContainer { display: flex; flex-direction: row; diff --git a/src/features/alertOnChange/DeleteWarningPopover.tsx b/src/features/alertOnChange/DeleteWarningPopover.tsx index 3ef5461ee2..d4c037210a 100644 --- a/src/features/alertOnChange/DeleteWarningPopover.tsx +++ b/src/features/alertOnChange/DeleteWarningPopover.tsx @@ -35,7 +35,7 @@ export function DeleteWarningPopover({ onOpenChange={() => setOpen(!open)} > {children} - +
{messageText}
diff --git a/src/layout/List/config.ts b/src/layout/List/config.ts index 9ab21b985c..329c44d7ab 100644 --- a/src/layout/List/config.ts +++ b/src/layout/List/config.ts @@ -36,11 +36,21 @@ export const Config = new CG.component({ ) .optional(), ), + new CG.prop( + 'checked', + new CG.dataModelBinding() + .setTitle('checked') + .setDescription( + 'If deletionStrategy=soft and group is set, this value points to where you want to save deleted status.', + ) + .optional(), + ), ) .optional() .additionalProperties(new CG.dataModelBinding().optional()) .exportAs('IDataModelBindingsForList'), ) + .addProperty(new CG.prop('deletionStrategy', new CG.enum('soft', 'hard').optional())) .addProperty( new CG.prop( 'tableHeaders', diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index d3e24df143..68024ae6ca 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -6,6 +6,7 @@ import dot from 'dot-object'; import { lookupErrorAsText } from 'src/features/datamodel/lookupErrorAsText'; import { useDisplayData } from 'src/features/displayData/useDisplayData'; import { evalQueryParameters } from 'src/features/options/evalQueryParameters'; +import { ObjectToGroupLayoutValidator } from 'src/features/saveToGroup/ObjectToGroupLayoutValidator'; import { ListDef } from 'src/layout/List/config.def.generated'; import { ListComponent } from 'src/layout/List/ListComponent'; import { ListSummary } from 'src/layout/List/ListSummary'; @@ -17,6 +18,7 @@ import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; import type { IDataModelReference } from 'src/layout/common.generated'; +import type { NodeValidationProps } from 'src/layout/layout'; import type { ExprResolver, SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; @@ -31,6 +33,7 @@ export class List extends ListDef { useDisplayData(nodeId: string): string { const dmBindings = NodesInternal.useNodeDataWhenType(nodeId, 'List', (data) => data.layout.dataModelBindings); const groupBinding = dmBindings?.group; + const checkedBinding = dmBindings?.checked; const summaryBinding = NodesInternal.useNodeDataWhenType(nodeId, 'List', (data) => data.item?.summaryBinding); const legacySummaryBinding = NodesInternal.useNodeDataWhenType( nodeId, @@ -44,9 +47,13 @@ export class List extends ListDef { // values are undefined. This is because useNodeFormDataWhenType doesn't know the intricacies of the group // binding and how it works in the List component. We need to find the values inside the rows ourselves. const rows = (formData?.group as unknown[] | undefined) ?? []; + const relativeCheckedBinding = checkedBinding?.field.replace(`${groupBinding.field}.`, ''); if (summaryBinding && dmBindings) { const summaryReference = dmBindings[summaryBinding]; - const rowData = rows.map((row) => (summaryReference ? findDataInRow(row, summaryReference, groupBinding) : '')); + const rowData = rows + .filter((row) => !relativeCheckedBinding || dot.pick(relativeCheckedBinding, row) === true) + .map((row) => (summaryReference ? findDataInRow(row, summaryReference, groupBinding) : '')); + return Object.values(rowData).join(', '); } @@ -92,6 +99,10 @@ export class List extends ListDef { return useValidateListIsEmpty(node); } + renderLayoutValidators(props: NodeValidationProps<'List'>): JSX.Element | null { + return ; + } + validateDataModelBindings(ctx: LayoutValidationCtx<'List'>): string[] { const errors: string[] = []; const allowedLeafTypes = ['string', 'boolean', 'number', 'integer']; diff --git a/src/layout/MultipleSelect/MultipleSelectComponent.tsx b/src/layout/MultipleSelect/MultipleSelectComponent.tsx index 0eacb267e9..89c6d667a7 100644 --- a/src/layout/MultipleSelect/MultipleSelectComponent.tsx +++ b/src/layout/MultipleSelect/MultipleSelectComponent.tsx @@ -12,6 +12,7 @@ import { FD } from 'src/features/formData/FormDataWrite'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; +import { useSaveValueToGroup } from 'src/features/saveToGroup/useSaveToGroup'; import { useIsValid } from 'src/features/validation/selectors/isValid'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import comboboxClasses from 'src/styles/combobox.module.css'; @@ -24,8 +25,11 @@ export type IMultipleSelectProps = PropsFromGenericComponent<'MultipleSelect'>; export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSelectProps) { const item = useNodeItem(node); const isValid = useIsValid(node); - const { id, readOnly, textResourceBindings, alertOnChange, grid, required } = item; - const { options, isFetching, selectedValues, setData } = useGetOptions(node, 'multi'); + const { id, readOnly, textResourceBindings, alertOnChange, grid, required, dataModelBindings } = item; + const { options, isFetching, selectedValues: selectedFromSimpleBinding, setData } = useGetOptions(node, 'multi'); + const groupBinding = useSaveValueToGroup(dataModelBindings); + const selectedValues = groupBinding.enabled ? groupBinding.selectedValues : selectedFromSimpleBinding; + const debounce = FD.useDebounceImmediately(); const { langAsString, lang } = useLanguage(node); @@ -44,9 +48,17 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele [lang, langAsString, options, selectedValues], ); + const handleOnChange = (values: string[]) => { + if (groupBinding.enabled) { + groupBinding.setCheckedValues(values); + } else { + setData(values); + } + }; + const { alertOpen, setAlertOpen, handleChange, confirmChange, cancelChange, alertMessage } = useAlertOnChange( Boolean(alertOnChange), - setData, + handleOnChange, // Only alert when removing values (values) => values.length < selectedValues.length, changeMessageGenerator, diff --git a/src/layout/MultipleSelect/config.ts b/src/layout/MultipleSelect/config.ts index 962185e294..2cbe4291e0 100644 --- a/src/layout/MultipleSelect/config.ts +++ b/src/layout/MultipleSelect/config.ts @@ -41,6 +41,30 @@ export const Config = new CG.component({ description: 'Boolean value indicating if the component should alert on change', }), ) - .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple')) + .addDataModelBinding( + new CG.obj( + new CG.prop( + 'group', + new CG.dataModelBinding() + .setTitle('group') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), + new CG.prop( + 'checked', + new CG.dataModelBinding() + .setTitle('checked') + .setDescription( + 'If deletionStrategy=soft and group is set, this value points to where you want to save deleted status.', + ) + .optional(), + ), + ) + .exportAs('IDataModelBindingsForGroupMultiselect') + .extends(CG.common('IDataModelBindingsOptionsSimple')), + ) + .addProperty(new CG.prop('deletionStrategy', new CG.enum('soft', 'hard').optional())) .extends(CG.common('LabeledComponentProps')) .extendTextResources(CG.common('TRBLabel')); diff --git a/src/layout/MultipleSelect/index.tsx b/src/layout/MultipleSelect/index.tsx index 934da2dd8c..dc32594508 100644 --- a/src/layout/MultipleSelect/index.tsx +++ b/src/layout/MultipleSelect/index.tsx @@ -1,22 +1,30 @@ import React, { forwardRef } from 'react'; import type { JSX } from 'react'; +import dot from 'dot-object'; + import { useLanguage } from 'src/features/language/useLanguage'; import { getCommaSeparatedOptionsToText } from 'src/features/options/getCommaSeparatedOptionsToText'; import { useNodeOptions } from 'src/features/options/useNodeOptions'; +import { validateSimpleBindingWithOptionalGroup } from 'src/features/saveToGroup/layoutValidation'; +import { ObjectToGroupLayoutValidator } from 'src/features/saveToGroup/ObjectToGroupLayoutValidator'; import { useEmptyFieldValidationOnlySimpleBinding } from 'src/features/validation/nodeValidation/emptyFieldValidation'; import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSummary'; import { MultipleSelectDef } from 'src/layout/MultipleSelect/config.def.generated'; import { MultipleSelectComponent } from 'src/layout/MultipleSelect/MultipleSelectComponent'; import { MultipleSelectSummary } from 'src/layout/MultipleSelect/MultipleSelectSummary'; +import { NodesInternal, useNode } from 'src/utils/layout/NodesContext'; import { useNodeFormDataWhenType } from 'src/utils/layout/useNodeItem'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; +import type { NodeValidationProps } from 'src/layout/layout'; import type { SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; +type Row = Record; + export class MultipleSelect extends MultipleSelectDef { render = forwardRef>( function LayoutComponentMultipleSelectRender(props, _): JSX.Element | null { @@ -25,10 +33,37 @@ export class MultipleSelect extends MultipleSelectDef { ); useDisplayData(nodeId: string): string { + const node = useNode(nodeId); const formData = useNodeFormDataWhenType(nodeId, 'MultipleSelect'); const options = useNodeOptions(nodeId).options; const langAsString = useLanguage().langAsString; - const data = getCommaSeparatedOptionsToText(formData?.simpleBinding, options, langAsString); + + if (!node) { + return ''; + } + const dataModelBindings = NodesInternal.useNodeData( + node as LayoutNode<'MultipleSelect'>, + (data) => data.layout.dataModelBindings, + ); + + const relativeCheckedPath = + dataModelBindings?.checked && dataModelBindings?.group + ? dataModelBindings.checked.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const relativeSimpleBindingPath = + dataModelBindings?.simpleBinding && dataModelBindings?.group + ? dataModelBindings.simpleBinding.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const displayRows = (formData?.group as unknown as Row[]) + ?.filter((row) => (!relativeCheckedPath ? true : dot.pick(relativeCheckedPath, row) === true)) + .map((row) => (!relativeSimpleBindingPath ? true : dot.pick(relativeSimpleBindingPath, row))); + + const data = dataModelBindings.group + ? displayRows + : getCommaSeparatedOptionsToText(formData?.simpleBinding, options, langAsString); + return Object.values(data).join(', '); } @@ -51,7 +86,11 @@ export class MultipleSelect extends MultipleSelectDef { return useEmptyFieldValidationOnlySimpleBinding(node); } + renderLayoutValidators(props: NodeValidationProps<'MultipleSelect'>): JSX.Element | null { + return ; + } + validateDataModelBindings(ctx: LayoutValidationCtx<'MultipleSelect'>): string[] { - return this.validateDataModelBindingsSimple(ctx); + return validateSimpleBindingWithOptionalGroup(this, ctx); } } diff --git a/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx b/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx index 095c92e01e..4720eebc2d 100644 --- a/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx +++ b/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { ErrorMessage, Label, List, Paragraph } from '@digdir/designsystemet-react'; import cn from 'classnames'; +import dot from 'dot-object'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -11,12 +12,14 @@ import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/ import { validationsOfSeverity } from 'src/features/validation/utils'; import { EditButton } from 'src/layout/Summary2/CommonSummaryComponents/EditButton'; import classes from 'src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.module.css'; -import { useNodeFormData } from 'src/utils/layout/useNodeItem'; +import { useNodeFormData, useNodeItem } from 'src/utils/layout/useNodeItem'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; type ValidTypes = 'MultipleSelect' | 'Checkboxes'; type ValidNodes = LayoutNode; +type Row = Record; + interface MultipleValueSummaryProps { title: React.ReactNode; componentNode: ValidNodes; @@ -46,16 +49,33 @@ export const MultipleValueSummary = ({ isCompact, emptyFieldText, }: MultipleValueSummaryProps) => { + const { dataModelBindings } = useNodeItem(componentNode); const options = useNodeOptions(componentNode).options; const rawFormData = useNodeFormData(componentNode); const { langAsString } = useLanguage(); - const displayValues = Object.values(getCommaSeparatedOptionsToText(rawFormData.simpleBinding, options, langAsString)); + + const relativeCheckedPath = + dataModelBindings?.checked && dataModelBindings?.group + ? dataModelBindings.checked.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const relativeSimpleBindingPath = + dataModelBindings?.simpleBinding && dataModelBindings?.group + ? dataModelBindings.simpleBinding.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const displayRows: string[] = (rawFormData?.group as unknown as Row[]) + ?.filter((row) => (!relativeCheckedPath ? true : dot.pick(relativeCheckedPath, row) === true)) + .map((row) => (!relativeSimpleBindingPath ? true : dot.pick(relativeSimpleBindingPath, row))); + + const displayValues = dataModelBindings.group + ? displayRows + : Object.values(getCommaSeparatedOptionsToText(rawFormData.simpleBinding, options, langAsString)); const validations = useUnifiedValidationsForNode(componentNode); const errors = validationsOfSeverity(validations, 'error'); const displayType = getDisplayType(displayValues, showAsList, isCompact); - return (
diff --git a/test/e2e/integration/component-library/checkboxes.ts b/test/e2e/integration/component-library/checkboxes.ts index dabf8058a2..d514bd03cd 100644 --- a/test/e2e/integration/component-library/checkboxes.ts +++ b/test/e2e/integration/component-library/checkboxes.ts @@ -28,4 +28,111 @@ describe('Checkboxes component', () => { .find('span.fds-paragraph') // Targets the span with the summary text .should('have.text', expectedText); }); + + it('Adds and removes data properly when using group and soft deletion', () => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Avkryssningsbokser'); + + const checkboxes = '[data-componentid=CheckboxesPage-Checkboxes2]'; + const repGroup = '[data-componentid=CheckboxesPage-RepeatingGroup]'; + + // Define the text for the last three checkboxes + const checkboxText1 = 'Karoline'; + const checkboxText2 = 'Kåre'; + const checkboxText3 = 'Johanne'; + const checkboxText4 = 'Kari'; + const checkboxText5 = 'Petter'; + + //Check options in checkboxes component + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText3).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').click(); + + //Uncheck + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').click(); + + //Check that checkboxes is correct + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText3).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').should('not.be.checked'); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').should('not.be.checked'); + + //Validate that the corresponding options in checkboxes is avaliable in repeating group< + cy.get(repGroup).findByRole('cell', { name: checkboxText1 }).should('exist'); + cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).should('exist'); + cy.get(repGroup).findByRole('cell', { name: checkboxText3 }).should('exist'); + + // Removing from RepeatingGroup should deselect from List + cy.get(repGroup).findAllByRole('row').should('have.length', 4); // Header + 1 row + cy.get(repGroup) + .findAllByRole('button', { name: /^Slett/ }) + .first() + .click(); + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').should('not.be.checked'); + + // Unchecking from Checkboxes should remove from RepeatingGroup (observe that data is preserved) + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').should('be.checked'); + cy.get(repGroup).findAllByRole('row').should('have.length', 3); // Header + 2 row + cy.get(repGroup) + .findAllByRole('button', { name: /^Rediger/ }) + .first() + .click(); + cy.findByRole('textbox', { name: /Age/ }).type('20'); + cy.findAllByRole('button', { name: /Lagre og lukk/ }) + .last() + .click(); + cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).parent().contains('td', '20'); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').click(); + + cy.get(repGroup).findAllByRole('row').should('have.length', 2); + + // Checking 'Kåre' again should bring back the surname + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').click(); + cy.get(repGroup).findAllByRole('row').should('have.length', 3); // Header + 1 row + cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).should('exist'); + cy.get(repGroup).findAllByRole('cell', { name: '20' }).should('exist'); + }); + + it('Renders the summary2 component with correct text for Checkboxes with group and soft deletion', () => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Avkryssningsbokser'); + + const checkboxes = '[data-componentid=CheckboxesPage-Checkboxes2]'; + const summary2 = '[data-componentid=CheckboxesPage-Header-Summary2-Component2]'; + + const checkboxText1 = 'Karoline'; + const checkboxText2 = 'Kåre'; + const checkboxText3 = 'Johanne'; + const checkboxText4 = 'Kari'; + const checkboxText5 = 'Petter'; + + //Check options in checkboxes component + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText3).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').click(); + + //Uncheck + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').click(); + + //Check that checkboxes is correct + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText3).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').should('not.be.checked'); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').should('not.be.checked'); + + const expectedText = [checkboxText1, checkboxText2, checkboxText3].join(', '); + + cy.get(`div${summary2}`) + .next() + .find('span.fds-paragraph') // Targets the span with the summary text + .should('have.text', expectedText); + }); }); diff --git a/test/e2e/integration/component-library/list.ts b/test/e2e/integration/component-library/list.ts index 8fdc8b63c4..3fcac454b6 100644 --- a/test/e2e/integration/component-library/list.ts +++ b/test/e2e/integration/component-library/list.ts @@ -49,19 +49,31 @@ describe('List component', () => { cy.get(repGroup).findAllByRole('row').should('have.length', 0); - // Checking 'Kari' again does not bring back the surname + // Checking 'Kari' again should bring back the surname cy.get(list).findByRole('cell', { name: 'Kari' }).parent().click(); cy.get(repGroup).findAllByRole('row').should('have.length', 2); // Header + 1 row cy.get(repGroup).findByRole('cell', { name: 'Kari' }).should('exist'); - cy.get(repGroup).findAllByRole('cell', { name: 'Olsen' }).should('not.exist'); + cy.get(repGroup).findAllByRole('cell', { name: 'Olsen' }).should('exist'); // Testing summaries cy.get(list).findByRole('cell', { name: 'Johanne' }).parent().click(); cy.get(repGroup).findAllByRole('row').should('have.length', 3); // Header + 2 rows - cy.get(summary1).should('contain.text', 'Kari, Johanne'); + cy.get(summary1).should('contain.text', 'Johanne, Kari'); // Summary2 is a bit more tricky to find cy.get('table').last().should('have.not.have.attr', 'id'); cy.get('table').last().findAllByRole('row').should('have.length', 3); // Header + 2 row + + // Find Kåre, make sure he's selected in both summaries + cy.get(list).findByRole('cell', { name: 'Kåre' }).parent().click(); + cy.get(repGroup).findAllByRole('row').should('have.length', 4); // Header + 3 rows + cy.get(summary1).should('contain.text', 'Johanne, Kari, Kåre'); + cy.get('table').last().findAllByRole('row').should('have.length', 4); // Header + 3 rows + + // Uncheck Kåre again, make sure he's no longer in any summaries + cy.get(list).findByRole('cell', { name: 'Kåre' }).parent().click(); + cy.get(repGroup).findAllByRole('row').should('have.length', 3); // Header + 2 rows + cy.get(summary1).should('contain.text', 'Johanne, Kari'); + cy.get('table').last().findAllByRole('row').should('have.length', 3); // Header + 2 rows }); }); diff --git a/test/e2e/integration/component-library/multiple-select.ts b/test/e2e/integration/component-library/multiple-select.ts index 542dfd64d4..c1ae1417ec 100644 --- a/test/e2e/integration/component-library/multiple-select.ts +++ b/test/e2e/integration/component-library/multiple-select.ts @@ -19,4 +19,124 @@ describe('Multiple select component', () => { .find('span.fds-paragraph') // Targets the span with the summary text .should('have.text', checkboxText); }); + it('Adds and removes data properly when using group and soft deletion', () => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Flervalg'); + + const multiselect = '#form-content-MultipleSelectPage-Checkboxes2'; + const multiselectList = 'div[role="listbox"]'; + const repGroup = '[data-componentid=MultipleSelectPage-RepeatingGroup]'; + //const summary1 = '[data-componentid=ListPage-Summary-Component2]'; + + // Define the text for the last three checkboxes + const checkboxText1 = 'Karoline'; + const checkboxText2 = 'Kåre'; + const checkboxText3 = 'Johanne'; + const checkboxText4 = 'Kari'; + const checkboxText5 = 'Petter'; + + cy.get(multiselect).click(); + + //Check options in checkboxes component + cy.get(multiselectList).contains('span', checkboxText1).click(); + cy.get(multiselectList).contains('span', checkboxText2).click(); + cy.get(multiselectList).contains('span', checkboxText3).click(); + cy.get(multiselectList).contains('span', checkboxText4).click(); + cy.get(multiselectList).contains('span', checkboxText5).click(); + + //Uncheck + cy.get(multiselectList).contains('span', checkboxText4).click(); + cy.get(multiselectList).contains('span', checkboxText5).click(); + + //Check that checkboxes is correct + cy.get(multiselect).contains('span', checkboxText1).should('exist'); + cy.get(multiselect).contains('span', checkboxText2).should('exist'); + cy.get(multiselect).contains('span', checkboxText3).should('exist'); + cy.get(multiselect).contains('span', checkboxText4).should('not.exist'); + cy.get(multiselect).contains('span', checkboxText5).should('not.exist'); + + //Clicking on the repeating group to close the popover from the multiselect + cy.get(repGroup).click({ force: true }); + + //Validate that the corresponding options in checkboxes is avaliable in repeating group + cy.get(repGroup).findByRole('cell', { name: checkboxText1 }).should('exist'); + cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).should('exist'); + cy.get(repGroup).findByRole('cell', { name: checkboxText3 }).should('exist'); + cy.get(repGroup).findByRole('cell', { name: checkboxText4 }).should('not.exist'); + cy.get(repGroup).findByRole('cell', { name: checkboxText5 }).should('not.exist'); + + // Removing from RepeatingGroup should deselect from List + cy.get(repGroup).findAllByRole('row').should('have.length', 4); // Header + 1 row + cy.get(repGroup) + .findAllByRole('button', { name: /^Slett/ }) + .first() + .click(); + cy.get(multiselect).contains('span', checkboxText1).should('not.exist'); + + // Unchecking from Checkboxes should remove from RepeatingGroup (observe that data is preserved) + cy.get(multiselect).contains('span', checkboxText2).should('exist'); + cy.get(repGroup).findAllByRole('row').should('have.length', 3); // Header + 2 row + cy.get(repGroup) + .findAllByRole('button', { name: /^Rediger/ }) + .first() + .click(); + cy.findByRole('textbox', { name: /Age/ }).type('20'); + cy.findAllByRole('button', { name: /Lagre og lukk/ }) + .last() + .click(); + cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).parent().contains('td', '20'); + cy.get(multiselect).click(); + cy.get(multiselectList).contains('span', checkboxText2).click(); + cy.get(repGroup).click({ force: true }); //closing the multiselect popover + cy.get(repGroup).findAllByRole('row').should('have.length', 2); + + // Checking 'Kåre' again should bring back the surname + cy.get(multiselect).click(); + cy.get(multiselectList).contains('span', checkboxText2).click(); + cy.get(repGroup).click({ force: true }); //closing the multiselect popover + cy.get(repGroup).findAllByRole('row').should('have.length', 3); // Header + 1 row + cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).should('exist'); + cy.get(repGroup).findAllByRole('cell', { name: '20' }).should('exist'); + }); + it('Renders the summary2 component with correct text for MultipleSelext with group and soft deletion', () => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Flervalg'); + + const multiselect = '#form-content-MultipleSelectPage-Checkboxes2'; + const multiselectList = 'div[role="listbox"]'; + const summary2 = '[data-componentid=MultipleSelectPage-Header-Summary2-Component2]'; + + const checkboxText1 = 'Karoline'; + const checkboxText2 = 'Kåre'; + const checkboxText3 = 'Johanne'; + const checkboxText4 = 'Kari'; + const checkboxText5 = 'Petter'; + + cy.get(multiselect).click(); + + //Check options in checkboxes component + cy.get(multiselectList).contains('span', checkboxText1).click(); + cy.get(multiselectList).contains('span', checkboxText2).click(); + cy.get(multiselectList).contains('span', checkboxText3).click(); + cy.get(multiselectList).contains('span', checkboxText4).click(); + cy.get(multiselectList).contains('span', checkboxText5).click(); + + //Uncheck + cy.get(multiselectList).contains('span', checkboxText4).click(); + cy.get(multiselectList).contains('span', checkboxText5).click(); + + //Check that checkboxes is correct + cy.get(multiselect).contains('span', checkboxText1).should('exist'); + cy.get(multiselect).contains('span', checkboxText2).should('exist'); + cy.get(multiselect).contains('span', checkboxText3).should('exist'); + cy.get(multiselect).contains('span', checkboxText4).should('not.exist'); + cy.get(multiselect).contains('span', checkboxText5).should('not.exist'); + + const expectedText = [checkboxText1, checkboxText2, checkboxText3].join(', '); + + cy.get(`div${summary2}`) + .next() + .find('span.fds-paragraph') // Targets the span with the summary text + .should('have.text', expectedText); + }); });