diff --git a/integrations/standalone/src/mock/dataclass-client-mock.ts b/integrations/standalone/src/mock/dataclass-client-mock.ts index 45e20a9a..bb45591c 100644 --- a/integrations/standalone/src/mock/dataclass-client-mock.ts +++ b/integrations/standalone/src/mock/dataclass-client-mock.ts @@ -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 = { @@ -74,9 +73,13 @@ export class DataClassClientMock implements Client { meta(path: TMeta): Promise { 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'); } diff --git a/integrations/standalone/src/mock/meta-mock.ts b/integrations/standalone/src/mock/meta-mock.ts deleted file mode 100644 index fb7cc459..00000000 --- a/integrations/standalone/src/mock/meta-mock.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { DataclassType, JavaType } from '@axonivy/dataclass-editor-protocol'; - -export namespace MetaMock { - export const IVYTYPES: Array = []; - export const DATACLASSES: Array = []; -} diff --git a/packages/dataclass-editor/src/detail/field/entity/FieldEntityAssociation.tsx b/packages/dataclass-editor/src/detail/field/entity/FieldEntityAssociation.tsx index 1130b0a0..37b453db 100644 --- a/packages/dataclass-editor/src/detail/field/entity/FieldEntityAssociation.tsx +++ b/packages/dataclass-editor/src/detail/field/entity/FieldEntityAssociation.tsx @@ -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'; @@ -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 ( Association - + @@ -69,9 +73,11 @@ export const FieldEntityAssociation = () => { - setMappedByFieldName(event.target.value)} + emptyItem + items={mappedByFields} + onValueChange={setMappedByFieldName} disabled={mappedByFieldNameIsDisabled} /> diff --git a/packages/protocol/src/editor.ts b/packages/protocol/src/editor.ts index 0c4a555f..9faa3795 100644 --- a/packages/protocol/src/editor.ts +++ b/packages/protocol/src/editor.ts @@ -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 { @@ -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; @@ -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; diff --git a/packages/protocol/src/types.ts b/packages/protocol/src/types.ts index 78e761a3..ed9410f1 100644 --- a/packages/protocol/src/types.ts +++ b/packages/protocol/src/types.ts @@ -6,6 +6,8 @@ import type { DataClassData, DataClassSaveDataArgs, DataclassType, + EntityClassFieldAssociation, + FieldContext, JavaType, TypeSearchRequest, ValidationResult @@ -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]; + 'meta/scripting/ivyTypes': [void, Array]; + 'meta/scripting/allTypes': [TypeSearchRequest, Array]; + 'meta/scripting/ownTypes': [TypeSearchRequest, Array]; + 'meta/scripting/cardinalities': [FieldContext, Array]; + 'meta/scripting/mappedByFields': [FieldContext, Array]; } export interface FunctionRequestTypes { diff --git a/playwright/dataclass-test-project/dataclasses/dataclass/AnotherEntityClass.d.json b/playwright/dataclass-test-project/dataclasses/dataclass/AnotherEntityClass.d.json index e3f7cd6b..e820f148 100644 --- a/playwright/dataclass-test-project/dataclasses/dataclass/AnotherEntityClass.d.json +++ b/playwright/dataclass-test-project/dataclasses/dataclass/AnotherEntityClass.d.json @@ -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" : { diff --git a/playwright/dataclass-test-project/dataclasses/dataclass/EntityClass.d.json b/playwright/dataclass-test-project/dataclasses/dataclass/EntityClass.d.json index c8da7c9d..559fd89f 100644 --- a/playwright/dataclass-test-project/dataclasses/dataclass/EntityClass.d.json +++ b/playwright/dataclass-test-project/dataclasses/dataclass/EntityClass.d.json @@ -33,7 +33,7 @@ "entity" : { "association" : "ONE_TO_ONE", "cascadeTypes" : [ "REMOVE", "REFRESH" ], - "mappedByFieldName" : "invers", + "mappedByFieldName" : "inverse", "orphanRemoval" : true } } ] diff --git a/playwright/tests/global.teardown.ts b/playwright/tests/global.teardown.ts index a5e877d3..897fb937 100644 --- a/playwright/tests/global.teardown.ts +++ b/playwright/tests/global.teardown.ts @@ -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; diff --git a/playwright/tests/integration/entityclass.spec.ts b/playwright/tests/integration/entityclass.spec.ts index e06ff304..69a6b46f 100644 --- a/playwright/tests/integration/entityclass.spec.ts +++ b/playwright/tests/integration/entityclass.spec.ts @@ -33,7 +33,7 @@ test('load data', async () => { remove: true, refresh: true }, - 'invers', + 'inverse', true ); }); @@ -69,7 +69,7 @@ test('save data', async ({ page }) => { remove: true, refresh: true }, - 'NewMappedByFieldName', + undefined, true ); @@ -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'); +}); diff --git a/playwright/tests/integration/mock/detail-dataclass-entity.spec.ts b/playwright/tests/integration/mock/detail-dataclass-entity.spec.ts index e5231176..8fb6663f 100644 --- a/playwright/tests/integration/mock/detail-dataclass-entity.spec.ts +++ b/playwright/tests/integration/mock/detail-dataclass-entity.spec.ts @@ -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(); diff --git a/playwright/tests/integration/mock/detail-field-entity-database-field.spec.ts b/playwright/tests/integration/mock/detail-field-entity-database-field.spec.ts index decbfd66..3f3538ef 100644 --- a/playwright/tests/integration/mock/detail-field-entity-database-field.spec.ts +++ b/playwright/tests/integration/mock/detail-field-entity-database-field.spec.ts @@ -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'); }); @@ -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({ diff --git a/playwright/tests/pageobjects/abstract/Select.ts b/playwright/tests/pageobjects/abstract/Select.ts index 18b423fa..96a93747 100644 --- a/playwright/tests/pageobjects/abstract/Select.ts +++ b/playwright/tests/pageobjects/abstract/Select.ts @@ -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; @@ -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) { + await this.locator.click(); + await expect(this.page.getByRole('option')).toHaveText(options); + await this.page.keyboard.press('Escape'); + } } diff --git a/playwright/tests/pageobjects/field/entity/FieldAssociation.ts b/playwright/tests/pageobjects/field/entity/FieldAssociation.ts index c3f18bf3..194c64ae 100644 --- a/playwright/tests/pageobjects/field/entity/FieldAssociation.ts +++ b/playwright/tests/pageobjects/field/entity/FieldAssociation.ts @@ -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 }; @@ -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) { @@ -28,7 +27,7 @@ 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'); } @@ -36,7 +35,7 @@ export class FieldAssociation { 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); } @@ -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(); }