Skip to content

Commit

Permalink
Merge pull request #95 from axonivy/XIVY-15134/cardinality
Browse files Browse the repository at this point in the history
XIVY-15134 cardinality and mappedBy options
  • Loading branch information
ivy-lgi authored Dec 6, 2024
2 parents df8613c + eb9b635 commit 23ed95d
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 45 deletions.
11 changes: 7 additions & 4 deletions integrations/standalone/src/mock/dataclass-client-mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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, ValidationResult, DataClassData } from '@axonivy/dataclass-editor-protocol/src/editor';
import { MetaMock } from './meta-mock';

export class DataClassClientMock implements Client {
private dataClassData: DataClassData = {
Expand Down Expand Up @@ -74,9 +73,13 @@ export class DataClassClientMock implements Client {
meta<TMeta extends keyof MetaRequestTypes>(path: TMeta): Promise<MetaRequestTypes[TMeta][1]> {
switch (path) {
case 'meta/scripting/ivyTypes':
return Promise.resolve(MetaMock.IVYTYPES);
return Promise.resolve([]);
case 'meta/scripting/dataClasses':
return Promise.resolve(MetaMock.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']);
default:
throw Error('mock meta path not programmed');
}
Expand Down
6 changes: 0 additions & 6 deletions integrations/standalone/src/mock/meta-mock.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import {
BasicCheckbox,
BasicField,
BasicSelect,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Flex,
Input
} from '@axonivy/ui-components';
import { useEntityField } from '../../../context/FieldContext';
import type { Association } from '@axonivy/dataclass-editor-protocol';
import { BasicCheckbox, BasicField, BasicSelect, Collapsible, CollapsibleContent, CollapsibleTrigger, Flex } from '@axonivy/ui-components';
import { useAppContext } from '../../../context/AppContext';
import { useEntityField } from '../../../context/FieldContext';
import { useMeta } from '../../../context/useMeta';
import './FieldEntityAssociation.css';
import { FieldEntityCascadeTypeCheckbox } from './FieldEntityCascadeTypeCheckbox';
import { useFieldEntityProperty } from './useFieldEntityProperty';
Expand Down Expand Up @@ -47,17 +40,28 @@ const cardinalityItems: Array<{ value: Association; label: string }> = [
] as const;

export const FieldEntityAssociation = () => {
const { context } = useAppContext();
const { field, setProperty } = useFieldEntityProperty();
const { mappedByFieldName, setMappedByFieldName, isDisabled: mappedByFieldNameIsDisabled } = useMappedByFieldName();
const { cardinality, setCardinality } = useCardinality();

const fieldContext = { ...context, field: field.name };

const possibleCardinalities = useMeta('meta/scripting/cardinalities', fieldContext, []).data;
const cardinalities = cardinalityItems.filter(cardinality => possibleCardinalities.includes(cardinality.value));

const mappedByFields = useMeta('meta/scripting/mappedByFields', fieldContext, []).data.map(mappedByField => ({
value: mappedByField,
label: mappedByField
}));

return (
<Collapsible>
<CollapsibleTrigger>Association</CollapsibleTrigger>
<CollapsibleContent>
<Flex direction='column' gap={4}>
<BasicField label='Cardinality'>
<BasicSelect value={cardinality} emptyItem items={cardinalityItems} onValueChange={setCardinality} />
<BasicSelect value={cardinality} emptyItem items={cardinalities} onValueChange={setCardinality} />
</BasicField>
<BasicField label='Cascade'>
<FieldEntityCascadeTypeCheckbox label='All' cascadeType='ALL' />
Expand All @@ -69,9 +73,11 @@ export const FieldEntityAssociation = () => {
</Flex>
</BasicField>
<BasicField label='Mapped by'>
<Input
<BasicSelect
value={mappedByFieldName}
onChange={event => setMappedByFieldName(event.target.value)}
emptyItem
items={mappedByFields}
onValueChange={setMappedByFieldName}
disabled={mappedByFieldNameIsDisabled}
/>
</BasicField>
Expand Down
10 changes: 10 additions & 0 deletions packages/protocol/src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Modifier =
| "NOT_UPDATEABLE"
| "NOT_INSERTABLE"
| "VERSION";
export type EntityClassFieldAssociation = "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_ONE";
export type Severity = "INFO" | "WARNING" | "ERROR";

export interface DataClasses {
Expand All @@ -27,7 +28,10 @@ export interface DataClasses {
dataClassModel: DataClassModel;
dataClassSaveDataArgs: DataClassSaveDataArgs;
dataclassType: DataclassType[];
entityClassFieldAssociation: EntityClassFieldAssociation[];
fieldContext: FieldContext;
javaType: JavaType[];
string: string[];
typeSearchRequest: TypeSearchRequest;
validationResult: ValidationResult[];
void: Void;
Expand Down Expand Up @@ -91,6 +95,12 @@ export interface DataclassType {
packageName: string;
path: string;
}
export interface FieldContext {
app: string;
field: string;
file: string;
pmv: string;
}
export interface JavaType {
fullQualifiedName: string;
packageName: string;
Expand Down
12 changes: 8 additions & 4 deletions packages/protocol/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
DataClassData,
DataClassSaveDataArgs,
DataclassType,
EntityClassFieldAssociation,
FieldContext,
JavaType,
TypeSearchRequest,
ValidationResult
Expand Down Expand Up @@ -57,10 +59,12 @@ export interface ClientContext {
}

export interface MetaRequestTypes {
'meta/scripting/dataClasses': [DataClassEditorDataContext, DataclassType[]];
'meta/scripting/ivyTypes': [void, JavaType[]];
'meta/scripting/allTypes': [TypeSearchRequest, JavaType[]];
'meta/scripting/ownTypes': [TypeSearchRequest, JavaType[]];
'meta/scripting/dataClasses': [DataClassEditorDataContext, Array<DataclassType>];
'meta/scripting/ivyTypes': [void, Array<JavaType>];
'meta/scripting/allTypes': [TypeSearchRequest, Array<JavaType>];
'meta/scripting/ownTypes': [TypeSearchRequest, Array<JavaType>];
'meta/scripting/cardinalities': [FieldContext, Array<EntityClassFieldAssociation>];
'meta/scripting/mappedByFields': [FieldContext, Array<string>];
}

export interface FunctionRequestTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
"orphanRemoval" : false
}
}, {
"name" : "invers",
"name" : "inverse",
"type" : "dataclass.EntityClass",
"modifiers" : [ "PERSISTENT" ],
"entity" : {
"association" : "ONE_TO_ONE",
"cascadeTypes" : [ "PERSIST", "MERGE" ],
"orphanRemoval" : false
}
}, {
"name" : "anotherInverse",
"type" : "dataclass.EntityClass",
"modifiers" : [ "PERSISTENT" ],
"entity" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"entity" : {
"association" : "ONE_TO_ONE",
"cascadeTypes" : [ "REMOVE", "REFRESH" ],
"mappedByFieldName" : "invers",
"mappedByFieldName" : "inverse",
"orphanRemoval" : true
}
} ]
Expand Down
2 changes: 1 addition & 1 deletion playwright/tests/global.teardown.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { rm } from 'node:fs';

const teardown = async () => {
rm('./tests/integration/projects/dataclass-test-project/dataclasses/temp', { force: true, recursive: true }, () => {});
rm('./dataclass-test-project/dataclasses/temp', { force: true, recursive: true }, () => {});
};

export default teardown;
14 changes: 11 additions & 3 deletions playwright/tests/integration/entityclass.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test('load data', async () => {
remove: true,
refresh: true
},
'invers',
'inverse',
true
);
});
Expand Down Expand Up @@ -69,7 +69,7 @@ test('save data', async ({ page }) => {
remove: true,
refresh: true
},
'NewMappedByFieldName',
undefined,
true
);

Expand Down Expand Up @@ -100,7 +100,15 @@ test('save data', async ({ page }) => {
remove: true,
refresh: true
},
'NewMappedByFieldName',
'',
true
);
});

test('association', async () => {
await editor.table.row(2).locator.click();
await editor.detail.field.entity.accordion.open();
await editor.detail.field.entity.association.collapsible.open();
await editor.detail.field.entity.association.cardinality.expectToHaveOptions('', 'One-to-One', 'Many-to-One');
await editor.detail.field.entity.association.mappedBy.expectToHaveOptions('', 'inverse', 'anotherInverse');
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('collapsible state', async () => {

await entity.association.collapsible.open();
await entity.association.cardinality.choose('One-to-One');
await entity.association.mappedBy.locator.fill('MappedByFieldName');
await entity.association.mappedBy.choose('MappedByFieldName');
await entity.accordion.reopen();

await entity.databaseField.collapsible.expectToBeClosed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ test('name', async () => {
await databaseFieldName.locator.fill('DatabaseFieldName');
await editor.detail.field.entity.association.collapsible.open();
await editor.detail.field.entity.association.cardinality.choose('One-to-One');
await editor.detail.field.entity.association.mappedBy.locator.fill('MappedByFieldName');
await editor.detail.field.entity.association.mappedBy.choose('MappedByFieldName');

await expect(databaseFieldName.locator).toHaveValue('');
await databaseFieldName.expectToHavePlaceholder('');
await expect(databaseFieldName.locator).toBeDisabled();

await editor.detail.field.entity.association.mappedBy.locator.clear();
await editor.detail.field.entity.association.mappedBy.choose('');

await expect(databaseFieldName.locator).toHaveValue('DatabaseFieldName');
});
Expand Down Expand Up @@ -131,7 +131,7 @@ test('properties', async () => {
await databaseField.properties.Version.click();
await editor.detail.field.entity.association.collapsible.open();
await editor.detail.field.entity.association.cardinality.choose('One-to-One');
await editor.detail.field.entity.association.mappedBy.locator.fill('MappedByFieldName');
await editor.detail.field.entity.association.mappedBy.choose('MappedByFieldName');

// mappedByFieldName is set
await databaseField.expectPropertiesToHaveEnabledState({
Expand Down
8 changes: 7 additions & 1 deletion playwright/tests/pageobjects/abstract/Select.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { expect, type Locator, type Page } from '@playwright/test';

export class Select {
readonly page: Page;
Expand All @@ -17,4 +17,10 @@ export class Select {
await this.locator.click();
await this.page.getByRole('option', { name: value, exact: true }).first().click();
}

async expectToHaveOptions(...options: Array<string>) {
await this.locator.click();
await expect(this.page.getByRole('option')).toHaveText(options);
await this.page.keyboard.press('Escape');
}
}
13 changes: 7 additions & 6 deletions playwright/tests/pageobjects/field/entity/FieldAssociation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { Collapsible } from '../../abstract/Collapsible';
import { Select } from '../../abstract/Select';
import { TextArea } from '../../abstract/TextArea';

export type FieldAssociationCascadeTypes = { [K in keyof FieldAssociation['cascadeTypes']]: boolean };

Expand All @@ -15,7 +14,7 @@ export class FieldAssociation {
remove: Locator;
refresh: Locator;
};
readonly mappedBy: TextArea;
readonly mappedBy: Select;
readonly removeOrphans: Locator;

constructor(page: Page, parentLocator: Locator) {
Expand All @@ -28,15 +27,15 @@ export class FieldAssociation {
remove: this.collapsible.locator.getByLabel('Remove', { exact: true }),
refresh: this.collapsible.locator.getByLabel('Refresh')
};
this.mappedBy = new TextArea(this.collapsible.locator, { label: 'Mapped by' });
this.mappedBy = new Select(page, this.collapsible.locator, { label: 'Mapped by' });
this.removeOrphans = this.collapsible.locator.getByLabel('Remove orphans');
}

async expectToHaveValues(cardinality: string, cascadeTypes: FieldAssociationCascadeTypes, mappedBy: string, removeOrphans: boolean) {
await this.collapsible.open();
await expect(this.cardinality.locator).toHaveText(cardinality);
await this.expectCascadeTypesToHaveCheckedState(cascadeTypes);
await expect(this.mappedBy.locator).toHaveValue(mappedBy);
await expect(this.mappedBy.locator).toHaveText(mappedBy);
expect(await this.removeOrphans.isChecked()).toEqual(removeOrphans);
}

Expand All @@ -62,11 +61,13 @@ export class FieldAssociation {
}
}

async fillValues(cardinality: string, cascadeTypes: FieldAssociationCascadeTypes, mappedBy: string, removeOrphans: boolean) {
async fillValues(cardinality: string, cascadeTypes: FieldAssociationCascadeTypes, mappedBy: string | undefined, removeOrphans: boolean) {
await this.collapsible.open();
await this.cardinality.choose(cardinality);
await this.fillCascadeTypes(cascadeTypes);
await this.mappedBy.locator.fill(mappedBy);
if (mappedBy !== undefined) {
await this.mappedBy.choose(mappedBy);
}
if (removeOrphans !== (await this.removeOrphans.isChecked())) {
await this.removeOrphans.click();
}
Expand Down

0 comments on commit 23ed95d

Please sign in to comment.