From 6ca4fe5ac6642b9715b8020b725eb6c853e76143 Mon Sep 17 00:00:00 2001 From: ThibaudAv Date: Tue, 1 Oct 2024 19:02:11 +0200 Subject: [PATCH] feat: init new gio-el-editor to build EL with UI Fist step for basic UI component --- .../ui-particles-angular/gio-el/README.mdx | 15 + ...o-el-editor-condition-group.component.html | 100 +++++++ ...o-el-editor-condition-group.component.scss | 56 ++++ ...gio-el-editor-condition-group.component.ts | 125 ++++++++ .../gio-el-editor-condition-group.harness.ts | 199 +++++++++++++ .../gio-el-editor.component.html | 28 ++ .../gio-el-editor.component.scss | 44 +++ .../gio-el-editor.component.spec.ts | 278 ++++++++++++++++++ .../gio-el-editor/gio-el-editor.component.ts | 175 +++++++++++ .../gio-el-editor/gio-el-editor.harness.ts | 24 ++ .../gio-el-editor/gio-el-editor.stories.ts | 61 ++++ .../gio-el-editor-type-boolean.component.html | 34 +++ .../gio-el-editor-type-boolean.component.scss | 24 ++ .../gio-el-editor-type-boolean.component.ts | 57 ++++ .../gio-el-editor-type-boolean.harness.ts | 34 +++ .../gio-el-editor-type-date.component.html | 38 +++ .../gio-el-editor-type-date.component.scss | 23 ++ .../gio-el-editor-type-date.component.ts | 80 +++++ .../gio-el-editor-type-date.harness.ts | 32 ++ .../gio-el-editor-type-number.component.html | 37 +++ .../gio-el-editor-type-number.component.scss | 23 ++ .../gio-el-editor-type-number.component.ts | 69 +++++ .../gio-el-editor-type-number.harness.ts | 32 ++ .../gio-el-editor-type-string.component.html | 41 +++ .../gio-el-editor-type-string.component.scss | 18 ++ .../gio-el-editor-type-string.component.ts | 78 +++++ .../gio-el-editor-type-string.harness.ts | 32 ++ .../gio-el-editor-type.component.scss | 24 ++ .../gio-el-type/gio-el-editor-type.harness.ts | 42 +++ .../gio-el/models/Condition.ts | 35 +++ .../gio-el/models/ConditionGroup.ts | 23 ++ .../gio-el/models/ConditionModel.ts | 35 +++ .../gio-el/models/ConditionsModel.ts | 24 ++ .../models/ExpressionLanguageBuilder.spec.ts | 107 +++++++ .../models/ExpressionLanguageBuilder.ts | 86 ++++++ .../gio-el/models/Operator.ts | 26 ++ .../gio-el/models/public-api.ts | 21 ++ .../gio-el/ng-package.json | 6 + .../ui-particles-angular/gio-el/public-api.ts | 19 ++ projects/ui-particles-angular/jest.config.js | 8 +- 40 files changed, 2212 insertions(+), 1 deletion(-) create mode 100644 projects/ui-particles-angular/gio-el/README.mdx create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.html create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.html create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.spec.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.stories.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.html create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.html create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.html create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.html create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.component.scss create mode 100644 projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.harness.ts create mode 100644 projects/ui-particles-angular/gio-el/models/Condition.ts create mode 100644 projects/ui-particles-angular/gio-el/models/ConditionGroup.ts create mode 100644 projects/ui-particles-angular/gio-el/models/ConditionModel.ts create mode 100644 projects/ui-particles-angular/gio-el/models/ConditionsModel.ts create mode 100644 projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.spec.ts create mode 100644 projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.ts create mode 100644 projects/ui-particles-angular/gio-el/models/Operator.ts create mode 100644 projects/ui-particles-angular/gio-el/models/public-api.ts create mode 100644 projects/ui-particles-angular/gio-el/ng-package.json create mode 100644 projects/ui-particles-angular/gio-el/public-api.ts diff --git a/projects/ui-particles-angular/gio-el/README.mdx b/projects/ui-particles-angular/gio-el/README.mdx new file mode 100644 index 000000000..60a6bea31 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/README.mdx @@ -0,0 +1,15 @@ +import { Meta, Story } from '@storybook/blocks'; + + + +# Expression Language (EL) + +This module provides an expression language (EL) builder for Gravitee.io. + +NOTE : WIP ! + +# Notes + +- Impl custom Operators ? +- Impl IN operator ? +- Impl Condition dag and drop ? diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.html b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.html new file mode 100644 index 000000000..9e361fc4e --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.html @@ -0,0 +1,100 @@ + +
+
+
+ + AND + OR + + +
+ + + + + +
+ + @if (nodeLvl > 0) { + + } +
+ +
+ @for (condition of conditionGroupFormGroup.controls.conditions.controls; track conditionIndex; let conditionIndex = $index) { + @if (isConditionGroupForm(condition)) { + + } @else { +
+ + Field + + + {{ field.label }} + + + + @switch (condition.controls.field.value?.type) { + @case ('string') { + + } + @case ('boolean') { + + } + @case ('number') { + + } + @case ('date') { + + } + } + + +
+ } + } +
+
diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.scss new file mode 100644 index 000000000..16321d179 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.scss @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.conditionGroup { + display: flex; + flex-direction: column; + gap: 8px; + + &__header { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .gio-button-toggle-group { + margin-bottom: 0; + } + } + + &__conditions { + display: flex; + flex-direction: column; + gap: 8px; + + &__condition { + display: flex; + align-items: flex-start; + gap: 8px; + + &__field { + flex: 0 0 auto; + } + + &__removeBtn { + align-self: center; + } + } + + &__conditionGroup { + margin-left: 18px; + } + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.ts new file mode 100644 index 000000000..567b85e2a --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.component.ts @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core'; +import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatMenuModule } from '@angular/material/menu'; +import { GioIconsModule } from '@gravitee/ui-particles-angular'; + +import { GioElEditorTypeBooleanComponent } from '../gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component'; +import { GioElEditorTypeDateComponent } from '../gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component'; +import { GioElEditorTypeNumberComponent } from '../gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component'; +import { GioElEditorTypeStringComponent } from '../gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component'; +import { ConditionModel } from '../../models/ConditionModel'; +import { ConditionForm, ConditionGroupForm } from '../gio-el-editor.component'; + +@Component({ + selector: 'gio-el-editor-condition-group', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + MatFormFieldModule, + MatSelectModule, + MatButtonModule, + MatButtonToggleModule, + MatMenuModule, + GioIconsModule, + GioElEditorTypeBooleanComponent, + GioElEditorTypeDateComponent, + GioElEditorTypeStringComponent, + GioElEditorTypeNumberComponent, + ], + templateUrl: './gio-el-editor-condition-group.component.html', + styleUrl: './gio-el-editor-condition-group.component.scss', +}) +export class GioElEditorConditionGroupComponent { + @Input({ + required: true, + }) + public conditionGroupFormGroup!: FormGroup; + + /** + * Condition models to generate the fields for the conditions. + */ + @Input({ + required: true, + }) + public conditionModels: ConditionModel[] = []; + + /** + * Level of the node in the tree. Useful for testing with Harness to limit the scope of the query. + */ + @Input() + @HostBinding('attr.node-lvl') + public nodeLvl = 0; + + @Output() + public remove = new EventEmitter(); + + protected addConditionGroup() { + this.conditionGroupFormGroup.controls.conditions.push(newConditionGroupFormGroup(), { emitEvent: true }); + this.checkMultipleCondition(); + } + + protected addCondition() { + this.conditionGroupFormGroup.controls.conditions.push(newConditionFormGroup()); + this.checkMultipleCondition(); + } + + protected removeCondition(conditionIndex: number) { + this.conditionGroupFormGroup.controls.conditions.removeAt(conditionIndex); + this.checkMultipleCondition(); + } + + protected removeConditionGroup() { + this.remove.emit(); + this.checkMultipleCondition(); + } + + protected isConditionGroupForm( + formGroup: FormGroup | FormGroup, + ): formGroup is FormGroup { + return 'condition' in formGroup.controls && 'conditions' in formGroup.controls; + } + + private checkMultipleCondition(): void { + if (this.conditionGroupFormGroup.controls.conditions.length > 1) { + this.conditionGroupFormGroup.controls.condition.enable(); + } else { + this.conditionGroupFormGroup.controls.condition.disable(); + } + } +} + +const newConditionGroupFormGroup = (): FormGroup => { + return new FormGroup({ + condition: new FormControl({ value: 'AND', disabled: true }, { nonNullable: true, validators: Validators.required }), + conditions: new FormArray | FormGroup>([]), + }); +}; + +const newConditionFormGroup = (): FormGroup => { + return new FormGroup({ + field: new FormControl(null), + operator: new FormControl(null), + value: new FormControl(null), + }); +}; diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.harness.ts new file mode 100644 index 000000000..934c9c0a6 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor-condition-group/gio-el-editor-condition-group.harness.ts @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BaseHarnessFilters, ComponentHarness, HarnessPredicate, parallel } from '@angular/cdk/testing'; +import { MatMenuHarness } from '@angular/material/menu/testing'; +import { MatSelectHarness } from '@angular/material/select/testing'; +import { MatButtonToggleGroupHarness } from '@angular/material/button-toggle/testing'; +import { DivHarness } from '@gravitee/ui-particles-angular/testing'; + +import { GioElEditorTypeBooleanHarness } from '../gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.harness'; +import { GioElEditorTypeStringHarness } from '../gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.harness'; +import { GioElEditorTypeNumberHarness } from '../gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.harness'; +import { GioElEditorTypeDateHarness } from '../gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.harness'; + +type ConditionGroupHarnessValues = { + condition: 'AND' | 'OR'; + conditions: (ConditionGroupHarnessValues | ConditionHarnessValues)[]; +}; +type ConditionHarnessValues = { + field: string; + operator?: string; + value?: string | boolean | number; +}; + +export class GioElEditorConditionGroupHarness extends ComponentHarness { + public static hostSelector = 'gio-el-editor-condition-group'; + + public static with(options: BaseHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(GioElEditorConditionGroupHarness, options); + } + + public getNodeLvl = () => this.host().then(host => host.getAttribute('node-lvl')); + + private getAddMenuButton = this.locatorFor(MatMenuHarness.with({ triggerText: /Add/ })); + private getConditionButtonToggleGroup = this.locatorFor(MatButtonToggleGroupHarness.with({ selector: '[formControlName="condition"]' })); + private getConditionsHarness = async () => { + const nodeLvl = await this.getNodeLvl(); + if (!nodeLvl) { + throw new Error('Node level not found'); + } + return this.locatorForAll( + DivHarness.with({ + selector: `.conditionGroup__conditions__condition[node-lvl="${nodeLvl}"]`, + }), + GioElEditorConditionGroupHarness.with({ + selector: `.conditionGroup__conditions__conditionGroup[node-lvl="${Number(nodeLvl) + 1}"]`, + }), + )(); + }; + + private getConditionHarness = async (index: number) => (await this.getConditionsHarness()).at(index); + private getConditionTypeHarness = (divHarness: DivHarness) => + divHarness.childLocatorForOptional( + GioElEditorTypeStringHarness, + GioElEditorTypeBooleanHarness, + GioElEditorTypeNumberHarness, + GioElEditorTypeDateHarness, + )(); + private getConditionField = (divHarness: DivHarness) => + divHarness.childLocatorFor(MatSelectHarness.with({ selector: '[formControlName="field"]' }))(); + + public async clickAddNewConditionButton(): Promise { + await (await this.getAddMenuButton()).clickItem({ text: /Add Condition/ }); + } + + public async clickAddNewGroupButton(): Promise { + await (await this.getAddMenuButton()).clickItem({ text: /Add Group/ }); + } + + public async getConditionValue(): Promise<'AND' | 'OR'> { + const conditionButtonToggleGroup = await this.getConditionButtonToggleGroup(); + const selectedToggles = await conditionButtonToggleGroup.getToggles({ checked: true }); + if (selectedToggles.length !== 1) { + throw new Error('No operator selected'); + } + const andOrMap: Record = { + AND: 'AND', + OR: 'OR', + }; + + return andOrMap[await selectedToggles[0].getText()]; + } + + public async selectConditionValue(operator: 'AND' | 'OR'): Promise { + const conditionButtonToggleGroup = await this.getConditionButtonToggleGroup(); + const toggles = await conditionButtonToggleGroup.getToggles({ text: operator }); + if (toggles.length !== 1) { + throw new Error('No operator selected'); + } + return toggles[0].check(); + } + + public async selectConditionField(index: number, field: string): Promise { + const conditionDiv = await this.getConditionHarness(index); + if (!conditionDiv || !(conditionDiv instanceof DivHarness)) { + throw new Error(`Condition with index ${index} not found`); + } + + const fieldSelect = await this.getConditionField(conditionDiv); + await fieldSelect.clickOptions({ text: field }); + } + + public async selectConditionOperator(index: number, operator: string): Promise { + const conditionDiv = await this.getConditionHarness(index); + if (!conditionDiv || !(conditionDiv instanceof DivHarness)) { + throw new Error(`Condition with index ${index} not found`); + } + + const conditionType = await this.getConditionTypeHarness(conditionDiv); + if (!conditionType) { + throw new Error(`Condition type not found. Select field first`); + } + + await conditionType.selectOperator(operator); + } + + public async getConditionAvailableOperators(index: number): Promise { + const conditionDiv = await this.getConditionHarness(index); + if (!conditionDiv || !(conditionDiv instanceof DivHarness)) { + throw new Error(`Condition with index ${index} not found`); + } + + const conditionType = await this.getConditionTypeHarness(conditionDiv); + if (!conditionType) { + throw new Error(`Condition type not found. Select field first`); + } + return conditionType.getAvailableOperators(); + } + + public async setConditionValue(index: number, value: string | boolean | number | Date): Promise { + const conditionDiv = await this.getConditionHarness(index); + if (!conditionDiv || !(conditionDiv instanceof DivHarness)) { + throw new Error(`Condition with index ${index} not found`); + } + + const conditionType = await this.getConditionTypeHarness(conditionDiv); + if (!conditionType) { + throw new Error(`Condition type not found. Select field first`); + } + + if (conditionType instanceof GioElEditorTypeStringHarness && typeof value === 'string') { + await conditionType.setValue(value); + } else if (conditionType instanceof GioElEditorTypeBooleanHarness && typeof value === 'boolean') { + await conditionType.setValue(value); + } else if (conditionType instanceof GioElEditorTypeNumberHarness && typeof value === 'number') { + await conditionType.setValue(value); + } else if (conditionType instanceof GioElEditorTypeDateHarness && value instanceof Date) { + await conditionType.setValue(value); + } else { + throw new Error(`Invalid value for condition type`); + } + } + + public async getConditionGroup(index: number): Promise { + const conditionGroup = await this.getConditionHarness(index); + + if (!conditionGroup || !(conditionGroup instanceof GioElEditorConditionGroupHarness)) { + throw new Error(`Condition group with index ${index} not found`); + } + return conditionGroup; + } + + public async getConditions(): Promise { + const getConditionsDiv = await this.getConditionsHarness(); + + const conditions = await parallel(() => + getConditionsDiv.map(async condition => { + if (condition instanceof GioElEditorConditionGroupHarness) { + return await condition.getConditions(); + } + const conditionField = await this.getConditionField(condition); + + const conditionType = await this.getConditionTypeHarness(condition); + return { + field: await conditionField.getValueText(), + operator: await conditionType?.getOperatorValue(), + value: await conditionType?.getValue(), + }; + }), + ); + + return { + condition: await this.getConditionValue(), + conditions, + }; + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.html b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.html new file mode 100644 index 000000000..13b8d85e3 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.html @@ -0,0 +1,28 @@ + + + + +@if (elOutput) { +
+ {{ elOutput }} +
+} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.scss new file mode 100644 index 000000000..fb3da01db --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.scss @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.conditionGroup { + display: flex; + flex-direction: column; + gap: 8px; + + &__header { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + } + + &__conditions { + display: flex; + flex-direction: column; + gap: 8px; + } +} + +.condition { + display: flex; + align-items: flex-start; + gap: 8px; + + &__removeBtn { + align-self: center; + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.spec.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.spec.ts new file mode 100644 index 000000000..d0294317f --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.spec.ts @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { provideNativeDateAdapter } from '@angular/material/core'; + +import { GioElEditorHarness } from './gio-el-editor.harness'; +import { GioElEditorComponent } from './gio-el-editor.component'; + +describe('GioElEditorComponent', () => { + let component: GioElEditorComponent; + let fixture: ComponentFixture; + let editorComponentHarness: GioElEditorHarness; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, MatIconTestingModule, GioElEditorComponent], + providers: [provideNativeDateAdapter()], + }).compileComponents(); + + fixture = TestBed.createComponent(GioElEditorComponent); + component = fixture.componentInstance; + + component.conditionsModel = [ + { + field: 'application', + label: 'Application', + type: 'string', + values: ['a'], + }, + { + field: 'isAuthenticated', + label: 'Is Authenticated', + type: 'boolean', + }, + { + field: 'duration', + label: 'Duration', + type: 'number', + max: 10, + }, + { + field: 'timestamp', + label: 'Timestamp', + type: 'date', + }, + ]; + fixture.detectChanges(); + + editorComponentHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, GioElEditorHarness); + }); + + it('should create empty editor', async () => { + expect(component).toBeTruthy(); + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [], + }); + }); + + it('should add new string condition', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewConditionButton(); + + await mainConditionGroup.selectConditionField(0, 'Application'); + expect(await mainConditionGroup.getConditionAvailableOperators(0)).toEqual(['Equals', 'Not equals']); + + await mainConditionGroup.selectConditionOperator(0, 'Equals'); + await mainConditionGroup.setConditionValue(0, 'Yolo'); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [ + { + field: 'Application', + operator: 'Equals', + value: 'Yolo', + }, + ], + }); + }); + + it('should add new boolean condition', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewConditionButton(); + + await mainConditionGroup.selectConditionField(0, 'Is Authenticated'); + expect(await mainConditionGroup.getConditionAvailableOperators(0)).toEqual(['Equals', 'Not equals']); + + await mainConditionGroup.selectConditionOperator(0, 'Not equals'); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [ + { + field: 'Is Authenticated', + operator: 'Not equals', + value: true, + }, + ], + }); + }); + + it('should add new number condition', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewConditionButton(); + + await mainConditionGroup.selectConditionField(0, 'Duration'); + expect(await mainConditionGroup.getConditionAvailableOperators(0)).toEqual([ + 'Equals', + 'Not equals', + 'Less than', + 'Less than or equals', + 'Greater than', + 'Greater than or equals', + ]); + + await mainConditionGroup.selectConditionOperator(0, 'Less than'); + await mainConditionGroup.setConditionValue(0, 5); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [ + { + field: 'Duration', + operator: 'Less than', + value: 5, + }, + ], + }); + }); + + it('should add new date condition', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewConditionButton(); + + await mainConditionGroup.selectConditionField(0, 'Timestamp'); + expect(await mainConditionGroup.getConditionAvailableOperators(0)).toEqual([ + 'Equals', + 'Not equals', + 'Less than', + 'Less than or equals', + 'Greater than', + 'Greater than or equals', + ]); + + await mainConditionGroup.selectConditionOperator(0, 'Greater than'); + await mainConditionGroup.setConditionValue(0, new Date('2021-01-01')); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [ + { + field: 'Timestamp', + operator: 'Greater than', + value: '2021-01-01T00:00:00.000Z', + }, + ], + }); + }); + + it('should add new group', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewGroupButton(); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [ + { + condition: 'AND', + conditions: [], + }, + ], + }); + }); + + it('should add new group with OR condition', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewConditionButton(); + await mainConditionGroup.selectConditionField(0, 'Application'); + await mainConditionGroup.selectConditionOperator(0, 'Equals'); + await mainConditionGroup.setConditionValue(0, 'Yolo'); + + await mainConditionGroup.clickAddNewConditionButton(); + await mainConditionGroup.selectConditionField(1, 'Application'); + await mainConditionGroup.selectConditionOperator(1, 'Equals'); + await mainConditionGroup.setConditionValue(1, 'Toto'); + + await mainConditionGroup.selectConditionValue('OR'); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'OR', + conditions: [ + { + field: 'Application', + operator: 'Equals', + value: 'Yolo', + }, + { + field: 'Application', + operator: 'Equals', + value: 'Toto', + }, + ], + }); + }); + + it('should add new group with conditions', async () => { + const mainConditionGroup = await editorComponentHarness.getMainConditionGroup(); + await mainConditionGroup.clickAddNewConditionButton(); + await mainConditionGroup.selectConditionField(0, 'Application'); + await mainConditionGroup.selectConditionOperator(0, 'Equals'); + await mainConditionGroup.setConditionValue(0, 'Yolo'); + + await mainConditionGroup.clickAddNewGroupButton(); + const lvl2ConditionGroup = await mainConditionGroup.getConditionGroup(1); + await lvl2ConditionGroup.clickAddNewConditionButton(); + await lvl2ConditionGroup.selectConditionField(0, 'Duration'); + await lvl2ConditionGroup.selectConditionOperator(0, 'Equals'); + await lvl2ConditionGroup.setConditionValue(0, 42); + + await lvl2ConditionGroup.clickAddNewConditionButton(); + await lvl2ConditionGroup.selectConditionValue('OR'); + await lvl2ConditionGroup.selectConditionField(1, 'Duration'); + await lvl2ConditionGroup.selectConditionOperator(1, 'Equals'); + await lvl2ConditionGroup.setConditionValue(1, 43); + + const conditions = await mainConditionGroup.getConditions(); + expect(conditions).toEqual({ + condition: 'AND', + conditions: [ + { + field: 'Application', + operator: 'Equals', + value: 'Yolo', + }, + { + condition: 'OR', + conditions: [ + { + field: 'Duration', + operator: 'Equals', + value: 42, + }, + { + field: 'Duration', + operator: 'Equals', + value: 43, + }, + ], + }, + ], + }); + }); +}); diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.ts new file mode 100644 index 000000000..0594ce3fd --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.component.ts @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ChangeDetectionStrategy, Component, DestroyRef, inject, Input, OnInit } from '@angular/core'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { CommonModule } from '@angular/common'; +import { MatSelectModule } from '@angular/material/select'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { map } from 'rxjs/operators'; +import { has, isEmpty, isNil } from 'lodash'; +import { GioIconsModule } from '@gravitee/ui-particles-angular'; + +import { ConditionModel } from '../models/ConditionModel'; +import { ConditionsModel, isConditionModel } from '../models/ConditionsModel'; +import { ExpressionLanguageBuilder } from '../models/ExpressionLanguageBuilder'; +import { ConditionGroup } from '../models/ConditionGroup'; +import { Condition } from '../models/Condition'; +import { Operator } from '../models/Operator'; + +import { GioElEditorTypeStringComponent } from './gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component'; +import { GioElEditorTypeBooleanComponent } from './gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component'; +import { GioElEditorTypeNumberComponent } from './gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component'; +import { GioElEditorTypeDateComponent } from './gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component'; +import { GioElEditorConditionGroupComponent } from './gio-el-editor-condition-group/gio-el-editor-condition-group.component'; + +export type ConditionForm = { + field: FormControl; + operator: FormControl; + value: FormControl; +}; + +export type ConditionGroupForm = { + condition: FormControl<'AND' | 'OR'>; + conditions: FormArray | FormGroup>; +}; + +@Component({ + selector: 'gio-el-editor', + standalone: true, + imports: [ + CommonModule, + MatButtonToggleModule, + ReactiveFormsModule, + MatButtonModule, + GioIconsModule, + MatMenuModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + GioElEditorTypeStringComponent, + GioElEditorTypeBooleanComponent, + GioElEditorTypeNumberComponent, + GioElEditorTypeDateComponent, + GioElEditorConditionGroupComponent, + ], + templateUrl: './gio-el-editor.component.html', + styleUrl: './gio-el-editor.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GioElEditorComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + + @Input({ required: true }) + public conditionsModel: ConditionsModel = []; + + protected conditionsGroupFormArray = new FormArray>([]); + protected conditionGroupFormGroup = newConditionGroupFormGroup(); + + protected fields: ConditionModel[] = []; + + protected elOutput?: string; + + public ngOnInit() { + for (const conditionModel of this.conditionsModel) { + if (isConditionModel(conditionModel)) { + // TOTO: Impl Deep Tree for conditions Limit to fist Node for fist impl + this.fields.push(conditionModel); + } + } + + this.conditionGroupFormGroup.valueChanges + .pipe( + takeUntilDestroyed(this.destroyRef), + map(() => this.conditionGroupFormGroup.getRawValue()), + map(value => { + type ConditionGroupValue = typeof value; + type ConditionValue = Exclude[number]; + + const toCondition = ( + conditionValue: ConditionValue, + ): Condition<'string' | 'number' | 'date' | 'boolean'> | ConditionGroup | null => { + if (isConditionGroupValue(conditionValue)) { + return toConditionGroup(conditionValue as ConditionGroupValue); + } + if ( + !isConditionValue(conditionValue) || + isNil(conditionValue.field) || + isNil(conditionValue.operator) || + isNil(conditionValue.value) + ) { + return null; + } + return new Condition(conditionValue.field.field, conditionValue.field.type, conditionValue.operator, conditionValue.value); + }; + + const toConditionGroup = (conditionGroupValue: ConditionGroupValue): ConditionGroup | null => { + const conditions: ConditionGroup['conditions'] = []; + if (!conditionGroupValue.condition || !conditionGroupValue.conditions || isEmpty(conditionGroupValue.conditions)) { + return null; + } + for (const conditionValue of conditionGroupValue.conditions) { + const condition = toCondition(conditionValue); + if (condition) { + conditions.push(condition); + } + } + if (isEmpty(conditions)) { + return null; + } + + return new ConditionGroup(conditionGroupValue.condition, conditions); + }; + + const conditionGroupValue = toConditionGroup(value); + if (conditionGroupValue) { + this.elOutput = new ExpressionLanguageBuilder(conditionGroupValue).build(); + } + }), + ) + .subscribe(); + } +} + +const newConditionGroupFormGroup = (): FormGroup => { + return new FormGroup({ + condition: new FormControl({ value: 'AND', disabled: true }, { nonNullable: true, validators: Validators.required }), + conditions: new FormArray | FormGroup>([]), + }); +}; + +const isConditionGroupValue = ( + value: unknown, +): value is { + condition: 'AND' | 'OR'; + conditions: unknown[]; +} => { + return !!value && has(value, 'condition') && has(value, 'conditions'); +}; + +const isConditionValue = ( + value: unknown, +): value is { + field: ConditionModel; + operator: Operator; + value: string | boolean | Date | number; +} => { + return !!value && has(value, 'field') && has(value, 'operator') && has(value, 'value'); +}; diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.harness.ts new file mode 100644 index 000000000..a80de8d11 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.harness.ts @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentHarness } from '@angular/cdk/testing'; + +import { GioElEditorConditionGroupHarness } from './gio-el-editor-condition-group/gio-el-editor-condition-group.harness'; + +export class GioElEditorHarness extends ComponentHarness { + public static hostSelector = 'gio-el-editor'; + + public getMainConditionGroup = this.locatorFor(GioElEditorConditionGroupHarness); +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.stories.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.stories.ts new file mode 100644 index 000000000..eae1be158 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-editor.stories.ts @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { applicationConfig, Meta, StoryObj } from '@storybook/angular'; +import { provideNativeDateAdapter } from '@angular/material/core'; + +import { GioElEditorComponent } from './gio-el-editor.component'; + +export default { + title: 'Components / EL / Editor', + component: GioElEditorComponent, + decorators: [ + applicationConfig({ + providers: [provideNativeDateAdapter()], + }), + ], + render: ({ conditionsModel }) => ({ + template: ``, + props: { conditionsModel }, + }), +} as Meta; + +export const Default: StoryObj = {}; +Default.args = { + conditionsModel: [ + { + field: 'application', + label: 'Application', + type: 'string', + values: ['a', 'b', 'c'], + }, + { + field: 'isAuthenticated', + label: 'Is Authenticated', + type: 'boolean', + }, + { + field: 'duration', + label: 'Duration', + type: 'number', + max: 10, + }, + { + field: 'timestamp', + label: 'Timestamp', + type: 'date', + }, + ], +}; diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.html b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.html new file mode 100644 index 000000000..0c1264c90 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.html @@ -0,0 +1,34 @@ + +@if (conditionFormGroup) { +
+ + Operator + + + @for (operator of operators; track operator.value) { + {{ operator.label }} + } + + + + + {{ valueToggle.checked ? 'True' : 'False' }} + +
+} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.scss new file mode 100644 index 000000000..22d9e1eb5 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.scss @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '../gio-el-editor-type.component' as *; + +@include gio-el-editor-type-common; + +.toggle { + min-width: 80px; + align-self: center; + margin-left: 8px; +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.ts new file mode 100644 index 000000000..144c1a6a3 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.component.ts @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { isEmpty } from 'lodash'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { CommonModule } from '@angular/common'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; + +import { Operator } from '../../../models/Operator'; +import { ConditionForm } from '../../gio-el-editor.component'; + +@Component({ + selector: 'gio-el-editor-type-boolean', + standalone: true, + imports: [CommonModule, MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatSlideToggle], + templateUrl: './gio-el-editor-type-boolean.component.html', + styleUrl: './gio-el-editor-type-boolean.component.scss', +}) +export class GioElEditorTypeBooleanComponent implements OnChanges { + @Input({ + required: true, + }) + public conditionFormGroup!: FormGroup; + + protected operators: { label: string; value: Operator }[] = [ + { label: 'Equals', value: 'EQUALS' }, + { label: 'Not equals', value: 'NOT_EQUALS' }, + ]; + + public ngOnChanges(changes: SimpleChanges) { + if (changes.conditionFormGroup) { + const field = this.conditionFormGroup.controls.field.value; + if (isEmpty(field) || field?.type !== 'boolean') { + throw new Error('Boolean field is required!'); + } + + this.conditionFormGroup.addControl('operator', new FormControl(null)); + this.conditionFormGroup.addControl('value', new FormControl(null)); + this.conditionFormGroup.get('value')?.setValue(true); + } + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.harness.ts new file mode 100644 index 000000000..adf7cb15a --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-boolean/gio-el-editor-type-boolean.harness.ts @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; + +import { GioElEditorTypeComponentHarness } from '../gio-el-editor-type.harness'; + +export class GioElEditorTypeBooleanHarness extends GioElEditorTypeComponentHarness { + public static hostSelector = 'gio-el-editor-type-boolean'; + + public async getValue(): Promise { + const slideToggleHarness = await this.locatorFor(MatSlideToggleHarness)(); + return await slideToggleHarness.isChecked(); + } + + public async setValue(value: boolean): Promise { + const slideToggleHarness = await this.locatorFor(MatSlideToggleHarness)(); + if (value !== (await slideToggleHarness.isChecked())) { + await slideToggleHarness.toggle(); + } + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.html b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.html new file mode 100644 index 000000000..f3809ed05 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.html @@ -0,0 +1,38 @@ + +@if (conditionFormGroup) { +
+ + Operator + + + @for (operator of operators; track operator.value) { + {{ operator.label }} + } + + + + + Choose a date + + MM/DD/YYYY + + + +
+} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.scss new file mode 100644 index 000000000..aee3843a0 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.scss @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '../gio-el-editor-type.component' as *; + +@include gio-el-editor-type-common; + +.toggle { + min-width: 80px; + margin-left: 8px; +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.ts new file mode 100644 index 000000000..306a7c37e --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.component.ts @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, DestroyRef, inject, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { isDate, isEmpty } from 'lodash'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { CommonModule } from '@angular/common'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { Operator } from '../../../models/Operator'; +import { ConditionForm } from '../../gio-el-editor.component'; + +@Component({ + selector: 'gio-el-editor-type-date', + standalone: true, + imports: [CommonModule, MatFormFieldModule, MatSelectModule, MatInputModule, ReactiveFormsModule, MatDatepickerModule], + templateUrl: './gio-el-editor-type-date.component.html', + styleUrl: './gio-el-editor-type-date.component.scss', +}) +export class GioElEditorTypeDateComponent implements OnChanges { + private readonly destroyRef = inject(DestroyRef); + @Input({ + required: true, + }) + public conditionFormGroup!: FormGroup; + + protected operators: { label: string; value: Operator }[] = [ + { label: 'Equals', value: 'EQUALS' }, + { label: 'Not equals', value: 'NOT_EQUALS' }, + { label: 'Less than', value: 'LESS_THAN' }, + { label: 'Less than or equals', value: 'LESS_THAN_OR_EQUALS' }, + { label: 'Greater than', value: 'GREATER_THAN' }, + { label: 'Greater than or equals', value: 'GREATER_THAN_OR_EQUALS' }, + ]; + + protected datepickerControl = new FormControl(); + + public ngOnChanges(changes: SimpleChanges) { + if (changes.conditionFormGroup) { + const field = this.conditionFormGroup.controls.field.value; + if (isEmpty(field) || field?.type !== 'date') { + throw new Error('Date field is required!'); + } + this.conditionFormGroup.addControl('operator', new FormControl(null)); + this.conditionFormGroup.addControl('value', new FormControl(true)); + + this.datepickerControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => { + if (!isDate(value)) { + return; + } + // Convert date to UTC ignoring timezone + const dateUtc = Date.UTC( + value.getFullYear(), + value.getMonth(), + value.getDate(), + value.getHours(), + value.getMinutes(), + value.getSeconds(), + ); + this.conditionFormGroup.controls.value.setValue(new Date(dateUtc)); + }); + } + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.harness.ts new file mode 100644 index 000000000..970f7d59f --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-date/gio-el-editor-type-date.harness.ts @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MatDatepickerInputHarness } from '@angular/material/datepicker/testing'; + +import { GioElEditorTypeComponentHarness } from '../gio-el-editor-type.harness'; + +export class GioElEditorTypeDateHarness extends GioElEditorTypeComponentHarness { + public static hostSelector = 'gio-el-editor-type-date'; + + public async getValue(): Promise { + const datepickerInputHarness = await this.locatorFor(MatDatepickerInputHarness)(); + return await datepickerInputHarness.getValue(); + } + + public async setValue(value: Date): Promise { + const datepickerInputHarness = await this.locatorFor(MatDatepickerInputHarness)(); + await datepickerInputHarness.setValue(value.toISOString()); + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.html b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.html new file mode 100644 index 000000000..3d5c37d04 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.html @@ -0,0 +1,37 @@ + +@if (conditionFormGroup) { +
+ + Operator + + + @for (operator of operators; track operator.value) { + {{ operator.label }} + } + + + + + Value + + Value must be at least {{ min }} + Value must be at most {{ max }} + +
+} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.scss new file mode 100644 index 000000000..aee3843a0 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.scss @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '../gio-el-editor-type.component' as *; + +@include gio-el-editor-type-common; + +.toggle { + min-width: 80px; + margin-left: 8px; +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.ts new file mode 100644 index 000000000..b76f8fba6 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.component.ts @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { isEmpty } from 'lodash'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { CommonModule } from '@angular/common'; + +import { ConditionForm } from '../../gio-el-editor.component'; +import { Operator } from '../../../models/Operator'; + +@Component({ + selector: 'gio-el-editor-type-number', + standalone: true, + imports: [CommonModule, MatFormFieldModule, MatSelectModule, MatInputModule, ReactiveFormsModule], + templateUrl: './gio-el-editor-type-number.component.html', + styleUrl: './gio-el-editor-type-number.component.scss', +}) +export class GioElEditorTypeNumberComponent implements OnChanges { + @Input({ + required: true, + }) + public conditionFormGroup!: FormGroup; + + protected operators: { label: string; value: Operator }[] = [ + { label: 'Equals', value: 'EQUALS' }, + { label: 'Not equals', value: 'NOT_EQUALS' }, + { label: 'Less than', value: 'LESS_THAN' }, + { label: 'Less than or equals', value: 'LESS_THAN_OR_EQUALS' }, + { label: 'Greater than', value: 'GREATER_THAN' }, + { label: 'Greater than or equals', value: 'GREATER_THAN_OR_EQUALS' }, + ]; + + protected min: number | null = null; + protected max: number | null = null; + + public ngOnChanges(changes: SimpleChanges) { + if (changes.conditionFormGroup) { + const field = this.conditionFormGroup.controls.field.value; + if (isEmpty(field) || field?.type !== 'number') { + throw new Error('Number field is required!'); + } + if (field.min) { + this.min = field.min; + } + if (field.max) { + this.max = field.max; + } + + this.conditionFormGroup.addControl('operator', new FormControl(null)); + this.conditionFormGroup.addControl('value', new FormControl(true)); + } + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.harness.ts new file mode 100644 index 000000000..69e79864b --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-number/gio-el-editor-type-number.harness.ts @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MatInputHarness } from '@angular/material/input/testing'; + +import { GioElEditorTypeComponentHarness } from '../gio-el-editor-type.harness'; + +export class GioElEditorTypeNumberHarness extends GioElEditorTypeComponentHarness { + public static hostSelector = 'gio-el-editor-type-number'; + + public async getValue(): Promise { + const input = await this.locatorFor(MatInputHarness)(); + return Number(await input.getValue()); + } + + public async setValue(value: number): Promise { + const input = await this.locatorFor(MatInputHarness)(); + await input.setValue(value.toString()); + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.html b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.html new file mode 100644 index 000000000..cb1578752 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.html @@ -0,0 +1,41 @@ + +@if (conditionFormGroup) { +
+ + Operator + + + @for (operator of operators; track operator.value) { + {{ operator.label }} + } + + + + + Value + + + + @for (option of filteredOptions$ | async; track option) { + {{ option }} + } + + +
+} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.scss new file mode 100644 index 000000000..ce702c2cc --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.scss @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '../gio-el-editor-type.component' as *; + +@include gio-el-editor-type-common; diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.ts new file mode 100644 index 000000000..7a1a9f844 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.component.ts @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, DestroyRef, inject, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { isEmpty, toString } from 'lodash'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { CommonModule } from '@angular/common'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { ConditionForm } from '../../gio-el-editor.component'; +import { Operator } from '../../../models/Operator'; + +@Component({ + selector: 'gio-el-editor-type-string', + standalone: true, + imports: [CommonModule, MatFormFieldModule, MatSelectModule, MatInputModule, ReactiveFormsModule, MatAutocompleteModule], + templateUrl: './gio-el-editor-type-string.component.html', + styleUrl: './gio-el-editor-type-string.component.scss', +}) +export class GioElEditorTypeStringComponent implements OnChanges { + private readonly destroyRef = inject(DestroyRef); + + @Input({ + required: true, + }) + public conditionFormGroup!: FormGroup; + + protected operators: { label: string; value: Operator }[] = [ + { label: 'Equals', value: 'EQUALS' }, + { label: 'Not equals', value: 'NOT_EQUALS' }, + ]; + + protected filteredOptions$: Observable = new Observable(); + + public ngOnChanges(changes: SimpleChanges) { + if (changes.conditionFormGroup) { + const field = this.conditionFormGroup.controls.field.value; + if (isEmpty(field) || field?.type !== 'string') { + throw new Error('String field is required!'); + } + + this.conditionFormGroup.addControl('operator', new FormControl(null)); + this.conditionFormGroup.addControl('value', new FormControl(null)); + + const values = field.values; + if (values && !isEmpty(values)) { + this.filteredOptions$ = this.conditionFormGroup.get('value')!.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef), + startWith(''), + map(value => filterValues(values, toString(value) ?? '')), + ); + } + } + } +} + +const filterValues = (values: string[], value: string) => { + const filterValue = value.toLowerCase(); + return values.filter(option => option.toLowerCase().includes(filterValue)); +}; diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.harness.ts new file mode 100644 index 000000000..43a8d188c --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type-string/gio-el-editor-type-string.harness.ts @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MatInputHarness } from '@angular/material/input/testing'; + +import { GioElEditorTypeComponentHarness } from '../gio-el-editor-type.harness'; + +export class GioElEditorTypeStringHarness extends GioElEditorTypeComponentHarness { + public static hostSelector = 'gio-el-editor-type-string'; + + public async getValue(): Promise { + const input = await this.locatorFor(MatInputHarness)(); + return await input.getValue(); + } + + public async setValue(value: string): Promise { + const input = await this.locatorFor(MatInputHarness)(); + await input.setValue(value); + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.component.scss b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.component.scss new file mode 100644 index 000000000..036924907 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.component.scss @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@mixin gio-el-editor-type-common { + .condition { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 8px; + } +} diff --git a/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.harness.ts b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.harness.ts new file mode 100644 index 000000000..6cd6265d2 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/gio-el-editor/gio-el-type/gio-el-editor-type.harness.ts @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentHarness, parallel } from '@angular/cdk/testing'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +export abstract class GioElEditorTypeComponentHarness extends ComponentHarness { + public getOperatorSelector = this.locatorFor(MatSelectHarness.with({ selector: '[formControlName="operator"]' })); + + public async getAvailableOperators(): Promise { + const operatorSelector = await this.getOperatorSelector(); + await operatorSelector.open(); + const options = await operatorSelector.getOptions(); + return parallel(() => options.map(async option => await option.getText())); + } + + public async selectOperator(operator: string): Promise { + const operatorSelector = await this.getOperatorSelector(); + await operatorSelector.clickOptions({ text: operator }); + } + + public async getOperatorValue(): Promise { + const operatorSelector = await this.getOperatorSelector(); + return operatorSelector.getValueText(); + } + + public abstract getValue(): Promise; + + public abstract setValue(value: string | boolean | number | Date): Promise; +} diff --git a/projects/ui-particles-angular/gio-el/models/Condition.ts b/projects/ui-particles-angular/gio-el/models/Condition.ts new file mode 100644 index 000000000..60d37cbeb --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/Condition.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Operator } from './Operator'; + +type ConditionValue = T extends 'string' + ? string + : T extends 'number' + ? number + : T extends 'date' + ? Date + : T extends 'boolean' + ? boolean + : never; + +export class Condition { + constructor( + public field: string, + public type: T, + public operator: Operator, + public value?: ConditionValue, + ) {} +} diff --git a/projects/ui-particles-angular/gio-el/models/ConditionGroup.ts b/projects/ui-particles-angular/gio-el/models/ConditionGroup.ts new file mode 100644 index 000000000..21187c2c4 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/ConditionGroup.ts @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Condition } from './Condition'; + +export class ConditionGroup { + constructor( + public condition: 'AND' | 'OR', + public conditions: (Condition<'string' | 'number' | 'date' | 'boolean'> | ConditionGroup)[], + ) {} +} diff --git a/projects/ui-particles-angular/gio-el/models/ConditionModel.ts b/projects/ui-particles-angular/gio-el/models/ConditionModel.ts new file mode 100644 index 000000000..19621d78b --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/ConditionModel.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type ConditionModelBase = { + field: string; + label: string; + type: T; +}; + +export type StringConditionModel = ConditionModelBase<'string'> & { + values?: string[]; +}; + +export type NumberConditionModel = ConditionModelBase<'number'> & { + min?: number; + max?: number; +}; + +export type DateConditionModel = ConditionModelBase<'date'>; + +export type BooleanConditionModel = ConditionModelBase<'boolean'>; + +export type ConditionModel = StringConditionModel | NumberConditionModel | DateConditionModel | BooleanConditionModel; diff --git a/projects/ui-particles-angular/gio-el/models/ConditionsModel.ts b/projects/ui-particles-angular/gio-el/models/ConditionsModel.ts new file mode 100644 index 000000000..0bb7e1e74 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/ConditionsModel.ts @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConditionModel } from './ConditionModel'; + +export type ParentConditionModel = { field: string; label: string; conditions: (ParentConditionModel | ConditionModel)[] }; + +export type ConditionsModel = (ParentConditionModel | ConditionModel)[]; + +export const isConditionModel = (conditions: ParentConditionModel | ConditionModel): conditions is ConditionModel => { + return 'field' in conditions && 'label' in conditions && 'type' in conditions; +}; diff --git a/projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.spec.ts b/projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.spec.ts new file mode 100644 index 000000000..712165909 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.spec.ts @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ExpressionLanguageBuilder } from './ExpressionLanguageBuilder'; +import { ConditionGroup } from './ConditionGroup'; +import { Condition } from './Condition'; + +describe('ExpressionLanguageBuilder', () => { + it('should build a condition group', () => { + // Arrange + const conditionGroup: ConditionGroup = new ConditionGroup('AND', [ + new Condition('field1', 'string', 'EQUALS', 'value1'), + new Condition('field2', 'string', 'NOT_EQUALS', 'value2'), + ]); + const expressionLanguageBuilder = new ExpressionLanguageBuilder(conditionGroup); + + // Act + const expression = expressionLanguageBuilder.build(); + + // Assert + expect(expression).toEqual('{ #field1 == "value1" && #field2 != "value2" }'); + }); + + it('should build a condition group with multiple conditions', () => { + // Arrange + const conditionGroup: ConditionGroup = new ConditionGroup('OR', [ + new Condition('field1', 'string', 'EQUALS', 'value1'), + new Condition('field2', 'string', 'NOT_EQUALS', 'value2'), + new Condition('field3', 'string', 'LESS_THAN', 'value3'), + ]); + const expressionLanguageBuilder = new ExpressionLanguageBuilder(conditionGroup); + + // Act + const expression = expressionLanguageBuilder.build(); + + // Assert + expect(expression).toEqual('{ #field1 == "value1" || #field2 != "value2" || #field3 < "value3" }'); + }); + + it('should build a condition group with nested condition groups', () => { + // Arrange + const conditionGroup = new ConditionGroup('OR', [ + new Condition('field1', 'string', 'EQUALS', 'value1'), + new Condition('field2', 'string', 'NOT_EQUALS', 'value2'), + new ConditionGroup('AND', [ + new Condition('field3', 'string', 'LESS_THAN', 'value3'), + new Condition('field4', 'string', 'GREATER_THAN', 'value4'), + ]), + ]); + const expressionLanguageBuilder = new ExpressionLanguageBuilder(conditionGroup); + + // Act + const expression = expressionLanguageBuilder.build(); + + // Assert + expect(expression).toEqual('{ #field1 == "value1" || #field2 != "value2" || ( #field3 < "value3" && #field4 > "value4" ) }'); + }); + + it('should build a condition group all operators', () => { + // Arrange + const conditionGroup = new ConditionGroup('AND', [ + new Condition('field1', 'string', 'EQUALS', 'value1'), + new Condition('field2', 'string', 'NOT_EQUALS', 'value2'), + new Condition('field3', 'string', 'LESS_THAN', 'value3'), + new Condition('field4', 'string', 'LESS_THAN_OR_EQUALS', 'value4'), + new Condition('field5', 'string', 'GREATER_THAN', 'value5'), + new Condition('field6', 'string', 'GREATER_THAN_OR_EQUALS', 'value6'), + new Condition('field7', 'string', 'CONTAINS', 'value7'), + new Condition('field8', 'string', 'NOT_CONTAINS', 'value8'), + new Condition('field9', 'string', 'STARTS_WITH', 'value9'), + new Condition('field10', 'string', 'ENDS_WITH', 'value10'), + ]); + const expressionLanguageBuilder = new ExpressionLanguageBuilder(conditionGroup); + + // Act + const expression = expressionLanguageBuilder.build(); + + // Assert + expect(expression).toEqual( + '{ #field1 == "value1" && #field2 != "value2" && #field3 < "value3" && #field4 <= "value4" && #field5 > "value5" && #field6 >= "value6" && #field7 matches ""value7"" && !#field8 matches ""value8"" && #field9 matches "^"value9"" && #field10 matches ""value10"$" }', + ); + }); + + it('should build a condition group with date', () => { + // Arrange + const conditionGroup = new ConditionGroup('AND', [new Condition('field1', 'date', 'EQUALS', new Date('2021-01-01'))]); + const expressionLanguageBuilder = new ExpressionLanguageBuilder(conditionGroup); + + // Act + const expression = expressionLanguageBuilder.build(); + + // Assert + expect(expression).toEqual('{ #field1 == 1609459200000l }'); + }); +}); diff --git a/projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.ts b/projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.ts new file mode 100644 index 000000000..6e0b8deaf --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/ExpressionLanguageBuilder.ts @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConditionGroup } from './ConditionGroup'; +import { Operator } from './Operator'; +import { Condition } from './Condition'; + +export class ExpressionLanguageBuilder { + private static CONDITION_MAP = { + OR: '||', + AND: '&&', + }; + private static OPERATOR_MAP: Record string> = { + EQUALS: (field, value) => `#${field} == ${value}`, + NOT_EQUALS: (field, value) => `#${field} != ${value}`, + LESS_THAN: (field, value) => `#${field} < ${value}`, + LESS_THAN_OR_EQUALS: (field, value) => `#${field} <= ${value}`, + GREATER_THAN: (field, value) => `#${field} > ${value}`, + GREATER_THAN_OR_EQUALS: (field, value) => `#${field} >= ${value}`, + CONTAINS: (field, value) => `#${field} matches "${value}"`, + NOT_CONTAINS: (field, value) => `!#${field} matches "${value}"`, + STARTS_WITH: (field, value) => `#${field} matches "^${value}"`, + ENDS_WITH: (field, value) => `#${field} matches "${value}$"`, + }; + + private static buildConditionGroup(conditionGroup: ConditionGroup): string { + let el = ''; + for (const condition of conditionGroup.conditions) { + if (condition instanceof ConditionGroup) { + el += '( '; + el += ExpressionLanguageBuilder.buildConditionGroup(condition); + el += ' )'; + } else { + el += ExpressionLanguageBuilder.buildCondition(condition); + } + + if (condition !== conditionGroup.conditions[conditionGroup.conditions.length - 1]) { + el += ` ${ExpressionLanguageBuilder.CONDITION_MAP[conditionGroup.condition]} `; + } + } + if (el === '') { + return ''; + } + return el; + } + + private static buildCondition(condition: Condition<'string' | 'number' | 'date' | 'boolean'>): string { + const operator = ExpressionLanguageBuilder.OPERATOR_MAP[condition.operator]; + return operator(condition.field, ExpressionLanguageBuilder.valueToString(condition.type, condition.value)); + } + + private static valueToString(type: T, value: unknown): string { + if (!type) { + return ''; + } + + switch (type) { + case 'string': + return `"${value}"`; + case 'date': + return `${(value as Date).getTime()}l`; + default: + return `${value}`; + } + } + + constructor(private conditionGroup: ConditionGroup) {} + + public build(): string { + const el = ExpressionLanguageBuilder.buildConditionGroup(this.conditionGroup); + + return `{ ${el} }`; + } +} diff --git a/projects/ui-particles-angular/gio-el/models/Operator.ts b/projects/ui-particles-angular/gio-el/models/Operator.ts new file mode 100644 index 000000000..d46daa6d0 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/Operator.ts @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type Operator = + | 'EQUALS' + | 'NOT_EQUALS' + | 'LESS_THAN' + | 'LESS_THAN_OR_EQUALS' + | 'GREATER_THAN' + | 'GREATER_THAN_OR_EQUALS' + | 'CONTAINS' + | 'NOT_CONTAINS' + | 'STARTS_WITH' + | 'ENDS_WITH'; diff --git a/projects/ui-particles-angular/gio-el/models/public-api.ts b/projects/ui-particles-angular/gio-el/models/public-api.ts new file mode 100644 index 000000000..be7542a50 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/models/public-api.ts @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Condition'; +export * from './ConditionModel'; +export * from './ConditionsModel'; +export * from './ExpressionLanguageBuilder'; +export * from './Operator'; diff --git a/projects/ui-particles-angular/gio-el/ng-package.json b/projects/ui-particles-angular/gio-el/ng-package.json new file mode 100644 index 000000000..e122d73b6 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "./public-api.ts" + } +} diff --git a/projects/ui-particles-angular/gio-el/public-api.ts b/projects/ui-particles-angular/gio-el/public-api.ts new file mode 100644 index 000000000..0379a06d2 --- /dev/null +++ b/projects/ui-particles-angular/gio-el/public-api.ts @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { GioElEditorComponent } from './gio-el-editor/gio-el-editor.component'; +export { GioElEditorHarness } from './gio-el-editor/gio-el-editor.harness'; + +export * from './models/public-api'; diff --git a/projects/ui-particles-angular/jest.config.js b/projects/ui-particles-angular/jest.config.js index 2ff73217c..8adc8c776 100644 --- a/projects/ui-particles-angular/jest.config.js +++ b/projects/ui-particles-angular/jest.config.js @@ -15,6 +15,12 @@ */ module.exports = { preset: 'jest-preset-angular', - roots: [__dirname + '/src'], + roots: [__dirname], setupFilesAfterEnv: [__dirname + '/src/setup-jest.ts'], + moduleNameMapper: { + // 📝 Order is important + '@gravitee/ui-particles-angular/testing': __dirname + '/../ui-particles-angular/testing/public-api.ts', + '@gravitee/ui-particles-angular/(.*)': __dirname + '/../ui-particles-angular/$1/public-api.ts', + '@gravitee/ui-particles-angular': __dirname + '/../ui-particles-angular/src/public-api.ts', + }, };