Skip to content

Commit

Permalink
Merge pull request #96 from axonivy/XIVY-15134/adjust-states
Browse files Browse the repository at this point in the history
XIVY-15134 responsive association components
  • Loading branch information
ivy-lgi authored Dec 9, 2024
2 parents 23ed95d + df0abf2 commit caee292
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 183 deletions.
53 changes: 46 additions & 7 deletions integrations/standalone/src/mock/dataclass-client-mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { DataActionArgs, DataClassData, ValidationResult } from '@axonivy/dataclass-editor-protocol/src/editor';
import type { Client, Event, FunctionRequestTypes, MetaRequestTypes } from '@axonivy/dataclass-editor-protocol/src/types';
import type { DataActionArgs, DataClassData, FieldContext, ValidationResult } from '@axonivy/dataclass-editor-protocol/src/editor';
import type {
Client,
Event,
FunctionRequestTypes,
MappedByFieldsContext,
MetaRequestTypes
} from '@axonivy/dataclass-editor-protocol/src/types';

export class DataClassClientMock implements Client {
private dataClassData: DataClassData = {
Expand Down Expand Up @@ -42,6 +48,20 @@ export class DataClassClientMock implements Client {
modifiers: [],
comment: 'Transcript of the conversation.',
annotations: []
},
{
name: 'entity',
type: 'mock.Entity',
modifiers: ['PERSISTENT'],
comment: 'An entity.',
annotations: []
},
{
name: 'entities',
type: 'List<mock.Entity>',
modifiers: ['PERSISTENT'],
comment: 'A list of entities.',
annotations: []
}
]
},
Expand Down Expand Up @@ -70,16 +90,16 @@ export class DataClassClientMock implements Client {
}
}

meta<TMeta extends keyof MetaRequestTypes>(path: TMeta): Promise<MetaRequestTypes[TMeta][1]> {
meta<TMeta extends keyof MetaRequestTypes>(path: TMeta, args: MetaRequestTypes[TMeta][0]): Promise<MetaRequestTypes[TMeta][1]> {
switch (path) {
case 'meta/scripting/ivyTypes':
return Promise.resolve([]);
case 'meta/scripting/dataClasses':
return Promise.resolve([]);
case 'meta/scripting/cardinalities':
return Promise.resolve(['ONE_TO_ONE', 'ONE_TO_MANY', 'MANY_TO_ONE']);
case 'meta/scripting/mappedByFields':
return Promise.resolve(['MappedByFieldName']);
case 'meta/scripting/cardinalities':
return Promise.resolve(cardinalities(args as FieldContext));
case 'meta/scripting/mappedByFields':
return Promise.resolve(mappedByFields(args as MappedByFieldsContext));
default:
throw Error('mock meta path not programmed');
}
Expand All @@ -91,3 +111,22 @@ export class DataClassClientMock implements Client {

onDataChanged: Event<void>;
}

const cardinalities = (context: FieldContext) => {
const cardinalities = [];
if (context.field.startsWith('entity')) {
cardinalities.push('ONE_TO_ONE', 'MANY_TO_ONE');
}
if (context.field.startsWith('entities')) {
cardinalities.push('ONE_TO_MANY');
}
return cardinalities;
};

const mappedByFields = (context: MappedByFieldsContext) => {
const mappedByFields = [];
if (context.cardinality === 'ONE_TO_ONE') {
mappedByFields.push('MappedByFieldName');
}
return mappedByFields;
};
23 changes: 21 additions & 2 deletions packages/dataclass-editor/src/data/dataclass-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DataClass } from '@axonivy/dataclass-editor-protocol';
import { classTypeOf, isEntity } from './dataclass-utils';
import type { DataClass, Field, Modifier } from '@axonivy/dataclass-editor-protocol';
import { classTypeOf, isEntity, isEntityField } from './dataclass-utils';

describe('classTypeOf', () => {
test('data', () => {
Expand Down Expand Up @@ -29,3 +29,22 @@ describe('isEntity', () => {
expect(isEntity(dataClass)).toBeFalsy();
});
});

describe('isEntityField', () => {
test('true', () => {
const field = { modifiers: ['PERSISTENT'], entity: {} } as Field;
expect(isEntityField(field)).toBeTruthy();
});

describe('false', () => {
test('missing modifier persistent', () => {
const field = { modifiers: [] as Array<Modifier>, entity: {} } as Field;
expect(isEntityField(field)).toBeFalsy();
});

test('missing entity', () => {
const field = { modifiers: ['PERSISTENT'] } as Field;
expect(isEntityField(field)).toBeFalsy();
});
});
});
20 changes: 19 additions & 1 deletion packages/dataclass-editor/src/data/dataclass-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { type DataClass, type Modifier, type DataClassType, type EntityDataClass } from '@axonivy/dataclass-editor-protocol';
import {
type Association,
type DataClass,
type DataClassType,
type EntityClassField,
type EntityDataClass,
type Field,
type Modifier
} from '@axonivy/dataclass-editor-protocol';

export const classTypeOf = (dataClass: DataClass): DataClassType => {
if (dataClass.entity) {
Expand All @@ -14,6 +22,10 @@ export const isEntity = (dataClass: DataClass): dataClass is EntityDataClass =>
return !!dataClass.entity;
};

export const isEntityField = (field: Field): field is EntityClassField => {
return !!field.entity && field.modifiers.includes('PERSISTENT');
};

export const updateModifiers = (add: boolean, newModifiers: Array<Modifier>, modifier: Modifier) => {
if (add) {
if (modifier === 'ID' || modifier === 'VERSION') {
Expand All @@ -28,3 +40,9 @@ export const updateModifiers = (add: boolean, newModifiers: Array<Modifier>, mod
}
return newModifiers;
};

export const updateCardinality = (newField: EntityClassField, association?: Association) => {
newField.entity.mappedByFieldName = '';
newField.entity.orphanRemoval = false;
newField.entity.association = association;
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Flex } from '@axonivy/ui-components';
import { EntityFieldProvider, useField } from '../../context/FieldContext';
import type { Field, EntityClassField } from '@axonivy/dataclass-editor-protocol';
import { isEntityField } from '../../data/dataclass-utils';
import { AnnotationsTable } from '../AnnotationsTable';
import { FieldEntityAssociation } from './entity/FieldEntityAssociation';
import { FieldEntityDatabaseField } from './entity/FieldEntityDatabaseField';
import { FieldNameTypeComment } from './FieldNameTypeComment';
import { FieldProperties } from './FieldProperties';
import { useFieldProperty } from './useFieldProperty';

export const isEntityField = (field: Field): field is EntityClassField => {
return !!field.entity && field.modifiers.includes('PERSISTENT');
};

export const FieldDetailContent = () => {
const { field, setField } = useField();
const { setProperty } = useFieldProperty();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { customRenderHook } from '../../context/test-utils/test-utils';
import type { Field } from '@axonivy/dataclass-editor-protocol';
import { customRenderHook } from '../../context/test-utils/test-utils';
import { useType } from './FieldNameTypeComment';

describe('useType', () => {
Expand Down Expand Up @@ -34,4 +34,26 @@ describe('useType', () => {
expect(newField.type).toEqual('String');
expect(newField.modifiers).toEqual(['PERSISTENT']);
});

test('clear association', () => {
const field = {
type: 'String',
modifiers: ['PERSISTENT'],
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'MappedByFieldName', orphanRemoval: true }
} as Field;
let newField = {} as Field;
const view = customRenderHook(() => useType(), {
wrapperProps: { fieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.type).toEqual('String');

const originalField = structuredClone(field);
view.result.current.setType('Integer');
expect(field).toEqual(originalField);

expect(newField.type).toEqual('Integer');
expect(newField.entity!.association).toBeUndefined();
expect(newField.entity!.mappedByFieldName).toEqual('');
expect(newField.entity!.orphanRemoval).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { isIDType, isVersionType } from '@axonivy/dataclass-editor-protocol';
import { BasicField, BasicInput, Collapsible, CollapsibleContent, CollapsibleTrigger, Flex, Textarea } from '@axonivy/ui-components';
import { useField } from '../../context/FieldContext';
import { isIDType, isVersionType } from '@axonivy/dataclass-editor-protocol';
import { updateModifiers } from '../../data/dataclass-utils';
import { useFieldProperty } from './useFieldProperty';
import { isEntityField, updateCardinality, updateModifiers } from '../../data/dataclass-utils';
import { InputFieldWithTypeBrowser } from './InputFieldWithTypeBrowser';
import { useFieldProperty } from './useFieldProperty';

export const useType = () => {
const { field, setField } = useField();
Expand All @@ -15,6 +15,9 @@ export const useType = () => {
if (!isVersionType(type)) {
newField.modifiers = updateModifiers(false, newField.modifiers, 'VERSION');
}
if (isEntityField(newField)) {
updateCardinality(newField);
}
newField.type = type;
setField(newField);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { customRenderHook } from '../../../context/test-utils/test-utils';
import type { Association, EntityClassField } from '@axonivy/dataclass-editor-protocol';
import { customRenderHook } from '../../../context/test-utils/test-utils';
import { useCardinality, useMappedByFieldName } from './FieldEntityAssociation';

describe('useMappedByFieldName', () => {
Expand Down Expand Up @@ -61,86 +61,79 @@ describe('useMappedByFieldName', () => {
});

describe('useCardinality', () => {
const expectAssociation = (
field: EntityClassField,
association: Association | undefined,
mappedByFieldName: string,
orphanRemoval: boolean
) => {
expect(field.entity.association).toEqual(association);
expect(field.entity.mappedByFieldName).toEqual(mappedByFieldName);
expect(field.entity.orphanRemoval).toEqual(orphanRemoval);
};

describe('clear properties', () => {
test('to none', () => {
const field = {
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_ONE');

const originalField = structuredClone(field);
view.result.current.setCardinality(undefined as unknown as Association);
expect(field).toEqual(originalField);

expectAssociation(newField, undefined, '', false);
test('to none', () => {
const field = {
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_ONE');

test('to many-to-one', () => {
const field = {
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_ONE');
const originalField = structuredClone(field);
view.result.current.setCardinality(undefined as unknown as Association);
expect(field).toEqual(originalField);

const originalDataClass = structuredClone(field);
view.result.current.setCardinality('MANY_TO_ONE');
expect(field).toEqual(originalDataClass);
expect(newField.entity.association).toBeUndefined();
expect(newField.entity.mappedByFieldName).toEqual('');
expect(newField.entity.orphanRemoval).toBeFalsy();
});

expectAssociation(newField, 'MANY_TO_ONE', '', false);
test('to many-to-one', () => {
const field = {
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
});
expect(view.result.current.cardinality).toEqual('ONE_TO_ONE');

describe('keep properties', () => {
test('to one-to-one', () => {
const field = {
entity: { association: 'ONE_TO_MANY', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_MANY');
const originalDataClass = structuredClone(field);
view.result.current.setCardinality('MANY_TO_ONE');
expect(field).toEqual(originalDataClass);

const originalField = structuredClone(field);
view.result.current.setCardinality('ONE_TO_ONE');
expect(field).toEqual(originalField);
expect(newField.entity.association).toEqual('MANY_TO_ONE');
expect(newField.entity.mappedByFieldName).toEqual('');
expect(newField.entity.orphanRemoval).toBeFalsy();
});

expectAssociation(newField, 'ONE_TO_ONE', 'mappedByFieldName', true);
test('to one-to-one', () => {
const field = {
entity: { association: 'ONE_TO_MANY', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_MANY');

test('to many-to-one', () => {
const field = {
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_ONE');
const originalField = structuredClone(field);
view.result.current.setCardinality('ONE_TO_ONE');
expect(field).toEqual(originalField);

const originalField = structuredClone(field);
view.result.current.setCardinality('ONE_TO_MANY');
expect(field).toEqual(originalField);
expect(newField.entity.association).toEqual('ONE_TO_ONE');
expect(newField.entity.mappedByFieldName).toEqual('');
expect(newField.entity.orphanRemoval).toBeFalsy();
});

expectAssociation(newField, 'ONE_TO_MANY', 'mappedByFieldName', true);
test('to many-to-one', () => {
const field = {
entity: { association: 'ONE_TO_ONE', mappedByFieldName: 'mappedByFieldName', orphanRemoval: true }
} as EntityClassField;
let newField = {} as EntityClassField;
const view = customRenderHook(() => useCardinality(), {
wrapperProps: { entityFieldContext: { field, setField: field => (newField = field) } }
});
expect(view.result.current.cardinality).toEqual('ONE_TO_ONE');

const originalField = structuredClone(field);
view.result.current.setCardinality('ONE_TO_MANY');
expect(field).toEqual(originalField);

expect(newField.entity.association).toEqual('ONE_TO_MANY');
expect(newField.entity.mappedByFieldName).toEqual('');
expect(newField.entity.orphanRemoval).toBeFalsy();
});
});
Loading

0 comments on commit caee292

Please sign in to comment.