Skip to content

Commit

Permalink
Merge pull request #59 from axonivy/add-combine-dataclassFields
Browse files Browse the repository at this point in the history
XIVY-15138 Add Multiselect on ctrl and remove/reorder multiselect
  • Loading branch information
ivy-edp authored Oct 23, 2024
2 parents d0a749c + 25231c5 commit f8954ca
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 68 deletions.
5 changes: 5 additions & 0 deletions integrations/standalone/src/mock/dataclass-client-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ValidationMessage
} from '@axonivy/dataclass-editor/src/protocol/types';
import { MetaMock } from './meta-mock';
import type { DataClassField } from '@axonivy/dataclass-editor';

export class DataClassClientMock implements Client {
private dataClassData: Data = {
Expand Down Expand Up @@ -67,6 +68,10 @@ export class DataClassClientMock implements Client {
return Promise.resolve([]);
}

function(): Promise<Array<DataClassField>> {
return Promise.resolve([]);
}

meta<TMeta extends keyof MetaRequestTypes>(path: TMeta): Promise<MetaRequestTypes[TMeta][1]> {
switch (path) {
case 'meta/scripting/ivyTypes':
Expand Down
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/dataclass-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"types": "lib/index.d.ts",
"main": "lib/editor.js",
"dependencies": {
"@axonivy/jsonrpc": "~12.0.0-next.342",
"@axonivy/ui-components": "~12.0.0-next.342",
"@axonivy/ui-icons": "~12.0.0-next.342",
"@axonivy/jsonrpc": "~12.0.0-next.344",
"@axonivy/ui-components": "~12.0.0-next.344",
"@axonivy/ui-icons": "~12.0.0-next.344",
"@tanstack/react-query": "5.32.1",
"@tanstack/react-query-devtools": "5.32.1",
"react": "^18.2.0"
Expand Down
77 changes: 71 additions & 6 deletions packages/dataclass-editor/src/master/DataClassMasterContent.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import {
arrayMoveMultiple,
BasicField,
Button,
deleteFirstSelectedRow,
deleteAllSelectedRows,
Flex,
handleMultiSelectOnCtrlRowClick,
indexOf,
Message,
ReorderHandleWrapper,
resetAndSetRowSelection,
selectRow,
Separator,
SortableHeader,
Table,
TableBody,
TableResizableHeader,
toast,
Tooltip,
TooltipContent,
TooltipProvider,
Expand All @@ -20,14 +25,18 @@ import {
useTableSort
} from '@axonivy/ui-components';
import { IvyIcons } from '@axonivy/ui-icons';
import { getCoreRowModel, useReactTable, type ColumnDef, type Table as TanstackTable } from '@tanstack/react-table';
import { getCoreRowModel, useReactTable, type ColumnDef, type Row, type Table as TanstackTable } from '@tanstack/react-table';
import { useEffect } from 'react';
import { useAppContext } from '../context/AppContext';
import { type DataClassField } from '../data/dataclass';
import { AddFieldDialog } from './AddFieldDialog';
import './DataClassMasterContent.css';
import { ValidationRow } from './ValidationRow';
import { useValidation } from './useValidation';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { genQueryKey } from '../query/query-client';
import { useClient } from '../protocol/ClientContextProvider';
import type { CombinePayload } from '../protocol/types';

const fullQualifiedClassNameRegex = /(?:[\w]+\.)+([\w]+)(?=[<,> ]|$)/g;

Expand All @@ -38,14 +47,17 @@ export const simpleTypeName = (fullQualifiedType: string) => {
export const useUpdateSelection = (table: TanstackTable<DataClassField>) => {
const { setSelectedField } = useAppContext();
const selectedRows = table.getSelectedRowModel().rows;
const selectedField = selectedRows.length === 0 ? undefined : selectedRows[0].index;
const selectedField = selectedRows.length === 1 ? selectedRows[0].index : undefined;
useEffect(() => {
setSelectedField(selectedField);
}, [selectedField, setSelectedField]);
};

export const DataClassMasterContent = () => {
const { dataClass, setDataClass, setSelectedField } = useAppContext();
const { context, dataClass, setDataClass, setSelectedField } = useAppContext();
const client = useClient();
const queryClient = useQueryClient();

const messages = useValidation();

const selection = useTableSelect<DataClassField>();
Expand Down Expand Up @@ -85,6 +97,7 @@ export const DataClassMasterContent = () => {
];
const table = useReactTable({
...selection.options,
enableMultiRowSelection: true,
...sort.options,
data: dataClass.fields,
columns,
Expand All @@ -97,16 +110,61 @@ export const DataClassMasterContent = () => {
useUpdateSelection(table);

const deleteField = () => {
const { newData: newFields, selection } = deleteFirstSelectedRow(table, dataClass.fields);
const { newData: newFields, selection } = deleteAllSelectedRows(table, dataClass.fields);
const newDataClass = structuredClone(dataClass);
newDataClass.fields = newFields;
setDataClass(newDataClass);
setSelectedField(selection);
};

const updateOrder = (moveId: string, targetId: string) => {
const selectedRows = table.getSelectedRowModel().flatRows.map(r => r.original.name);
const moveIds = selectedRows.length > 1 ? selectedRows : [dataClass.fields[parseInt(moveId)].name];
const newDataClass = structuredClone(dataClass);
const moveIndexes = moveIds.map(moveId => indexOf(newDataClass.fields, field => field.name === moveId));
const toIndex = parseInt(targetId);
arrayMoveMultiple(newDataClass.fields, moveIndexes, toIndex);

setDataClass(newDataClass);
resetAndSetRowSelection(table, newDataClass.fields, moveIds, row => row.name);
};

const combineFields = useMutation({
mutationKey: genQueryKey('function', context),
mutationFn: async () => {
const selectedRows = table.getSelectedRowModel().rows;
const payload: CombinePayload = {
fieldNames: selectedRows.map(row => row.original.name)
};
return client.function({ actionId: 'combineFields', context, payload });
},
onSuccess: () => {
toast.info('Fields successfully combined');
queryClient.invalidateQueries({ queryKey: genQueryKey('data', context) });
},
onError: error => {
toast.error('Failed to combine fields', { description: error.message });
}
});

const handleRowDrag = (row: Row<DataClassField>) => {
if (!row.getIsSelected()) {
table.resetRowSelection();
}
};

const readonly = useReadonly();
const control = readonly ? null : (
<Flex gap={2}>
{table.getSelectedRowModel().rows.length > 0 && (
<Button
key='combineButton'
icon={IvyIcons.WrapToSubprocess}
onClick={() => combineFields.mutate()}
aria-label='Combine fields'
title='Combine fields'
/>
)}
<AddFieldDialog table={table} />
<Separator decorative orientation='vertical' style={{ height: '20px', margin: 0 }} />
<Button
Expand All @@ -131,7 +189,14 @@ export const DataClassMasterContent = () => {
<TableResizableHeader headerGroups={table.getHeaderGroups()} onClick={() => selectRow(table)} />
<TableBody>
{table.getRowModel().rows.map(row => (
<ValidationRow key={row.id} row={row} isReorderable={table.getState().sorting.length === 0} />
<ValidationRow
key={row.id}
row={row}
isReorderable={table.getState().sorting.length === 0}
onDrag={() => handleRowDrag(row)}
onClick={event => handleMultiSelectOnCtrlRowClick(table, row, event)}
updateOrder={updateOrder}
/>
))}
</TableBody>
</Table>
Expand Down
26 changes: 1 addition & 25 deletions packages/dataclass-editor/src/master/ValidationRow.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,4 @@
import { customRenderHook } from '../context/test-utils/test-utils';
import type { DataClass } from '../data/dataclass';
import { rowClassName, useUpdateOrder } from './ValidationRow';

test('useUpdateOrder', () => {
const dataClass = {
fields: [{ name: 'field0' }, { name: 'field1' }, { name: 'field2' }, { name: 'field3' }, { name: 'field4' }]
} as DataClass;
let newDataClass = {} as DataClass;
const view = customRenderHook(() => useUpdateOrder(), {
wrapperProps: { appContext: { dataClass, setDataClass: dataClass => (newDataClass = dataClass) } }
});

const originalDataClass = structuredClone(dataClass);
view.result.current('0', '2');
expect(dataClass).toEqual(originalDataClass);

expect(newDataClass.fields).toEqual([{ name: 'field1' }, { name: 'field2' }, { name: 'field0' }, { name: 'field3' }, { name: 'field4' }]);

view.result.current('2', '4');
expect(newDataClass.fields).toEqual([{ name: 'field0' }, { name: 'field1' }, { name: 'field3' }, { name: 'field4' }, { name: 'field2' }]);

view.result.current('4', '0');
expect(newDataClass.fields).toEqual([{ name: 'field4' }, { name: 'field0' }, { name: 'field1' }, { name: 'field2' }, { name: 'field3' }]);
});
import { rowClassName } from './ValidationRow';

test('rowClassName', () => {
expect(rowClassName([{ variant: 'info' }, { variant: 'info' }, { variant: 'info' }])).toBeUndefined();
Expand Down
42 changes: 23 additions & 19 deletions packages/dataclass-editor/src/master/ValidationRow.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { arraymove, MessageRow, ReorderRow, SelectRow, TableCell, type MessageData } from '@axonivy/ui-components';
import { MessageRow, ReorderRow, SelectRow, TableCell, type MessageData } from '@axonivy/ui-components';
import { flexRender, type Row } from '@tanstack/react-table';
import { useAppContext } from '../context/AppContext';
import type { DataClassField } from '../data/dataclass';
import './ValidationRow.css';
import { useValidation } from './useValidation';

export const useUpdateOrder = () => {
const { dataClass, setDataClass } = useAppContext();
return (moveId: string, targetId: string) => {
const newDataClass = structuredClone(dataClass);
arraymove(newDataClass.fields, parseInt(moveId), parseInt(targetId));
setDataClass(newDataClass);
};
};

export const rowClassName = (messages: Array<MessageData>) => {
if (messages.some(message => message.variant === 'error')) {
return 'row-error';
Expand All @@ -26,21 +16,35 @@ export const rowClassName = (messages: Array<MessageData>) => {
type ValidationRowProps = {
row: Row<DataClassField>;
isReorderable: boolean;
updateOrder: (moveId: string, targetId: string) => void;
onClick: React.MouseEventHandler<HTMLTableRowElement>;
onDrag: React.DragEventHandler<HTMLTableRowElement>;
};

export const ValidationRow = ({ row, isReorderable }: ValidationRowProps) => {
const updateOrder = useUpdateOrder();
export const ValidationRow = ({ row, isReorderable, updateOrder, onClick, onDrag }: ValidationRowProps) => {
const messages = useValidation(row.original);

const RowComponent = isReorderable ? ReorderRow : SelectRow;
const tableCell = row
.getVisibleCells()
.map(cell => <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>);

const commonProps = {
id: row.index.toString(),
row: row,
className: rowClassName(messages),
onClick: onClick,
onDrag: onDrag
};

return (
<>
<RowComponent id={row.index.toString()} row={row} className={rowClassName(messages)} updateOrder={updateOrder}>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</RowComponent>
{isReorderable ? (
<ReorderRow {...commonProps} updateOrder={updateOrder}>
{tableCell}
</ReorderRow>
) : (
<SelectRow {...commonProps}>{tableCell}</SelectRow>
)}
{messages.map((message, index) => (
<MessageRow key={index} columnCount={3} message={message} />
))}
Expand Down
5 changes: 5 additions & 0 deletions packages/dataclass-editor/src/protocol/client-json-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
RequestTypes,
ValidationMessage
} from './types';
import type { DataClassField } from '../data/dataclass';

export class ClientJsonRpc extends BaseRpcClient implements Client {
protected onDataChangedEmitter = new Emitter<void>();
Expand All @@ -40,6 +41,10 @@ export class ClientJsonRpc extends BaseRpcClient implements Client {
return this.sendRequest('validate', context);
}

function(func: DataClassActionArgs): Promise<Array<DataClassField>> {
return this.sendRequest('function', func);
}

meta<TMeta extends keyof MetaRequestTypes>(path: TMeta, args: MetaRequestTypes[TMeta][0]): Promise<MetaRequestTypes[TMeta][1]> {
return this.sendRequest(path, args);
}
Expand Down
12 changes: 9 additions & 3 deletions packages/dataclass-editor/src/protocol/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { DataClass } from '../data/dataclass';
import type { DataClass, DataClassField } from '../data/dataclass';

export type Data = { context: DataContext; data: DataClass };
export type DataContext = { app: string; pmv: string; file: string };
export type EditorProps = { context: DataContext; directSave?: boolean };
export type SaveArgs = Data & { directSave?: boolean };
export interface DataClassActionArgs {
actionId: 'openForm' | 'openProcess';
payload: string;
actionId: 'openForm' | 'openProcess' | 'combineFields';
payload: string | CombinePayload;
context: DataContext;
}

Expand All @@ -17,6 +17,7 @@ export interface RequestTypes extends MetaRequestTypes {
data: [DataContext, Data];
saveData: [Data, Array<ValidationMessage>];
validate: [DataContext, Array<ValidationMessage>];
function: [DataClassActionArgs, Array<DataClassField>];
}

export interface NotificationTypes {
Expand All @@ -39,6 +40,7 @@ export interface Client {
data(context: DataContext): Promise<Data>;
saveData(saveArgs: SaveArgs): Promise<Array<ValidationMessage>>;
validate(context: DataContext): Promise<Array<ValidationMessage>>;
function(func: DataClassActionArgs): Promise<Array<DataClassField>>;

meta<TMeta extends keyof MetaRequestTypes>(path: TMeta, args: MetaRequestTypes[TMeta][0]): Promise<MetaRequestTypes[TMeta][1]>;

Expand Down Expand Up @@ -76,3 +78,7 @@ export interface DataclassType {
packageName: string;
path: string;
}

export interface CombinePayload {
fieldNames: string[];
}

0 comments on commit f8954ca

Please sign in to comment.