Skip to content

Commit

Permalink
Merge pull request #720 from axonivy/hold-state-12
Browse files Browse the repository at this point in the history
XIVY-15429 Hold inscription accordion state over different selections > r12
  • Loading branch information
ivy-lli authored Jan 21, 2025
2 parents b3ddbfc + 0bdbd35 commit 9c90cb3
Show file tree
Hide file tree
Showing 44 changed files with 255 additions and 167 deletions.
28 changes: 12 additions & 16 deletions packages/inscription-view/src/components/editors/part/Part.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import './Part.css';
import { useState } from 'react';
import { IvyIcons } from '@axonivy/ui-icons';
import {
Accordion,
Expand All @@ -12,7 +11,7 @@ import {
Flex
} from '@axonivy/ui-components';
import { ErrorBoundary } from 'react-error-boundary';
import type { PartProps } from './usePart';
import { useAccordionState, type PartProps } from './usePart';
import type { Severity } from '@axonivy/process-editor-inscription-protocol';
import { useSticky } from './useSticky';
import ErrorFallback from '../../widgets/error/ErrorFallback';
Expand All @@ -29,23 +28,20 @@ const Control = ({ name, reset, control, ...props }: Pick<PartProps, 'name' | 'r
return null;
};

const State = ({ state }: Pick<PartProps, 'state'>) => {
return (
<AccordionState
messages={state.validations.map(({ message, severity }) => ({
message,
variant: severity.toLocaleLowerCase() as Lowercase<Severity>
}))}
state={state.state}
/>
);
};
const State = ({ state }: Pick<PartProps, 'state'>) => (
<AccordionState
messages={state.validations.map(({ message, severity }) => ({
message,
variant: severity.toLocaleLowerCase() as Lowercase<Severity>
}))}
state={state.state}
/>
);

const Part = ({ parts }: { parts: PartProps[] }) => {
const [value, setValue] = useState('');

const { value, updateValue } = useAccordionState(parts);
return (
<Accordion type='single' collapsible value={value} onValueChange={setValue}>
<Accordion type='single' collapsible value={value} onValueChange={updateValue}>
{parts.map(part => (
<PartItem {...part} key={part.name} />
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { renderHook } from 'test-utils';
import type { PartStateFlag } from './usePart';
import { usePartState } from './usePart';
import type { PartProps, PartStateFlag } from './usePart';
import { useAccordionState, usePartState } from './usePart';
import type { ValidationResult } from '@axonivy/process-editor-inscription-protocol';
import { describe, test, expect } from 'vitest';

describe('PartState', () => {
describe('usePartState', () => {
function assertState(expectedState: PartStateFlag, data?: unknown, message?: ValidationResult[]) {
const { result } = renderHook(() => usePartState({}, data ?? {}, message ?? []));
expect(result.current.state).toEqual(expectedState);
Expand All @@ -25,3 +25,33 @@ describe('PartState', () => {
]);
});
});

describe('useAccordionState', () => {
const ACCORDION_STORAGE_KEY = 'process-inscription-accordion';
const parts = [{ name: 'General' }, { name: 'Dialog' }] as Array<PartProps>;

test('empty storage', () => {
const { result } = renderHook(() => useAccordionState(parts));
expect(result.current.value).toEqual('');
});

test('wrong storage', () => {
sessionStorage.setItem(ACCORDION_STORAGE_KEY, 'wrong');
const { result } = renderHook(() => useAccordionState(parts));
expect(result.current.value).toEqual('');
});

test('other storage', () => {
sessionStorage.setItem(ACCORDION_STORAGE_KEY, `["Result"]`);
const { result } = renderHook(() => useAccordionState(parts));
expect(result.current.value).toEqual('');
});

test('matching storage', () => {
sessionStorage.setItem(ACCORDION_STORAGE_KEY, `["Result", "Dialog"]`);
const { result } = renderHook(() => useAccordionState(parts));
expect(result.current.value).toEqual('Dialog');
result.current.updateValue('');
expect(sessionStorage.getItem(ACCORDION_STORAGE_KEY)).toEqual(`["Result"]`);
});
});
36 changes: 35 additions & 1 deletion packages/inscription-view/src/components/editors/part/usePart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ValidationResult } from '@axonivy/process-editor-inscription-protocol';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { deepEqual } from '../../../utils/equals';

export type PartStateFlag = 'configured' | 'warning' | 'error' | undefined;
Expand Down Expand Up @@ -36,3 +36,37 @@ export function usePartDirty(initData: unknown, data: unknown): boolean {
return !deepEqual(data, initData);
}, [data, initData]);
}

const ACCORDION_STORAGE_KEY = 'process-inscription-accordion';

export const useAccordionState = (parts: Array<PartProps>) => {
const [value, setValue] = useState(() => {
try {
const storage = sessionStorage.getItem(ACCORDION_STORAGE_KEY) ?? '[]';
const states = JSON.parse(storage) as Array<string>;
return parts.find(part => states.includes(part.name))?.name ?? '';
} catch {
console.error('Error reading from sessionStorage');
return '';
}
});
const updateValue = (value: string) => {
setValue(old => {
try {
const storage = sessionStorage.getItem(ACCORDION_STORAGE_KEY) ?? '[]';
let states = JSON.parse(storage) as Array<string>;
if (states.includes(old)) {
states.splice(states.indexOf(old), 1);
}
if (value) {
states = [value, ...states];
}
sessionStorage.setItem(ACCORDION_STORAGE_KEY, JSON.stringify(states));
} catch {
console.error('Error store to sessionStorage');
}
return value;
});
};
return { value, updateValue };
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ describe('CustomFieldTable', () => {
test('table can add rows by keyboard', async () => {
const view = renderTable();
await TableUtil.assertAddRowWithKeyboard(view, 'number', '1');
// data does not contain empty object
expect(view.data()).toEqual([
{ name: 'field1', type: 'STRING', value: 'this is a string' },
{ name: 'number1', type: 'NUMBER', value: '1' },
{ name: '', type: 'STRING', value: '' }
{ name: 'number1', type: 'NUMBER', value: '1' }
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { memo, useMemo } from 'react';
import { ValidationRow } from '../path/validation/ValidationRow';
import { PathCollapsible } from '../path/PathCollapsible';
import { useResizableEditableTable } from '../table/useResizableEditableTable';
import { deepEqual } from '../../../../utils/equals';
import { ComboCell, SelectCell, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components';
import type { SelectItem } from '../../../widgets/select/Select';
import { useEditorContext } from '../../../../context/useEditorContext';
Expand Down Expand Up @@ -56,35 +55,20 @@ const CustomFieldTable = ({ data, onChange, type }: CustomFieldTableProps) => {
[data, items, predefinedCustomField]
);

const updateCustomFields = (rowId: string, columnId: string, value: string) => {
const rowIndex = parseInt(rowId);
const updatedData = data.map((row, index) => {
const updateCustomFields = (data: Array<WfCustomField>, rowIndex: number, columnId: string) => {
if (columnId !== 'name') {
return data;
}
return data.map((customField, index) => {
if (index === rowIndex) {
return {
...data[rowIndex]!,
[columnId]: value
};
const predefinedField = predefinedCustomField.find(pcf => pcf.name === customField.name);
if (predefinedField && predefinedField.type !== customField.type) {
return { ...customField, type: predefinedField.type };
}
return customField;
}
return row;
return customField;
});
const autoChangedData =
columnId === 'name'
? updatedData.map((customField, index) => {
if (index === rowIndex) {
const predefinedField = predefinedCustomField.find(pcf => pcf.name === customField.name);
if (predefinedField && predefinedField.type !== customField.type) {
return { ...customField, type: predefinedField.type };
}
return customField;
}
return customField;
})
: updatedData;
if (!deepEqual(autoChangedData[updatedData.length - 1], EMPTY_WFCUSTOMFIELD) && rowIndex === data.length - 1) {
onChange([...autoChangedData, EMPTY_WFCUSTOMFIELD]);
} else {
onChange(autoChangedData);
}
};

const { table, rowSelection, setRowSelection, removeRowAction, showAddButton } = useResizableEditableTable({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ describe('StartCustomFieldTable', () => {
const view = renderTable();
await userEvent.click(screen.getAllByRole('row')[2]);
await TableUtil.assertAddRowWithKeyboard(view, 'number', '1');
// data does not contain empty object
expect(view.data()).toEqual([
{ name: 'field1', value: 'this is a string' },
{ name: 'number1', value: '1' },
{ name: '', value: '' }
{ name: 'number1', value: '1' }
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ describe('ParameterTable', () => {
const view = renderTable();
await CollapsableUtil.toggle('Input parameters');
await TableUtil.assertAddRowWithKeyboard(view, 'number');
// data does not contain empty object
expect(view.data()).toEqual([
{ name: 'field1', type: 'String', desc: 'this is a string' },
{ name: 'number', type: 'Number', desc: '1' },
{ name: '', type: 'String', desc: '' }
{ name: 'number', type: 'Number', desc: '1' }
]);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table';
import { getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { deepEqual } from '../../../../utils/equals';
import { IvyIcons } from '@axonivy/ui-icons';
import { TableAddRow } from '@axonivy/ui-components';
Expand All @@ -11,7 +11,7 @@ interface UseResizableEditableTableProps<TData> {
columns: ColumnDef<TData, string>[];
onChange: (change: TData[]) => void;
emptyDataObject: TData;
specialUpdateData?: (rowId: string, columnId: string, value: string) => void;
specialUpdateData?: (data: Array<TData>, rowIndex: number, columnId: string) => void;
}

const useResizableEditableTable = <TData,>({
Expand All @@ -21,29 +21,36 @@ const useResizableEditableTable = <TData,>({
emptyDataObject,
specialUpdateData
}: UseResizableEditableTableProps<TData>) => {
const [tableData, setTableData] = useState<TData[]>(data);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const updateTableData = (tableData: Array<TData>) => {
setTableData(tableData);
onChange(tableData.filter(obj => !deepEqual(obj, emptyDataObject)));
};

const updateData = (rowId: string, columnId: string, value: string) => {
const rowIndex = parseInt(rowId);
const updatedData = data.map((row, index) => {
const updatedData = tableData.map((row, index) => {
if (index === rowIndex) {
return {
...data[rowIndex]!,
...tableData[rowIndex],
[columnId]: value
};
}
return row;
});
if (!deepEqual(updatedData[updatedData.length - 1], emptyDataObject) && rowIndex === data.length - 1) {
onChange([...updatedData, emptyDataObject]);
specialUpdateData?.(updatedData, rowIndex, columnId);
if (!deepEqual(updatedData.at(-1), emptyDataObject) && rowIndex === tableData.length - 1) {
updateTableData([...updatedData, emptyDataObject]);
} else {
onChange(updatedData);
updateTableData(updatedData);
}
};

const table = useReactTable({
data,
data: tableData,
columns,
state: { sorting, rowSelection },
columnResizeMode: 'onChange',
Expand All @@ -55,42 +62,36 @@ const useResizableEditableTable = <TData,>({
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
meta: { updateData: specialUpdateData ? specialUpdateData : updateData }
meta: { updateData }
});

useEffect(() => {
if (Object.keys(rowSelection).length !== 1) {
const filteredData = data.filter(obj => !deepEqual(obj, emptyDataObject));
if (filteredData.length !== data.length) {
setRowSelection({});
onChange(filteredData);
}
}
}, [data, emptyDataObject, onChange, rowSelection, table]);

const addRow = () => {
const newData = [...data];
const newData = [...tableData];
newData.push(emptyDataObject);
onChange(newData);
updateTableData(newData);
setRowSelection({ [`${newData.length - 1}`]: true });
};

const showAddButton = () => {
if (data.filter(obj => deepEqual(obj, emptyDataObject)).length === 0) {
if (tableData.filter(obj => deepEqual(obj, emptyDataObject)).length === 0) {
return <TableAddRow addRow={addRow} />;
}
return null;
};

const removeRow = (index: number) => {
const newData = [...data];
const newData = [...tableData];
newData.splice(index, 1);
if (newData.length === 0) {
setRowSelection({});
} else if (index === data.length - 1) {
} else if (index === tableData.length - 1) {
setRowSelection({ [`${newData.length - 1}`]: true });
}
onChange(newData);
if (newData.length === 1 && deepEqual(newData[0], emptyDataObject)) {
updateTableData([]);
} else {
updateTableData(newData);
}
};

const removeRowAction: FieldsetControl = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ describe('DocumentTable', () => {
test('table can add rows by keyboard', async () => {
const view = renderTable();
await TableUtil.assertAddRowWithKeyboard(view, 'ivyTeam ❤️');
// data does not contain empty object
expect(view.data()).toEqual([
{ name: 'Doc 1', url: 'axonivy.com' },
{ name: 'ivyTeam ❤️', url: 'ivyteam.ch' },
{ name: '', url: '' }
{ name: 'ivyTeam ❤️', url: 'ivyteam.ch' }
]);
});

Expand Down
5 changes: 3 additions & 2 deletions packages/inscription-view/src/test-utils/table-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export namespace TableUtil {
await assertRowCount(expectedRows);
const addButton = screen.getByRole('button', { name: 'Add row' });
await userEvent.click(addButton);
expect(view.data()).toHaveLength(expectedRows);
// data does not contain empty object
expect(view.data()).toHaveLength(expectedRows - 1);

view.rerender();
await assertRowCount(expectedRows + 1);
Expand All @@ -51,7 +52,7 @@ export namespace TableUtil {
}
await userEvent.tab();
view.rerender();
await waitFor(() => expect(screen.getAllByRole('row')).toHaveLength(rowCount + 1));
await assertRowCount(rowCount + 1);
}

export async function assertRemoveRow(view: { data: () => unknown[]; rerender: () => void }, expectedRows: number): Promise<void> {
Expand Down
Loading

0 comments on commit 9c90cb3

Please sign in to comment.