Skip to content

Commit

Permalink
List component (#638)
Browse files Browse the repository at this point in the history
Co-authored-by: Johanne Lie <joli@patentstyret.no>
Co-authored-by: johlie <48760352+johlie@users.noreply.github.com>
Co-authored-by: Ole Martin Handeland <git@olemartin.org>
closes undefined
  • Loading branch information
caroliss authored Dec 6, 2022
1 parent fa9cfae commit c47699b
Show file tree
Hide file tree
Showing 33 changed files with 1,120 additions and 20 deletions.
70 changes: 68 additions & 2 deletions schemas/json/layout/layout.schema.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"type": "string",
"title": "Type",
"description": "The component type.",
"enum": ["AddressComponent", "AttachmentList", "Button", "Checkboxes", "Custom", "Datepicker", "Dropdown", "FileUpload", "FileUploadWithTag", "Group", "Header", "Image", "Input", "InstantiationButton", "Likert", "MultipleSelect", "NavigationButtons", "NavigationBar", "Panel", "Paragraph", "PrintButton", "RadioButtons", "Summary", "TextArea"]
"enum": ["AddressComponent", "AttachmentList", "Button", "Checkboxes", "Custom", "Datepicker", "Dropdown", "FileUpload", "FileUploadWithTag", "Group", "Header", "Image", "Input", "InstantiationButton", "Likert","List", "MultipleSelect", "NavigationButtons", "NavigationBar", "Panel", "Paragraph", "PrintButton", "RadioButtons", "Summary", "TextArea"]
},
"required": {
"title": "Required",
Expand Down Expand Up @@ -138,7 +138,8 @@
{ "if": {"properties": {"type": { "const": "RadioButtons"}}}, "then": { "$ref": "#/definitions/radioAndCheckboxComponents"}},
{ "if": {"properties": {"type": { "const": "Summary"}}}, "then": {"$ref": "#/definitions/summaryComponent"}},
{ "if": {"properties": {"type": { "const": "Header"}}}, "then": {"$ref": "#/definitions/headerComponent"}},
{ "if": {"properties": {"type": { "const": "Panel"}}}, "then": {"$ref": "#/definitions/panelComponent"}}
{ "if": {"properties": {"type": { "const": "Panel"}}}, "then": {"$ref": "#/definitions/panelComponent"}},
{ "if": {"properties": {"type": { "const": "List"}}}, "then": {"$ref": "#/definitions/listComponent"}}
]
},
"headerComponent": {
Expand Down Expand Up @@ -758,6 +759,71 @@
"additionalProperties": {
"type": "string"
}
},
"listComponent": {
"type": "object",
"properties": {
"tableHeaders": {
"type": "array",
"items": {
"type": "string"
},
"title": "Table Headers",
"description": "An array of strings that is going to be headers of the table. Can be added to the resource files to change between languages"
},
"sortableColumns": {
"type": "array",
"items": {
"type": "string"
},
"title": "Sortable Columns",
"description": "An array of the columns that is going to be sortable. The column has to be represented by the the headername that is written in tableHeaders"
},
"pagination": {
"title": "Pagination",
"$ref": "#/definitions/paginationProperties"
},
"dataListId": {
"type": "string",
"title": "List ID",
"description": "The Id of the list. This id is used to retrive the datalist from the backend"
},
"secure": {
"type": "boolean",
"title": "Secure ListItems",
"description": "Boolean value indicating if the options should be instance aware. Defaults to false."
},
"bindingToShowInSummary": {
"type": "string",
"title": "Binding to show in summary",
"description": "The value of this binding will be shown in the summary component for the list. This binding must be one of the specified bindings under dataModelBindings."
}
},
"required": [
"dataListId"
]
},
"paginationProperties": {
"type": "object",
"properties": {
"alternatives": {
"type": "array",
"items": {
"type": "number"
},
"title": "Alternatives",
"description": "List of page sizes the user can choose from. Make sure to test the performance of the largest number of items per page you are allowing."
},
"default": {
"type": "number",
"title": "Default",
"description": "The pagination size that is set to default"
}
},
"required": [
"alternatives",
"default"
]
}
}
}
4 changes: 4 additions & 0 deletions src/altinn-app-frontend/__mocks__/initialStateMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export function getInitialStateMock(customStates?: Partial<IRuntimeState>): IRun
options: {},
error: null,
},
dataListState: {
dataLists: {},
error: null,
},
applicationSettings: {
applicationSettings: applicationSettingsMock,
error: null,
Expand Down
74 changes: 74 additions & 0 deletions src/altinn-app-frontend/src/components/base/ListComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';

import { getInitialStateMock } from '__mocks__/initialStateMock';
import { SortDirection } from '@altinn/altinn-design-system';
import { screen } from '@testing-library/react';
import { mockComponentProps, renderWithProviders } from 'testUtils';
import type { PreloadedState } from 'redux';

import { ListComponent } from 'src/components/base/ListComponent';
import type { IListProps } from 'src/components/base/ListComponent';
import type { IDataListsState } from 'src/shared/resources/dataLists';
import type { RootState } from 'src/store';

const countries = [
{ Name: 'Norway', Population: 5, HighestMountain: 2469 },
{ Name: 'Sweden', Population: 10, HighestMountain: 1738 },
{ Name: 'Denmark', Population: 6, HighestMountain: 170 },
{ Name: 'Germany', Population: 83, HighestMountain: 2962 },
{ Name: 'Spain', Population: 47, HighestMountain: 3718 },
{ Name: 'France', Population: 67, HighestMountain: 4807 },
];

export const testState: IDataListsState = {
dataLists: {
['countries']: {
listItems: countries,
dataListId: 'countries',
loading: true,
sortColumn: 'HighestMountain',
sortDirection: SortDirection.Ascending,
},
},
dataListsWithIndexIndicator: [],
error: null,
};

const render = (props: Partial<IListProps> = {}, customState: PreloadedState<RootState> = {}) => {
const allProps: IListProps = {
...mockComponentProps,
dataListId: 'countries',
tableHeaders: ['Name', 'Population', 'HighestMountain'],
sortableColumns: ['Population', 'HighestMountain'],
pagination: { alternatives: [2, 5], default: 2 },
getTextResourceAsString: (value) => value,
...props,
};

renderWithProviders(<ListComponent {...allProps} />, {
preloadedState: {
...getInitialStateMock(),
dataListState: {
dataLists: {
[allProps.id]: { listItems: countries, id: 'countries' },
},
error: {
name: '',
message: '',
},
...customState,
},
},
});
};

describe('ListComponent', () => {
jest.useFakeTimers();

it('should render rows that is sent in but not rows that is not sent in', async () => {
render({});
expect(screen.getByText('Norway')).toBeInTheDocument();
expect(screen.getByText('Sweden')).toBeInTheDocument();
expect(screen.queryByText('Italy')).not.toBeInTheDocument();
});
});
195 changes: 195 additions & 0 deletions src/altinn-app-frontend/src/components/base/ListComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React from 'react';

import {
Pagination,
RadioButton,
SortDirection,
Table,
TableBody,
TableCell,
TableFooter,
TableHeader,
TableRow,
} from '@altinn/altinn-design-system';
import type { ChangeProps, RowData, SortProps } from '@altinn/altinn-design-system';

import type { PropsFromGenericComponent } from '..';

import { useAppDispatch, useAppSelector } from 'src/common/hooks';
import { useGetDataList } from 'src/components/hooks';
import { DataListsActions } from 'src/shared/resources/dataLists/dataListsSlice';

import { getLanguageFromKey } from 'altinn-shared/utils';

export type IListProps = PropsFromGenericComponent<'List'>;

const defaultDataList: any[] = [];
export interface rowValue {
[key: string]: string;
}

export const ListComponent = ({
tableHeaders,
id,
pagination,
formData,
handleDataChange,
getTextResourceAsString,
sortableColumns,
dataModelBindings,
language,
}: IListProps) => {
const dynamicDataList = useGetDataList({ id });
const calculatedDataList = dynamicDataList || defaultDataList;
const defaultPagination = pagination ? pagination.default : 0;
const rowsPerPage = useAppSelector((state) => state.dataListState.dataLists[id]?.size || defaultPagination);
const currentPage = useAppSelector((state) => state.dataListState.dataLists[id]?.pageNumber || 0);

const sortColumn = useAppSelector((state) => state.dataListState.dataLists[id]?.sortColumn || null);
const sortDirection = useAppSelector(
(state) => state.dataListState.dataLists[id]?.sortDirection || SortDirection.NotActive,
);
const totalItemsCount = useAppSelector(
(state) => state.dataListState.dataLists[id]?.paginationData?.totaltItemsCount || 0,
);

const handleChange = ({ selectedValue }: ChangeProps) => {
for (const key in formData) {
handleDataChange(selectedValue[key], { key: key });
}
};

const renderRow = (datalist) => {
const cells: JSX.Element[] = [];
for (const key of Object.keys(datalist)) {
cells.push(<TableCell key={`${key}_${datalist[key]}`}>{datalist[key]}</TableCell>);
}
return cells;
};

const renderHeaders = (headers) => {
const cell: JSX.Element[] = [];
for (const header of headers) {
if ((sortableColumns || []).includes(header)) {
cell.push(
<TableCell
onChange={handleSortChange}
sortKey={header}
key={header}
sortDirecton={sortColumn === header ? sortDirection : SortDirection.NotActive}
>
{getTextResourceAsString(header)}
</TableCell>,
);
} else {
cell.push(<TableCell key={header}>{getTextResourceAsString(header)}</TableCell>);
}
}
return cell;
};

const dispatch = useAppDispatch();

const handleSortChange = ({ sortedColumn, previousSortDirection }: SortProps) => {
dispatch(
DataListsActions.setSort({
key: id || '',
sortColumn: sortedColumn,
sortDirection:
previousSortDirection === SortDirection.Descending ? SortDirection.Ascending : SortDirection.Descending,
}),
);
};

const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLSelectElement>) => {
dispatch(
DataListsActions.setPageSize({
key: id || '',
size: parseInt(event.target.value, 10),
}),
);
};

const handleChangeCurrentPage = (newPage: number) => {
dispatch(
DataListsActions.setPageNumber({
key: id || '',
pageNumber: newPage,
}),
);
};
const rowAsValue = (datalist) => {
const chosenRowData: rowValue = {};
for (const key in dataModelBindings) {
chosenRowData[key] = datalist[key];
}
return chosenRowData;
};
const rowAsValueString = (datalist) => {
return JSON.stringify(rowAsValue(datalist));
};

const createLabelRadioButton = (datalist) => {
let label = '';
for (const key in formData) {
label += `${key} ${datalist[key]} `;
}
return label;
};

return (
<Table
selectRows={true}
onChange={handleChange}
selectedValue={formData as RowData}
>
<TableHeader>
<TableRow>
<TableCell radiobutton={true}></TableCell>
{renderHeaders(tableHeaders)}
</TableRow>
</TableHeader>
<TableBody>
{calculatedDataList.map((datalist) => {
return (
<TableRow
key={JSON.stringify(datalist)}
rowData={rowAsValue(datalist)}
>
<TableCell radiobutton={true}>
<RadioButton
name={datalist}
onChange={() => {
// Intentionally empty to prevent double-selection
}}
value={rowAsValueString(datalist)}
checked={rowAsValueString(datalist) === JSON.stringify(formData) ? true : false}
label={createLabelRadioButton(datalist)}
hideLabel={true}
></RadioButton>
</TableCell>
{renderRow(datalist)}
</TableRow>
);
})}
</TableBody>
{pagination && (
<TableFooter>
<TableRow>
<TableCell colSpan={tableHeaders && 1 + tableHeaders?.length}>
<Pagination
numberOfRows={totalItemsCount}
rowsPerPageOptions={pagination.alternatives}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleChangeRowsPerPage}
currentPage={currentPage}
setCurrentPage={handleChangeCurrentPage}
descriptionTexts={getLanguageFromKey('list_component', language)}
/>
</TableCell>
</TableRow>
</TableFooter>
)}
</Table>
);
};
Loading

0 comments on commit c47699b

Please sign in to comment.