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

Feature/list component with checkboxes #2926

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c30f82c
Added componentType property + checkbox
walldenfilippa Nov 20, 2024
8dfaac6
validation frontend index.tsx and new datamodelbinding group + columns
walldenfilippa Nov 28, 2024
8c1b4f7
add formDataMock to test to checkout values in the listTable
Dec 5, 2024
2bf7b55
add to list workign
adamhaeger Dec 6, 2024
1404a1e
removeFromList
walldenfilippa Dec 10, 2024
ceabf28
added checkboxes mobile
walldenfilippa Dec 11, 2024
d9cfc48
clean up
walldenfilippa Dec 11, 2024
fdb8917
validateDataModelBindings
walldenfilippa Dec 11, 2024
7954676
summary2
walldenfilippa Dec 19, 2024
0ff1a26
summary2 + summary (in progress)
walldenfilippa Jan 6, 2025
9c75c58
edits summary component
walldenfilippa Jan 7, 2025
154590e
update isRowChecked to work on lists with more columns than bindings
Jan 7, 2025
5d3b00d
validations in progress + moved Listsummary component
walldenfilippa Jan 14, 2025
d092c67
Merge branch 'Feature/ListComponent' of https://github.com/walldenfil…
walldenfilippa Jan 14, 2025
94036dd
minor edits
walldenfilippa Jan 14, 2025
9dcd1d8
adjustments
walldenfilippa Jan 14, 2025
30a8f7c
Merge remote-tracking branch 'origin/main' into Feature/ListComponent
walldenfilippa Jan 17, 2025
e11efca
Merge branch 'Altinn:main' into Feature/ListComponent
walldenfilippa Jan 20, 2025
29d8aff
Refactor ListComponent and ListSummary to use DEFAULT_DEBOUNCE_TIMEOU…
walldenfilippa Feb 5, 2025
dd9263a
Merge branch 'Feature/ListComponent' of https://github.com/walldenfil…
walldenfilippa Feb 5, 2025
2dde0c5
Validate properties in saveToList binding to ensure correct field types
walldenfilippa Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions src/layout/List/ListComponent.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
height: 60px;
}

.radio {
.toggleControl {
align-content: center;
justify-content: center;
}
Expand All @@ -47,37 +47,60 @@
justify-content: center;
}

.mobileRadioGroup > div {
.mobileGroup > div {
display: flex;
flex-direction: column;
}

.mobileRadioGroup > div :hover {
.mobileGroup > div :hover {
background-color: var(--fds-colors-grey-100);
}

.mobileRadio {
.mobile {
border-top: 1px solid var(--fds-colors-grey-200);
padding: var(--fds-spacing-4);
gap: var(--fds-spacing-8);
}

.mobileRadio input:checked:hover {
.mobile input:checked:hover {
background-color: var(--fds-radio-border-color);
}

.mobileRadio label {
.mobile label {
width: 100%;
}

.mobileRadio span {
.mobile span {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-4);
}

.mobileRadio label div {
.mobile label div {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-2);
}

.listContainer {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-2);
}

.headerContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}

.header {
font-size: 1.125rem;
align-content: center;
}

.editButton {
margin-left: auto;
min-width: unset;
}
199 changes: 148 additions & 51 deletions src/layout/List/ListComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
import type { AriaAttributes } from 'react';

import { Pagination as AltinnPagination } from '@altinn/altinn-design-system';
import { Heading, Radio, Table } from '@digdir/designsystemet-react';
import { Checkbox, Heading, Radio, Table } from '@digdir/designsystemet-react';
import cn from 'classnames';
import { v4 as uuidv4 } from 'uuid';
import type { DescriptionText } from '@altinn/altinn-design-system/dist/types/src/components/Pagination/Pagination';

import { Description } from 'src/components/form/Description';
import { RadioButton } from 'src/components/form/RadioButton';
import { RequiredIndicator } from 'src/components/form/RequiredIndicator';
import { getLabelId } from 'src/components/label/Label';
import { useDataListQuery } from 'src/features/dataLists/useDataListQuery';
import { FD } from 'src/features/formData/FormDataWrite';
import { ALTINN_ROW_ID } from 'src/features/formData/types';
import { useDataModelBindings } from 'src/features/formData/useDataModelBindings';
import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
Expand Down Expand Up @@ -54,16 +57,23 @@

const { data } = useDataListQuery(filter, dataListId, secure, mapping, queryParameters);
const bindings = item.dataModelBindings ?? ({} as IDataModelBindingsForList);
const { formData, setValues } = useDataModelBindings(bindings);

const { formData, setValues } = useDataModelBindings(bindings, 1, 'raw');

const appendToList = FD.useAppendToList();

const removeFromList = FD.useRemoveIndexFromList();

const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter(
(key) => !tableHeadersMobile || tableHeadersMobile.includes(key),
);

const selectedRow =
data?.listItems.find((row) => Object.keys(formData).every((key) => row[key] === formData[key])) ?? '';
item.componentType != 'CheckBoxes'
? (data?.listItems.find((row) => Object.keys(formData).every((key) => row[key] === formData[key])) ?? '')
: '';

function handleRowSelect({ selectedValue }: { selectedValue: Row }) {
function handleSelectedRadioRow({ selectedValue }: { selectedValue: Row }) {
const next: Row = {};
for (const binding of Object.keys(bindings)) {
next[binding] = selectedValue[binding];
Expand All @@ -75,46 +85,124 @@
return JSON.stringify(selectedRow) === JSON.stringify(row);
}

function isRowChecked(row: Row): boolean {
return (formData?.saveToList as Row[]).some((selectedRow) => {
const { altinnRowId, ...rest } = selectedRow;

Check warning on line 90 in src/layout/List/ListComponent.tsx

View workflow job for this annotation

GitHub Actions / Type-checks, eslint, unit tests and SonarCloud

'altinnRowId' is assigned a value but never used. Allowed unused vars must match /^_/u
return Object.keys(rest).every((key) => Object.hasOwn(row, key) && row[key] === rest[key]);
});
}

const title = item.textResourceBindings?.title;
const description = item.textResourceBindings?.description;
const component = item.componentType;

const handleRowClick = (row) => {
if (item.componentType === 'CheckBoxes') {
handleSelectedCheckboxRow(row);
} else {
handleSelectedRadioRow({ selectedValue: row });
}
};

const handleSelectedCheckboxRow = (row) => {
if (!item.dataModelBindings?.saveToList) {
return;
}
if (isRowChecked(row)) {
const index = (formData?.saveToList as Row[]).findIndex((selectedRow) => {
const { altinnRowId, ...rest } = selectedRow;

Check warning on line 113 in src/layout/List/ListComponent.tsx

View workflow job for this annotation

GitHub Actions / Type-checks, eslint, unit tests and SonarCloud

'altinnRowId' is assigned a value but never used. Allowed unused vars must match /^_/u
return Object.keys(rest).every((key) => Object.hasOwn(row, key) && row[key] === rest[key]);
});
if (index >= 0) {
removeFromList({
reference: item.dataModelBindings.saveToList,
index,
});
}
} else {
const uuid = uuidv4();
const next: Row = { [ALTINN_ROW_ID]: uuid };
for (const binding of Object.keys(bindings)) {
if (binding != 'saveToList') {
next[binding] = row[binding];
}
}
appendToList({
reference: item.dataModelBindings.saveToList,
newValue: { ...next },
});
}
};

const renderListItems = (row, tableHeaders) =>
tableHeadersToShowInMobile.map((key) => (
<div key={key}>
<strong>
<Lang id={tableHeaders[key]} />
</strong>
<span>{typeof row[key] === 'string' ? <Lang id={row[key]} /> : row[key]}</span>
</div>
));

if (isMobile) {
return (
<ComponentStructureWrapper node={node}>
<Radio.Group
role='radiogroup'
required={required}
legend={
<Heading
level={2}
size='sm'
>
<Lang id={title} />
<RequiredIndicator required={required} />
</Heading>
}
description={description && <Lang id={description} />}
className={classes.mobileRadioGroup}
value={JSON.stringify(selectedRow)}
>
{data?.listItems.map((row) => (
<Radio
key={JSON.stringify(row)}
value={JSON.stringify(row)}
className={cn(classes.mobileRadio, { [classes.selectedRow]: isRowSelected(row) })}
onClick={() => handleRowSelect({ selectedValue: row })}
>
{tableHeadersToShowInMobile.map((key) => (
<div key={key}>
<strong>
<Lang id={tableHeaders[key]} />
</strong>
<span>{typeof row[key] === 'string' ? <Lang id={row[key]} /> : row[key]}</span>
</div>
))}
</Radio>
))}
</Radio.Group>
{component === 'CheckBoxes' ? (
<Checkbox.Group
role='group'
legend={
<Heading
level={2}
size='sm'
>
<Lang id={title} />
<RequiredIndicator required={required} />
</Heading>
}
description={description && <Lang id={description} />}
className={classes.mobileCheckboxGroup}
>
{data?.listItems.map((row) => (
<Checkbox
key={JSON.stringify(row)}
onClick={() => handleRowClick(row)}
value={JSON.stringify(row)}
className={cn(classes.mobile)}
checked={isRowChecked(row)}
>
{renderListItems(row, tableHeaders)}
</Checkbox>
))}
</Checkbox.Group>
) : (
<Radio.Group
role='radiogroup'
required={required}
legend={
<Heading
level={2}
size='sm'
>
<Lang id={title} />
<RequiredIndicator required={required} />
</Heading>
}
description={description && <Lang id={description} />}
className={classes.mobileGroup}
value={JSON.stringify(selectedRow)}
>
{data?.listItems.map((row) => (
<Radio
key={JSON.stringify(row)}
value={JSON.stringify(row)}
className={cn(classes.mobile, { [classes.selectedRow]: isRowSelected(row) })}
onClick={() => handleSelectedRadioRow({ selectedValue: row })}
>
{renderListItems(row, tableHeaders)}
</Radio>
))}
</Radio.Group>
)}
<Pagination
pageSize={pageSize}
setPageSize={setPageSize}
Expand Down Expand Up @@ -173,25 +261,34 @@
{data?.listItems.map((row) => (
<Table.Row
key={JSON.stringify(row)}
onClick={() => {
handleRowSelect({ selectedValue: row });
}}
onClick={() => handleRowClick(row)}
>
<Table.Cell
className={cn({
[classes.selectedRowCell]: isRowSelected(row),
})}
>
<RadioButton
className={classes.radio}
aria-label={JSON.stringify(row)}
onChange={() => {
handleRowSelect({ selectedValue: row });
}}
value={JSON.stringify(row)}
checked={isRowSelected(row)}
name={node.id}
/>
{component === 'CheckBoxes' ? (
<Checkbox
className={classes.toggleControl}
aria-label={JSON.stringify(row)}
onChange={() => {}}
value={JSON.stringify(row)}
checked={isRowChecked(row)}
name={node.id}
/>
) : (
<RadioButton
className={classes.toggleControl}
aria-label={JSON.stringify(row)}
onChange={() => {
handleSelectedRadioRow({ selectedValue: row });
}}
value={JSON.stringify(row)}
checked={isRowSelected(row)}
name={node.id}
/>
)}
</Table.Cell>
{Object.keys(tableHeaders).map((key) => (
<Table.Cell
Expand Down
Loading
Loading