Skip to content

Commit

Permalink
XIVY-15134 responsive association components
Browse files Browse the repository at this point in the history
Update the state of components relevent to configuring the association based on the meta data values.

- Hide the association collapsible entirely if no cardinality is applicable.
  This essentially means that the type of the field is neither an entity nor a list of entities.
  Therefore, no association needs to be configured.
- Always open the association collapsible by default.
  If it's visible in the first place, it means that the type of the field is an entity or a list of entities.
  In this case, the association must be configured.
- Add the cardinality to the query key of the `mappedByFields` endpoint to refresh the 'Mapped by' options on a cardinality change.
- Add tests for the new behaviour and adjust existing tests.
  • Loading branch information
ivy-lgi committed Dec 9, 2024
1 parent 23ed95d commit 286978e
Show file tree
Hide file tree
Showing 14 changed files with 264 additions and 183 deletions.
55 changes: 48 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,18 @@ 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 +113,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
Loading

0 comments on commit 286978e

Please sign in to comment.