From 4aca40a9d500dc543ec692b2e177cc3eab0b97d7 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 10:46:26 +0200 Subject: [PATCH 01/10] add basics for custom row actions --- .../edit-action-buttons-modal.component.ts | 26 +-- .../tab-actions/tab-actions.component.html | 3 + .../custom-row-actions.component.html | 133 +++++++++++++ .../custom-row-actions.component.scss | 0 .../custom-row-actions.component.spec.ts | 22 ++ .../custom-row-actions.component.ts | 188 ++++++++++++++++++ ...dit-custom-row-action-modal.component.html | 1 + ...dit-custom-row-action-modal.component.scss | 0 ...-custom-row-action-modal.component.spec.ts | 22 ++ .../edit-custom-row-action-modal.component.ts | 11 + .../grid-settings.component.html | 8 +- .../grid-settings/grid-settings.component.ts | 2 + .../widgets/grid/action-button.type.ts | 39 ++++ 13 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.html create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.spec.ts create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.scss create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.spec.ts create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts create mode 100644 libs/shared/src/lib/components/widgets/grid/action-button.type.ts diff --git a/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.ts b/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.ts index 4a3f7ba775..5929daa0c9 100644 --- a/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.ts +++ b/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.ts @@ -70,7 +70,7 @@ export class EditActionButtonsModalComponent * @param data.dashboard Current dashboard * @param data.form Current form * @param dialog dialog module for button edition / creation / deletion - * @param translateService used to translate modal text + * @param translate used to translate modal text * @param applicationService shared application service */ constructor( @@ -78,7 +78,7 @@ export class EditActionButtonsModalComponent @Inject(DIALOG_DATA) private data: { dashboard?: Dashboard; form?: Page | Step }, public dialog: Dialog, - public translateService: TranslateService, + public translate: TranslateService, public applicationService: ApplicationService ) { super(); @@ -173,20 +173,14 @@ export class EditActionButtonsModalComponent const { ConfirmModalComponent } = await import('@oort-front/shared'); const dialogRef = this.dialog.open(ConfirmModalComponent, { data: { - title: this.translateService.instant('common.deleteObject', { - name: this.translateService.instant( - 'models.dashboard.actionButtons.one' - ), + title: this.translate.instant('common.deleteObject', { + name: this.translate.instant('models.dashboard.actionButtons.one'), }), - content: this.translateService.instant( + content: this.translate.instant( 'models.dashboard.actionButtons.confirmDelete' ), - confirmText: this.translateService.instant( - 'components.confirmModal.delete' - ), - cancelText: this.translateService.instant( - 'components.confirmModal.cancel' - ), + confirmText: this.translate.instant('components.confirmModal.delete'), + cancelText: this.translate.instant('components.confirmModal.cancel'), confirmVariant: 'danger', }, }); @@ -210,9 +204,9 @@ export class EditActionButtonsModalComponent */ public async onDuplicateActionButton(actionButton: ActionButton) { const newActionButton = structuredClone(actionButton); - newActionButton.text = `${ - newActionButton.text - } (${this.translateService.instant('common.copy')})`; + newActionButton.text = `${newActionButton.text} (${this.translate.instant( + 'common.copy' + )})`; this.actionButtons.push(newActionButton); this.searchTerm = ''; this.updateTable(); diff --git a/libs/shared/src/lib/components/widgets/common/tab-actions/tab-actions.component.html b/libs/shared/src/lib/components/widgets/common/tab-actions/tab-actions.component.html index 10b69af21c..70d38880e4 100644 --- a/libs/shared/src/lib/components/widgets/common/tab-actions/tab-actions.component.html +++ b/libs/shared/src/lib/components/widgets/common/tab-actions/tab-actions.component.html @@ -1,8 +1,10 @@
+

{{ 'components.widget.settings.grid.actions.title' | translate }}

+
+
diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.html b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.html new file mode 100644 index 0000000000..de3027a59e --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.html @@ -0,0 +1,133 @@ +

Custom Row Actions

+ +
+
+ + +
+ + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + {{ 'common.title' | translate }} + + {{ element.text }} + + {{ 'models.dashboard.actionButtons.visibleTo' | translate }} + + {{ + element.hasRoleRestriction + ? getCorrespondingRoles(element.roles) + : '' + }} + + + + + + + + + +
+
+
+
+
+ + + {{ 'models.dashboard.actionButtons.create' | translate }} + + + + + + diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.spec.ts b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.spec.ts new file mode 100644 index 0000000000..20f698d458 --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomRowActionsComponent } from './custom-row-actions.component'; + +describe('CustomRowActionsComponent', () => { + let component: CustomRowActionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CustomRowActionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CustomRowActionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts new file mode 100644 index 0000000000..341c22a1a0 --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts @@ -0,0 +1,188 @@ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { + ButtonModule, + DividerModule, + FormWrapperModule, + IconModule, + MenuModule, + TableModule, + TooltipModule, +} from '@oort-front/ui'; +import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; +import { BehaviorSubject, takeUntil } from 'rxjs'; +import { EmptyModule } from '../../../ui/empty/empty.module'; +import { UnsubscribeComponent } from '../../../utils/unsubscribe/unsubscribe.component'; +import { ApplicationService } from '../../../../services/application/application.service'; +import { Role } from '../../../../models/user.model'; +import { ActionButton } from '../../grid/action-button.type'; +import { Dialog } from '@angular/cdk/dialog'; + +@Component({ + selector: 'shared-custom-row-actions', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TranslateModule, + FormWrapperModule, + ButtonModule, + DividerModule, + TableModule, + MenuModule, + TooltipModule, + IconModule, + DragDropModule, + EmptyModule, + ], + templateUrl: './custom-row-actions.component.html', + styleUrls: ['./custom-row-actions.component.scss'], +}) +export class CustomRowActionsComponent extends UnsubscribeComponent { + /** List of action buttons from dashboard */ + public actionButtons: ActionButton[] = []; + /** Behavior subject to track change in action buttons */ + public datasource = new BehaviorSubject(this.actionButtons); + /** Current search string */ + public searchTerm = ''; + /** Columns to display */ + public displayedColumns = ['dragDrop', 'name', 'roles', 'actions']; + private applicationService = inject(ApplicationService); + private translate = inject(TranslateService); + private dialog = inject(Dialog); + + public async onAddActionButton() { + const { EditCustomRowActionModalComponent } = await import( + '../edit-custom-row-action-modal/edit-custom-row-action-modal.component' + ); + const dialogRef = this.dialog.open( + EditCustomRowActionModalComponent, + { + data: { + disableClose: true, + }, + } + ); + + dialogRef.closed + .pipe(takeUntil(this.destroy$)) + .subscribe(async (button) => { + if (!button) return; + this.actionButtons.push(button); + this.searchTerm = ''; + this.updateTable(); + }); + } + + public async onEditActionButton(actionButton: ActionButton) { + const { EditCustomRowActionModalComponent } = await import( + '../edit-custom-row-action-modal/edit-custom-row-action-modal.component' + ); + const dialogRef = this.dialog.open( + EditCustomRowActionModalComponent, + { + data: { + button: actionButton, + disableClose: true, + }, + } + ); + + dialogRef.closed + .pipe(takeUntil(this.destroy$)) + .subscribe(async (button) => { + if (!button) return; + const index = this.actionButtons.indexOf(actionButton); + if (index > -1) { + this.actionButtons[index] = button; + this.updateTable(); + } + }); + } + + public async onDeleteActionButton(actionButton: ActionButton) { + const { ConfirmModalComponent } = await import( + '../../../confirm-modal/confirm-modal.component' + ); + const dialogRef = this.dialog.open(ConfirmModalComponent, { + data: { + title: this.translate.instant('common.deleteObject', { + name: this.translate.instant('models.dashboard.actionButtons.one'), + }), + content: this.translate.instant( + 'models.dashboard.actionButtons.confirmDelete' + ), + confirmText: this.translate.instant('components.confirmModal.delete'), + cancelText: this.translate.instant('components.confirmModal.cancel'), + confirmVariant: 'danger', + }, + }); + + dialogRef.closed.subscribe((value: any) => { + if (value) { + const index = this.actionButtons.indexOf(actionButton); + if (index > -1) { + this.actionButtons.splice(index, 1); + this.searchTerm = ''; + this.updateTable(); + } + } + }); + } + + public async onDuplicateActionButton(actionButton: ActionButton) { + const newActionButton = structuredClone(actionButton); + newActionButton.text = `${newActionButton.text} (${this.translate.instant( + 'common.copy' + )})`; + this.actionButtons.push(newActionButton); + this.searchTerm = ''; + this.updateTable(); + } + + /** + * Get the roles names from the roles ids + * + * @param roles ids of roles + * @returns names of roles + */ + getCorrespondingRoles(roles: string[]) { + return this.applicationService.application.value?.roles + ?.filter((role: Role) => roles.includes(role.id ?? '')) + .map((role: Role) => role.title) + .join(', '); + } + + /** + * Moves item in array + * + * @param event Drag and drop event + */ + drop(event: any) { + if (this.searchTerm) return; + moveItemInArray( + this.actionButtons, + event.previousIndex, + event.currentIndex + ); + this.updateTable(); + } + + /** + * Updates the datasource to reflect the state of the action buttons and to apply the filter + */ + public updateTable() { + let actionButtons: any[]; + + if (this.searchTerm !== '') { + actionButtons = this.actionButtons.filter((action) => + action.text.toLowerCase().includes(this.searchTerm.toLowerCase()) + ); + } else { + actionButtons = this.actionButtons; + } + this.datasource.next([...actionButtons]); + } +} diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html new file mode 100644 index 0000000000..ad6d2ee0fb --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html @@ -0,0 +1 @@ +

edit-custom-row-action-modal works!

diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.scss b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.spec.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.spec.ts new file mode 100644 index 0000000000..a7917bfad9 --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditCustomRowActionModalComponent } from './edit-custom-row-action-modal.component'; + +describe('EditCustomRowActionModalComponent', () => { + let component: EditCustomRowActionModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditCustomRowActionModalComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EditCustomRowActionModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts new file mode 100644 index 0000000000..5f552b09f9 --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'shared-edit-custom-row-action-modal', + standalone: true, + imports: [CommonModule], + templateUrl: './edit-custom-row-action-modal.component.html', + styleUrls: ['./edit-custom-row-action-modal.component.scss'], +}) +export class EditCustomRowActionModalComponent {} diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html index a6659a152e..909a096647 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html @@ -51,10 +51,10 @@ > - + + + + diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts index c9dcde552f..c2ac04281c 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts @@ -45,6 +45,7 @@ import { SortingSettingsModule } from '../common/sorting-settings/sorting-settin import { TabActionsModule } from '../common/tab-actions/tab-actions.module'; import { TabMainModule } from './tab-main/tab-main.module'; import { TabGridActionsComponent } from './tab-grid-actions/tab-grid-actions.component'; +import { CustomRowActionsComponent } from './custom-row-actions/custom-row-actions.component'; /** * Modal content for the settings of the grid widgets. @@ -69,6 +70,7 @@ import { TabGridActionsComponent } from './tab-grid-actions/tab-grid-actions.com SortingSettingsModule, ToggleModule, ContextualFiltersSettingsComponent, + CustomRowActionsComponent, ], }) export class GridSettingsComponent diff --git a/libs/shared/src/lib/components/widgets/grid/action-button.type.ts b/libs/shared/src/lib/components/widgets/grid/action-button.type.ts new file mode 100644 index 0000000000..accbe324cc --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid/action-button.type.ts @@ -0,0 +1,39 @@ +import { Category, Variant } from '@oort-front/ui'; + +/** + * Action button Type + */ +export type ActionButton = { + text: string; + // Display + variant: Variant; + category: Category; + // Role restriction + hasRoleRestriction: boolean; + roles: string[]; + // Navigation + href?: string; + openInNewTab: boolean; + previousPage?: boolean; + // Edit Record + editRecord?: { + template?: string; + }; + // Clone Record + cloneRecord?: { + template?: string; + autoReload?: boolean; + onSave?: { + navigateTo?: { + targetUrl?: { + href?: string; + openInNewTab?: boolean; + }; + targetPage?: { + pageUrl?: string; + field?: string; + }; + }; + }; + }; +}; From e988bd8707ea4ab4f7dfbfa234be86fb43cf90aa Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 11:42:01 +0200 Subject: [PATCH 02/10] refactor: use factories for forms --- .../custom-row-actions.component.ts | 31 +- ...dit-custom-row-action-modal.component.html | 457 +++++++++++- .../edit-custom-row-action-modal.component.ts | 268 ++++++- .../grid-settings.component.html | 2 +- .../grid-settings/grid-settings.component.ts | 22 +- .../grid-settings/grid-settings.forms.ts | 688 ++++++++++++------ .../tab-grid-actions.component.ts | 12 +- .../summary-card-settings.component.ts | 21 +- .../summary-card-settings.forms.ts | 333 +++++---- 9 files changed, 1448 insertions(+), 386 deletions(-) diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts index 341c22a1a0..4820becd83 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; @@ -19,7 +19,11 @@ import { ApplicationService } from '../../../../services/application/application import { Role } from '../../../../models/user.model'; import { ActionButton } from '../../grid/action-button.type'; import { Dialog } from '@angular/cdk/dialog'; +import { Resource } from '../../../../models/resource.model'; +/** + * Custom row actions settings component. + */ @Component({ selector: 'shared-custom-row-actions', standalone: true, @@ -41,6 +45,8 @@ import { Dialog } from '@angular/cdk/dialog'; styleUrls: ['./custom-row-actions.component.scss'], }) export class CustomRowActionsComponent extends UnsubscribeComponent { + /** Resource associated with the grid */ + @Input() resource: Resource | null = null; /** List of action buttons from dashboard */ public actionButtons: ActionButton[] = []; /** Behavior subject to track change in action buttons */ @@ -49,10 +55,16 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { public searchTerm = ''; /** Columns to display */ public displayedColumns = ['dragDrop', 'name', 'roles', 'actions']; + /** Shared application service */ private applicationService = inject(ApplicationService); + /** Translate service */ private translate = inject(TranslateService); + /** Dialog service */ private dialog = inject(Dialog); + /** + * Add new action button + */ public async onAddActionButton() { const { EditCustomRowActionModalComponent } = await import( '../edit-custom-row-action-modal/edit-custom-row-action-modal.component' @@ -61,6 +73,7 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { EditCustomRowActionModalComponent, { data: { + resource: this.resource, disableClose: true, }, } @@ -76,6 +89,11 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { }); } + /** + * Edit action button + * + * @param actionButton Action button to edit + */ public async onEditActionButton(actionButton: ActionButton) { const { EditCustomRowActionModalComponent } = await import( '../edit-custom-row-action-modal/edit-custom-row-action-modal.component' @@ -84,6 +102,7 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { EditCustomRowActionModalComponent, { data: { + resource: this.resource, button: actionButton, disableClose: true, }, @@ -102,6 +121,11 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { }); } + /** + * Delete action button + * + * @param actionButton Action button to delete + */ public async onDeleteActionButton(actionButton: ActionButton) { const { ConfirmModalComponent } = await import( '../../../confirm-modal/confirm-modal.component' @@ -132,6 +156,11 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { }); } + /** + * Duplication action button + * + * @param actionButton Action button to duplicate + */ public async onDuplicateActionButton(actionButton: ActionButton) { const newActionButton = structuredClone(actionButton); newActionButton.text = `${newActionButton.text} (${this.translate.instant( diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html index ad6d2ee0fb..2b911ac5d8 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html @@ -1 +1,456 @@ -

edit-custom-row-action-modal works!

+ + +

+ {{ + (isNew + ? 'models.dashboard.actionButtons.create' + : 'models.dashboard.actionButtons.edit.one' + ) | translate + }} +

+
+ + +
+ + + + + + {{ 'common.general' | translate }} + + +
+ +
+ + +
+ + + + + {{ + 'models.dashboard.actionButtons.hasRoleRestriction' + | translate + }} + + + +
+ + + + {{ role.title }} + + +
+ + +
+ +
+ + + + {{ category }} + + +
+ + +
+ + + + {{ variant }} + + +
+
+ + +

{{ 'common.preview' | translate }}

+
+ + + {{ form.value.general.buttonText }} + +
+
+
+
+ + + + + + {{ + 'models.dashboard.actionButtons.action' | translate + }} + + +
+ + + + + {{ + 'models.dashboard.actionButtons.actions.navigateTo' + | translate + }} + + +
+ + + + {{ + 'models.dashboard.actionButtons.actions.previousPage' + | translate + }} + + + + + + {{ + 'models.dashboard.actionButtons.actions.url' + | translate + }} + + + +
+ + +
+ + + {{ + 'models.dashboard.actionButtons.actions.openInNewTab' + | translate + }} + + +
+
+
+
+ + + + + {{ + 'models.dashboard.actionButtons.actions.editRecord.text' + | translate + }} + + +
+ +
+ + + - + + {{ template.name }} + + + +
+ + + + {{ + 'models.dashboard.actionButtons.actions.autoReload' + | translate + }} + + +
+
+ + + + + {{ + 'models.dashboard.actionButtons.actions.cloneRecord.text' + | translate + }} + + +
+ +
+ + + - + + {{ template.name }} + + + +
+ +
+
+ + + {{ + 'models.dashboard.actionButtons.actions.cloneRecord.onSave.navigateTo' + | translate + }} + + +
+ +
+ + + {{ + 'components.widget.settings.grid.actions.goTo.target.label' + | translate + }} + + + +
+ + + + {{ page.name }} + + +
+
+ {{ + 'components.widget.settings.grid.actions.goTo.field.label' + | translate + }} + + + + {{ field.text || field.name }} + + + + {{ field.text || field.name }} + + {{ + field.name === '$attribute' + ? subField.text || subField.name + : field.name + + ' - ' + + (subField.text || subField.name) + }} + + + + +
+
+
+ +
+ + + {{ + 'models.dashboard.actionButtons.actions.url' + | translate + }} + + + +
+ + +
+ + + {{ + 'models.dashboard.actionButtons.actions.openInNewTab' + | translate + }} + + +
+
+
+
+
+ + + + {{ + 'models.dashboard.actionButtons.actions.autoReload' + | translate + }} + + +
+
+
+
+
+
+
+
+ + + + {{ + 'common.cancel' | translate + }} + + {{ 'common.save' | translate }} + + +
diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts index 5f552b09f9..6115120911 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts @@ -1,11 +1,273 @@ -import { Component } from '@angular/core'; +import { Component, Inject, Injector, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { + AlertModule, + ButtonModule, + DialogModule, + DividerModule, + FormWrapperModule, + IconModule, + SelectMenuModule, + TabsModule, + ToggleModule, + TooltipModule, + categories as ButtonCategories, + variants as ButtonVariants, +} from '@oort-front/ui'; +import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { EditorModule } from '@tinymce/tinymce-angular'; +import { EditorControlComponent } from '../../../controls/editor-control/editor-control.component'; +import { QueryBuilderModule } from '../../../query-builder/query-builder.module'; +import { ActionButton } from '../../grid/action-button.type'; +import { UnsubscribeComponent } from '../../../utils/unsubscribe/unsubscribe.component'; +import { RawEditorSettings } from 'tinymce'; +import { INLINE_EDITOR_CONFIG } from '../../../../const/tinymce.const'; +import { Role } from '../../../../models/user.model'; +import { Router } from '@angular/router'; +import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog'; +import { EditorService } from '../../../../services/editor/editor.service'; +import { DataTemplateService } from '../../../../services/data-template/data-template.service'; +import { ApplicationService } from '../../../../services/application/application.service'; +import { QueryBuilderService } from '../../../../services/query-builder/query-builder.service'; +import { Application } from '../../../../models/application.model'; +import { ContentType, Page } from '../../../../models/page.model'; +import { Form } from '../../../../models/form.model'; +import { Resource } from '../../../../models/resource.model'; +import { GridSettingsFormFactory } from '../grid-settings.forms'; +/** Dialog data interface */ +interface DialogData { + resource: Resource; + button: ActionButton; +} + +/** + * Edit custom row action modal component. + */ @Component({ selector: 'shared-edit-custom-row-action-modal', standalone: true, - imports: [CommonModule], + imports: [ + CommonModule, + DialogModule, + FormsModule, + ReactiveFormsModule, + TranslateModule, + FormWrapperModule, + SelectMenuModule, + ButtonModule, + ToggleModule, + EditorModule, + EditorControlComponent, + DividerModule, + TabsModule, + IconModule, + TooltipModule, + QueryBuilderModule, + AlertModule, + ], templateUrl: './edit-custom-row-action-modal.component.html', styleUrls: ['./edit-custom-row-action-modal.component.scss'], }) -export class EditCustomRowActionModalComponent {} +export class EditCustomRowActionModalComponent + extends UnsubscribeComponent + implements OnInit +{ + /** Form group */ + public form: FormGroup; + /** Button variants */ + public variants = ButtonVariants; + /** Button categories */ + public categories = ButtonCategories; + /** Is the action new */ + public isNew: boolean; + /** tinymce href editor */ + public hrefEditor: RawEditorSettings = INLINE_EDITOR_CONFIG; + /** Roles from current application */ + public roles: Role[]; + /** Fields, of current grid resource */ + public gridResourceFields: any[] = []; + /** Edit record template list */ + public editRecordTemplates: Form[] = []; + /** Available pages from the application for targetPage selection */ + public pages: any[] = []; + + /** + * Edit custom row action modal component. + * + * @param dialogRef Dialog reference + * @param data Dialog data + * @param editorService Shared editor service + * @param dataTemplateService Data template service + * @param router Angular router + * @param applicationService Shared application service + * @param queryBuilder Shared query builder + * @param injector Angular injector + */ + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) public data: DialogData, + private editorService: EditorService, + private dataTemplateService: DataTemplateService, + private router: Router, + public applicationService: ApplicationService, + private queryBuilder: QueryBuilderService, + private injector: Injector + ) { + super(); + this.roles = this.applicationService.application.value?.roles || []; + const factory = new GridSettingsFormFactory(this.injector, this.destroy$); + this.form = factory.createCustomRowActionFormGroup(data.button); + this.isNew = !data.button; + + // Set the editor base url based on the environment file + this.hrefEditor.base_url = editorService.url; + // Set the editor language + this.hrefEditor.language = editorService.language; + } + + ngOnInit(): void { + // Build list of pages for autocompletion in navigation settings + this.editorService.addCalcAndKeysAutoCompleter( + this.hrefEditor, + this.dataTemplateService.getAutoCompleterPageKeys() + ); + // Build list of pages for targetPage selection + this.pages = this.getPages(this.applicationService.application.getValue()); + // Populate templates & fields from resource + this.editRecordTemplates = this.data.resource.forms || []; + this.gridResourceFields = this.queryBuilder.getFields( + this.data.resource.queryName as string + ); + } + + /** On click on the preview button open the href */ + public preview(): void { + let href = this.form.get('action.navigateTo.targetUrl.href')?.value; + let isNewTab = + this.form.get('action.navigateTo.targetUrl.openInNewTab')?.value ?? true; + if (!href) { + href = this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetUrl.href' + )?.value; + isNewTab = + this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetUrl.openInNewTab' + )?.value ?? true; + } + if (href) { + //regex to verify if it's a page id key + const regex = /{{page\((.*?)\)}}/; + const match = href.match(regex); + if (match) { + href = this.dataTemplateService.getButtonLink(match[1]); + } + if (isNewTab) window.open(href, '_blank'); + else this.router.navigate([href]); + } + } + + /** On click on the save button close the dialog with the form value */ + public onSubmit(): void { + const button: ActionButton = { + text: this.form.get('general.buttonText')?.value, + hasRoleRestriction: this.form.get('general.hasRoleRestriction')?.value, + roles: this.form.get('general.roles')?.value, + category: this.form.get('general.category')?.value, + variant: this.form.get('general.variant')?.value, + // If navigateTo enabled + ...(this.form.get('action.navigateTo.enabled')?.value && { + previousPage: this.form.get('action.navigateTo.previousPage')?.value, + // If targetUrl enabled + ...(this.form.get('action.navigateTo.targetUrl.enabled')?.value && { + href: this.form.get('action.navigateTo.targetUrl.href')?.value, + openInNewTab: this.form.get( + 'action.navigateTo.targetUrl.openInNewTab' + )?.value, + }), + }), + // If editRecord enabled + ...(this.form.get('action.editRecord.enabled')?.value && { + editRecord: { + template: this.form.get('action.editRecord.template')?.value, + autoReload: this.form.get('action.editRecord.autoReload')?.value, + }, + }), + // If cloneRecord enabled + ...(this.form.get('action.cloneRecord.enabled')?.value && { + cloneRecord: { + template: this.form.get('action.cloneRecord.template')?.value, + autoReload: this.form.get('action.cloneRecord.autoReload')?.value, + onSave: { + ...(this.form.get('action.cloneRecord.onSave.navigateTo.enabled') + ?.value && { + navigateTo: { + // If targetPage enabled + ...(this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetPage.enabled' + )?.value && { + targetPage: { + pageUrl: this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetPage.pageUrl' + )?.value, + field: this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetPage.field' + )?.value, + }, + }), + // If targetUrl enabled + ...(this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetUrl.enabled' + )?.value && { + targetUrl: { + href: this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetUrl.href' + )?.value, + openInNewTab: this.form.get( + 'action.cloneRecord.onSave.navigateTo.targetUrl.openInNewTab' + )?.value, + }, + }), + }, + }), + }, + }, + }), + }; + + this.dialogRef.close(button); + } + + /** + * Get available pages from app + * + * @param application application + * @returns list of pages and their url + */ + private getPages(application: Application | null) { + return ( + application?.pages?.map((page: any) => ({ + id: page.id, + name: page.name, + urlParams: this.getPageUrlParams(application, page), + placeholder: `{{page(${page.id})}}`, + })) || [] + ); + } + + /** + * Get page url params + * + * @param application application + * @param page page to get url from + * @returns url of the page + */ + private getPageUrlParams(application: Application, page: Page): string { + const applicationPath = + this.applicationService.getApplicationPath(application); + return page.type === ContentType.form + ? `${applicationPath}/${page.type}/${page.id}` + : `${applicationPath}/${page.type}/${page.content}`; + } +} diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html index 909a096647..5f8f77aab6 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html @@ -53,7 +53,7 @@ - + diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts index c2ac04281c..b1ff3434f2 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.ts @@ -6,6 +6,7 @@ import { Output, EventEmitter, AfterViewInit, + Injector, } from '@angular/core'; import { UntypedFormArray, @@ -25,7 +26,6 @@ import { Resource, ResourceQueryResponse, } from '../../../models/resource.model'; -import { createGridWidgetFormGroup } from './grid-settings.forms'; import { UnsubscribeComponent } from '../../utils/unsubscribe/unsubscribe.component'; import { takeUntil } from 'rxjs/operators'; import { AggregationService } from '../../../services/aggregation/aggregation.service'; @@ -46,6 +46,7 @@ import { TabActionsModule } from '../common/tab-actions/tab-actions.module'; import { TabMainModule } from './tab-main/tab-main.module'; import { TabGridActionsComponent } from './tab-grid-actions/tab-grid-actions.component'; import { CustomRowActionsComponent } from './custom-row-actions/custom-row-actions.component'; +import { GridSettingsFormFactory } from './grid-settings.forms'; /** * Modal content for the settings of the grid widgets. @@ -78,16 +79,22 @@ export class GridSettingsComponent implements OnInit, AfterViewInit, - WidgetSettings + WidgetSettings< + typeof GridSettingsFormFactory.prototype.createGridWidgetFormGroup + > { /** Widget */ @Input() widget: any; /** Event emitter for change */ @Output() formChange: EventEmitter< - ReturnType + ReturnType< + typeof GridSettingsFormFactory.prototype.createGridWidgetFormGroup + > > = new EventEmitter(); /** Form group */ - public widgetFormGroup!: ReturnType; + public widgetFormGroup!: ReturnType< + typeof GridSettingsFormFactory.prototype.createGridWidgetFormGroup + >; /** Form array for filters */ public filtersFormArray: any = null; /** List of channels */ @@ -118,6 +125,7 @@ export class GridSettingsComponent * @param fb Angular form builder * @param aggregationService Shared aggregation service * @param emailService Email Service + * @param injector Angular injector */ constructor( private apollo: Apollo, @@ -125,7 +133,8 @@ export class GridSettingsComponent private queryBuilder: QueryBuilderService, private fb: FormBuilder, private aggregationService: AggregationService, - private emailService: EmailService + private emailService: EmailService, + private injector: Injector ) { super(); } @@ -416,7 +425,8 @@ export class GridSettingsComponent * Build the settings form, using the widget saved parameters */ public buildSettingsForm() { - this.widgetFormGroup = createGridWidgetFormGroup( + const factory = new GridSettingsFormFactory(this.injector, this.destroy$); + this.widgetFormGroup = factory.createGridWidgetFormGroup( this.widget.id, this.widget.settings ); diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts index 761a5d5a66..3eb516cf58 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts @@ -4,6 +4,7 @@ import { FormBuilder, FormControl, ValidationErrors, + ValidatorFn, Validators, } from '@angular/forms'; import get from 'lodash/get'; @@ -12,6 +13,11 @@ import { createQueryForm, } from '../../query-builder/query-builder-forms'; import { extendWidgetForm } from '../common/display-settings/extendWidgetForm'; +import { ActionButton } from '../grid/action-button.type'; +import { Role } from '../../../models/user.model'; +import { Injector } from '@angular/core'; +import { ApplicationService } from '../../../services/application/application.service'; +import { Subject, takeUntil } from 'rxjs'; /** Default action name */ const DEFAULT_ACTION_NAME = 'Action'; @@ -22,229 +28,491 @@ const DEFAULT_CONTEXT_FILTER = `{ "filters": [] }`; -/** Creating a new instance of the FormBuilder class. */ -const fb = new FormBuilder(); - /** - * Create Grid Action. - * - * @param value default value ( if any ) - * @returns Grid Action Form Group + * Grid settings form factory */ -export const createGridActionFormGroup = (value: any) => { - const formGroup = fb.group({ - show: [value && value.show ? value.show : false, Validators.required], - name: [ - value && value.name ? value.name : DEFAULT_ACTION_NAME, - Validators.required, - ], - selectAll: [value && value.selectAll ? value.selectAll : false], - selectPage: [value && value.selectPage ? value.selectPage : false], - goToNextStep: [get(value, 'goToNextStep', false)], - goToPreviousStep: [get(value, 'goToPreviousStep', false)], - prefillForm: [value && value.prefillForm ? value.prefillForm : false], - prefillTargetForm: [ - value && value.prefillTargetForm ? value.prefillTargetForm : null, - value && value.prefillForm ? Validators.required : null, - ], - closeWorkflow: [value && value.closeWorkflow ? value.closeWorkflow : false], - confirmationText: [ - value && value.confirmationText ? value.confirmationText : '', - value && value.closeWorkflow ? Validators.required : null, - ], - autoSave: [value && value.autoSave ? value.autoSave : false], - modifySelectedRows: [value ? value.modifySelectedRows : false], - modifications: fb.array( - value && value.modifications && value.modifications.length - ? value.modifications.map((x: any) => - fb.group({ - field: [x.field, Validators.required], - value: [x.value], - }) - ) - : [] - ), - attachToRecord: [get(value, 'attachToRecord', false)], - targetResource: [get(value, 'targetResource', null)], - targetForm: [get(value, 'targetForm', null)], - targetFormField: [get(value, 'targetFormField', null)], - targetFormQuery: createQueryForm( - value && value.targetFormQuery ? value.targetFormQuery : null, - Boolean(value && value.targetForm) - ), - notify: [value && value.notify ? value.notify : false], - notificationChannel: [ - value && value.notificationChannel ? value.notificationChannel : null, - value && value.notify ? Validators.required : null, - ], - notificationMessage: [ - value && value.notificationMessage - ? value.notificationMessage - : 'Records update', - ], - publish: [value && value.publish ? value.publish : false], - publicationChannel: [ - value && value.publicationChannel ? value.publicationChannel : null, - value && value.publish ? Validators.required : null, - ], - sendMail: [value && value.sendMail ? value.sendMail : false], - distributionList: [ - get(value, 'distributionList', null), - value && value.sendMail ? Validators.required : null, - ], - templates: [ - get(value, 'templates', []), - value && value.sendMail ? Validators.required : null, - ], - export: [value && value.export ? value.export : false], - bodyFields: fb.array( - value && value.bodyFields - ? value.bodyFields.map((x: any) => addNewField(x)) - : [], - value && value.sendMail ? Validators.required : null - ), - navigateToPage: [ - value && value.navigateToPage ? value.navigateToPage : false, - ], - }); - // Avoid goToNextStep & goToPreviousStep to coexist - if (formGroup.get('goToNextStep')?.value) { - formGroup.get('goToPreviousStep')?.setValue(false); - } else if (formGroup.get('goToPreviousStep')?.value) { - formGroup.get('goToNextStep')?.setValue(false); +export class GridSettingsFormFactory { + /** Roles from current application */ + public roles: Role[]; + /** Form Builder */ + private fb!: FormBuilder; + /** Application Service */ + private applicationService!: ApplicationService; + + /** + * Grid settings form factory + * + * @param injector Angular injector + * @param destroy$ Destroy reference + */ + constructor(private injector: Injector, public destroy$: Subject) { + this.fb = this.injector.get(FormBuilder); + this.applicationService = this.injector.get(ApplicationService); + this.roles = this.applicationService.application.value?.roles || []; } - formGroup.get('goToNextStep')?.valueChanges.subscribe((value) => { - if (value) { - formGroup.get('goToPreviousStep')?.setValue(false); - } - }); - formGroup.get('goToPreviousStep')?.valueChanges.subscribe((value) => { - if (value) { - formGroup.get('goToNextStep')?.setValue(false); - } - }); - return formGroup; -}; -/** - * Create a grid widget form group. - * - * @param id id of the widget - * @param configuration previous configuration - * @returns form group - */ -export const createGridWidgetFormGroup = (id: string, configuration: any) => { - const formGroup = fb.group( - { - id, - title: [get(configuration, 'title', ''), Validators.required], - resource: [get(configuration, 'resource', null), Validators.required], - template: [get(configuration, 'template', null)], - layouts: [get(configuration, 'layouts', []), Validators.required], - aggregations: [ - get(configuration, 'aggregations', []), + /** + * Create Grid Action. + * + * @param value default value ( if any ) + * @returns Grid Action Form Group + */ + createGridActionFormGroup = (value: any) => { + const formGroup = this.fb.group({ + show: [value && value.show ? value.show : false, Validators.required], + name: [ + value && value.name ? value.name : DEFAULT_ACTION_NAME, Validators.required, ], - actions: createGridActionsFormGroup(configuration), - floatingButtons: fb.array( - configuration.floatingButtons && configuration.floatingButtons.length - ? configuration.floatingButtons.map((x: any) => - createGridActionFormGroup(x) + selectAll: [value && value.selectAll ? value.selectAll : false], + selectPage: [value && value.selectPage ? value.selectPage : false], + goToNextStep: [get(value, 'goToNextStep', false)], + goToPreviousStep: [get(value, 'goToPreviousStep', false)], + prefillForm: [value && value.prefillForm ? value.prefillForm : false], + prefillTargetForm: [ + value && value.prefillTargetForm ? value.prefillTargetForm : null, + value && value.prefillForm ? Validators.required : null, + ], + closeWorkflow: [ + value && value.closeWorkflow ? value.closeWorkflow : false, + ], + confirmationText: [ + value && value.confirmationText ? value.confirmationText : '', + value && value.closeWorkflow ? Validators.required : null, + ], + autoSave: [value && value.autoSave ? value.autoSave : false], + modifySelectedRows: [value ? value.modifySelectedRows : false], + modifications: this.fb.array( + value && value.modifications && value.modifications.length + ? value.modifications.map((x: any) => + this.fb.group({ + field: [x.field, Validators.required], + value: [x.value], + }) ) - : [createGridActionFormGroup(null)] - ) as FormArray>, - sortFields: new FormArray([]), - contextFilters: [ - get(configuration, 'contextFilters', DEFAULT_CONTEXT_FILTER), + : [] + ), + attachToRecord: [get(value, 'attachToRecord', false)], + targetResource: [get(value, 'targetResource', null)], + targetForm: [get(value, 'targetForm', null)], + targetFormField: [get(value, 'targetFormField', null)], + targetFormQuery: createQueryForm( + value && value.targetFormQuery ? value.targetFormQuery : null, + Boolean(value && value.targetForm) + ), + notify: [value && value.notify ? value.notify : false], + notificationChannel: [ + value && value.notificationChannel ? value.notificationChannel : null, + value && value.notify ? Validators.required : null, ], - at: get(configuration, 'at', ''), - }, - { - validators: [templateRequiredWhenAddRecord], + notificationMessage: [ + value && value.notificationMessage + ? value.notificationMessage + : 'Records update', + ], + publish: [value && value.publish ? value.publish : false], + publicationChannel: [ + value && value.publicationChannel ? value.publicationChannel : null, + value && value.publish ? Validators.required : null, + ], + sendMail: [value && value.sendMail ? value.sendMail : false], + distributionList: [ + get(value, 'distributionList', null), + value && value.sendMail ? Validators.required : null, + ], + templates: [ + get(value, 'templates', []), + value && value.sendMail ? Validators.required : null, + ], + export: [value && value.export ? value.export : false], + bodyFields: this.fb.array( + value && value.bodyFields + ? value.bodyFields.map((x: any) => addNewField(x)) + : [], + value && value.sendMail ? Validators.required : null + ), + navigateToPage: [ + value && value.navigateToPage ? value.navigateToPage : false, + ], + }); + // Avoid goToNextStep & goToPreviousStep to coexist + if (formGroup.get('goToNextStep')?.value) { + formGroup.get('goToPreviousStep')?.setValue(false); + } else if (formGroup.get('goToPreviousStep')?.value) { + formGroup.get('goToNextStep')?.setValue(false); } - ); - const extendedForm = extendWidgetForm( - formGroup, - configuration?.widgetDisplay - ); - return extendedForm; -}; + formGroup.get('goToNextStep')?.valueChanges.subscribe((value) => { + if (value) { + formGroup.get('goToPreviousStep')?.setValue(false); + } + }); + formGroup.get('goToPreviousStep')?.valueChanges.subscribe((value) => { + if (value) { + formGroup.get('goToNextStep')?.setValue(false); + } + }); + return formGroup; + }; -/** - * Validators for checking that a template is selected when configuring "add record" action. - * - * @param group form group - * @returns validation errors - */ -export const templateRequiredWhenAddRecord = ( - group: AbstractControl -): ValidationErrors | null => { - const templateControl = group.get('template'); - const addRecordControl = group.get('actions.addRecord'); - - if (templateControl && addRecordControl) { - const templateValue = templateControl.value; - const addRecordValue = addRecordControl.value; - - if (addRecordValue && !templateValue) { - addRecordControl.setErrors({ - missingTemplate: true, - }); - return { - actions: { - addRecord: { - missingTemplate: true, + /** + * Create a grid widget form group. + * + * @param id id of the widget + * @param configuration previous configuration + * @returns form group + */ + public createGridWidgetFormGroup = (id: string, configuration: any) => { + const formGroup = this.fb.group( + { + id, + title: [get(configuration, 'title', ''), Validators.required], + resource: [get(configuration, 'resource', null), Validators.required], + template: [get(configuration, 'template', null)], + layouts: [get(configuration, 'layouts', []), Validators.required], + aggregations: [ + get(configuration, 'aggregations', []), + Validators.required, + ], + actions: this.createGridActionsFormGroup(configuration), + floatingButtons: this.fb.array( + configuration.floatingButtons && configuration.floatingButtons.length + ? configuration.floatingButtons.map((x: any) => + this.createGridActionFormGroup(x) + ) + : [this.createGridActionFormGroup(null)] + ) as FormArray>, + customRowActions: this.fb.array( + configuration.customRowActions && + configuration.customRowActions.length + ? configuration.customRowActions.map((x: any) => + this.createCustomRowActionFormGroup(x) + ) + : [] + ) as FormArray>, + sortFields: new FormArray([]), + contextFilters: [ + get(configuration, 'contextFilters', DEFAULT_CONTEXT_FILTER), + ], + at: get(configuration, 'at', ''), + }, + { + validators: [this.templateRequiredWhenAddRecord], + } + ); + const extendedForm = extendWidgetForm( + formGroup, + configuration?.widgetDisplay + ); + return extendedForm; + }; + + /** + * Validators for checking that a template is selected when configuring "add record" action. + * + * @param group form group + * @returns validation errors + */ + templateRequiredWhenAddRecord = ( + group: AbstractControl + ): ValidationErrors | null => { + const templateControl = group.get('template'); + const addRecordControl = group.get('actions.addRecord'); + + if (templateControl && addRecordControl) { + const templateValue = templateControl.value; + const addRecordValue = addRecordControl.value; + + if (addRecordValue && !templateValue) { + addRecordControl.setErrors({ + missingTemplate: true, + }); + return { + actions: { + addRecord: { + missingTemplate: true, + }, }, + }; + } else { + addRecordControl.setErrors(null); + } + } + return null; + }; + + /** + * Creates a form group for the grid settings with the given grid actions configuration + * + * @param configuration configuration to build up the grid actions form group + * @returns form group with the given grid actions configuration + */ + createGridActionsFormGroup = (configuration: any) => { + const formGroup = this.fb.group({ + delete: [get(configuration, 'actions.delete', true)], + history: [get(configuration, 'actions.history', true)], + convert: [get(configuration, 'actions.convert', true)], + update: [get(configuration, 'actions.update', true)], + inlineEdition: [get(configuration, 'actions.inlineEdition', true)], + addRecord: [get(configuration, 'actions.addRecord', false)], + export: [get(configuration, 'actions.export', true)], + showDetails: [get(configuration, 'actions.showDetails', true)], + navigateToPage: [get(configuration, 'actions.navigateToPage', false)], + navigateSettings: this.fb.group({ + pageUrl: [get(configuration, 'actions.navigateSettings.pageUrl', '')], + field: [get(configuration, 'actions.navigateSettings.field', '')], + title: [ + get(configuration, 'actions.navigateSettings.title', 'Details view'), + ], + }), + }); + // Set validators ot navigate to page title option, based on other params + const setValidatorsNavigateToPageTitle = (value: boolean) => { + if (value) { + formGroup + .get('navigateSettings.title') + ?.setValidators(Validators.required); + } else { + formGroup.get('navigateSettings.title')?.clearValidators(); + } + formGroup.get('navigateSettings.title')?.updateValueAndValidity(); + }; + // Initialize + setValidatorsNavigateToPageTitle(formGroup.get('navigateToPage')?.value); + // Subscribe to changes + formGroup.get('navigateToPage')?.valueChanges.subscribe((value) => { + setValidatorsNavigateToPageTitle(value); + }); + return formGroup; + }; + + /** + * Create new custom row action form group + * + * @param value initial value + * @returns Form group + */ + public createCustomRowActionFormGroup = (value: ActionButton) => { + const form = this.fb.group({ + general: this.fb.group({ + buttonText: [get(value, 'text', ''), Validators.required], + hasRoleRestriction: [ + get(value, 'hasRoleRestriction', false), + Validators.required, + ], + roles: [ + get( + value, + 'roles', + this.roles.map((role) => role.id || '') + ), + ], + category: [get(value, 'category', 'secondary')], + variant: [get(value, 'variant', 'primary')], + }), + action: this.fb.group( + { + navigateTo: this.fb.group( + { + enabled: [ + !!get(value, 'href', false) || get(value, 'previousPage'), + ], + previousPage: [get(value, 'previousPage', false)], + targetUrl: this.fb.group({ + enabled: [!!get(value, 'href', false)], + href: [get(value, 'href', '')], + openInNewTab: [get(value, 'openInNewTab', true)], + }), + }, + { validator: this.navigateToValidator } + ), + editRecord: this.fb.group({ + enabled: [!!get(value, 'editRecord', false)], + template: [get(value, 'editRecord.template', '')], + autoReload: [get(value, 'editRecord.autoReload', false)], + }), + cloneRecord: this.fb.group({ + enabled: [!!get(value, 'cloneRecord', false)], + template: [get(value, 'cloneRecord.template', '')], + autoReload: [get(value, 'cloneRecord.autoReload', false)], + onSave: this.fb.group({ + navigateTo: this.fb.group( + { + // Enabled if one of the two navigateTo options is set + enabled: [ + !!get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.href', + false + ) || + !!get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.pageUrl', + false + ), + ], + targetUrl: this.fb.group({ + enabled: [ + !!get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.href', + false + ), + ], + href: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.href', + '' + ), + ], + openInNewTab: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.openInNewTab', + true + ), + ], + }), + targetPage: this.fb.group({ + enabled: [ + !!get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.pageUrl', + false + ), + ], + pageUrl: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.pageUrl', + '' + ), + ], + field: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.field', + '' + ), + ], + }), + }, + { validator: this.cloneRecordNavigateToValidator } + ), + }), + }), }, - }; - } else { - addRecordControl.setErrors(null); + { validator: this.actionValidator } + ), + }); + + // Set up mutual exclusivity + // Between action controls + this.setupMutualExclusivity([ + form.get('action.navigateTo.enabled'), + form.get('action.editRecord.enabled'), + form.get('action.cloneRecord.enabled'), + ] as AbstractControl[]); + // Between navigateTo controls + this.setupMutualExclusivity([ + form.get('action.navigateTo.previousPage'), + form.get('action.navigateTo.targetUrl.enabled'), + ] as AbstractControl[]); + // Between navigateTo controls of clone record action + this.setupMutualExclusivity([ + form.get('action.cloneRecord.onSave.navigateTo.targetPage.enabled'), + form.get('action.cloneRecord.onSave.navigateTo.targetUrl.enabled'), + ] as AbstractControl[]); + return form; + }; + + /** + * Utility function to set up mutual exclusivity for a set of controls + * + * @param controls Array of controls to set up mutual exclusivity for + */ + private setupMutualExclusivity = (controls: AbstractControl[]) => { + controls.forEach((control, index) => { + control?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((value: boolean | null) => { + if (value) { + controls.forEach((otherControl, otherIndex) => { + if (index !== otherIndex) { + otherControl?.setValue(false, { emitEvent: false }); + } + }); + } + }); + }); + }; + + /** + * Validator to ensure that at least one action is enabled + * + * @param control form group + * @returns validation errors + */ + private actionValidator: ValidatorFn = ( + control: AbstractControl + ): ValidationErrors | null => { + const actions = control.value; + if (actions) { + const atLeastOneEnabled = + actions.navigateTo?.enabled || + actions.editRecord?.enabled || + actions.cloneRecord?.enabled; + + return atLeastOneEnabled ? null : { atLeastOneRequired: true }; } - } - return null; -}; + return { atLeastOneRequired: true }; + }; -/** - * Creates a form group for the grid settings with the given grid actions configuration - * - * @param configuration configuration to build up the grid actions form group - * @returns form group with the given grid actions configuration - */ -export const createGridActionsFormGroup = (configuration: any) => { - const formGroup = fb.group({ - delete: [get(configuration, 'actions.delete', true)], - history: [get(configuration, 'actions.history', true)], - convert: [get(configuration, 'actions.convert', true)], - update: [get(configuration, 'actions.update', true)], - inlineEdition: [get(configuration, 'actions.inlineEdition', true)], - addRecord: [get(configuration, 'actions.addRecord', false)], - export: [get(configuration, 'actions.export', true)], - showDetails: [get(configuration, 'actions.showDetails', true)], - navigateToPage: [get(configuration, 'actions.navigateToPage', false)], - navigateSettings: fb.group({ - pageUrl: [get(configuration, 'actions.navigateSettings.pageUrl', '')], - field: [get(configuration, 'actions.navigateSettings.field', '')], - title: [ - get(configuration, 'actions.navigateSettings.title', 'Details view'), - ], - }), - }); - // Set validators ot navigate to page title option, based on other params - const setValidatorsNavigateToPageTitle = (value: boolean) => { - if (value) { - formGroup - .get('navigateSettings.title') - ?.setValidators(Validators.required); - } else { - formGroup.get('navigateSettings.title')?.clearValidators(); + /** + * Validator to ensure that at least one navigateTo action is enabled + * + * @param control form group + * @returns validation errors + */ + private navigateToValidator: ValidatorFn = ( + control: AbstractControl + ): ValidationErrors | null => { + const navigateTo = control.value; + if (navigateTo?.enabled) { + const atLeastOneEnabled = + navigateTo.previousPage || navigateTo.targetUrl?.enabled; + const hrefValid = + !navigateTo.targetUrl?.enabled || + (navigateTo.targetUrl.enabled && navigateTo.targetUrl.href); + if (!atLeastOneEnabled) return { atLeastOneRequired: true }; + if (!hrefValid) return { hrefRequired: true }; + } + return null; + }; + + /** + * Validator to ensure that at least one clone record navigateTo action is enabled + * + * @param control form group + * @returns validation errors + */ + private cloneRecordNavigateToValidator: ValidatorFn = ( + control: AbstractControl + ): ValidationErrors | null => { + const navigateTo = control.value; + if (navigateTo?.enabled) { + const atLeastOneEnabled = + navigateTo.targetPage?.enabled || navigateTo.targetUrl?.enabled; + const hrefValid = + !navigateTo.targetUrl?.enabled || + (navigateTo.targetUrl.enabled && navigateTo.targetUrl.href); + const pageUrlValid = + !navigateTo.targetPage?.enabled || + (navigateTo.targetPage.enabled && navigateTo.targetPage.pageUrl); + if (!atLeastOneEnabled) return { atLeastOneRequired: true }; + if (!hrefValid) return { hrefRequired: true }; + if (!pageUrlValid) return { pageUrlRequired: true }; } - formGroup.get('navigateSettings.title')?.updateValueAndValidity(); + return null; }; - // Initialize - setValidatorsNavigateToPageTitle(formGroup.get('navigateToPage')?.value); - // Subscribe to changes - formGroup.get('navigateToPage')?.valueChanges.subscribe((value) => { - setValidatorsNavigateToPageTitle(value); - }); - return formGroup; -}; +} diff --git a/libs/shared/src/lib/components/widgets/grid-settings/tab-grid-actions/tab-grid-actions.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/tab-grid-actions/tab-grid-actions.component.ts index ba567ef1eb..b4c8fdefdf 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/tab-grid-actions/tab-grid-actions.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/tab-grid-actions/tab-grid-actions.component.ts @@ -1,6 +1,8 @@ import { Component, EventEmitter, + inject, + Injector, Input, Output, ViewChild, @@ -11,7 +13,7 @@ import { UntypedFormArray, UntypedFormGroup, } from '@angular/forms'; -import { createGridActionFormGroup } from '../grid-settings.forms'; +import { GridSettingsFormFactory } from '../grid-settings.forms'; import { Form } from '../../../../models/form.model'; import { Channel } from '../../../../models/channel.model'; import { @@ -30,6 +32,7 @@ import { import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { GridActionSettingsComponent } from '../grid-action-settings/grid-action-settings.component'; +import { UnsubscribeComponent } from '../../../utils/unsubscribe/unsubscribe.component'; /** * Grid Actions configuration tab. @@ -53,7 +56,7 @@ import { GridActionSettingsComponent } from '../grid-action-settings/grid-action TooltipModule, ], }) -export class TabGridActionsComponent { +export class TabGridActionsComponent extends UnsubscribeComponent { /** Form group */ @Input() formGroup!: UntypedFormGroup; /** List of fields */ @@ -68,6 +71,8 @@ export class TabGridActionsComponent { @Input() distributionLists: any[] = []; /** Emits when the select channel is opened for the first time */ @Output() loadChannels = new EventEmitter(); + /** Angular injector */ + private injector = inject(Injector); /** Tabs component */ @ViewChild(TabsComponent, { static: false }) tabGroup!: TabsComponent; @@ -85,10 +90,11 @@ export class TabGridActionsComponent { * @param event Mouse event */ public addAction(event: MouseEvent): void { + const factory = new GridSettingsFormFactory(this.injector, this.destroy$); const gridActions = this.formGroup?.get( 'floatingButtons' ) as UntypedFormArray; - gridActions.push(createGridActionFormGroup({ show: true })); + gridActions.push(factory.createGridActionFormGroup({ show: true })); // Open new action this.tabGroup.selectedIndex = gridActions.length - 1; event.stopPropagation(); diff --git a/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.component.ts b/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.component.ts index f7da7d508e..30bf90c12e 100644 --- a/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.component.ts +++ b/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.component.ts @@ -2,6 +2,7 @@ import { AfterViewInit, Component, EventEmitter, + Injector, Input, OnDestroy, OnInit, @@ -30,7 +31,7 @@ import { UnsubscribeComponent } from '../../utils/unsubscribe/unsubscribe.compon import { GET_REFERENCE_DATA, GET_RESOURCE } from './graphql/queries'; import { startWith, takeUntil } from 'rxjs'; import { Form } from '../../../models/form.model'; -import { createSummaryCardForm } from './summary-card-settings.forms'; +import { SummaryCardSettingsFormFactory } from './summary-card-settings.forms'; import { ReferenceData, ReferenceDataQueryResponse, @@ -63,7 +64,9 @@ import { DisplayTabModule } from './display-tab/display.module'; import { TabActionsModule } from '../common/tab-actions/tab-actions.module'; import { SortingSettingsModule } from '../common/sorting-settings/sorting-settings.module'; -export type SummaryCardFormT = ReturnType; +export type SummaryCardFormT = ReturnType< + typeof SummaryCardSettingsFormFactory.prototype.createSummaryCardForm +>; /** * Summary Card Settings component. @@ -106,7 +109,9 @@ export class SummaryCardSettingsComponent OnInit, AfterViewInit, OnDestroy, - WidgetSettings + WidgetSettings< + typeof SummaryCardSettingsFormFactory.prototype.createSummaryCardForm + > { /** Widget configuration */ @Input() widget: any; @@ -155,12 +160,14 @@ export class SummaryCardSettingsComponent * @param aggregationService Shared aggregation service * @param fb FormBuilder instance * @param widgetService Shared widget service + * @param injector Angular injector */ constructor( private apollo: Apollo, private aggregationService: AggregationService, private fb: FormBuilder, - private widgetService: WidgetService + private widgetService: WidgetService, + private injector: Injector ) { super(); } @@ -450,7 +457,11 @@ export class SummaryCardSettingsComponent * Build the settings form, using the widget saved parameters. */ public buildSettingsForm() { - this.widgetFormGroup = createSummaryCardForm( + const factory = new SummaryCardSettingsFormFactory( + this.injector, + this.destroy$ + ); + this.widgetFormGroup = factory.createSummaryCardForm( this.widget.id, this.widget.settings ); diff --git a/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.forms.ts b/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.forms.ts index 90af119331..a0c48adb2c 100644 --- a/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.forms.ts +++ b/libs/shared/src/lib/components/widgets/summary-card-settings/summary-card-settings.forms.ts @@ -6,79 +6,13 @@ import { ValidationErrors, } from '@angular/forms'; import get from 'lodash/get'; -import { createGridActionsFormGroup } from '../grid-settings/grid-settings.forms'; +import { GridSettingsFormFactory } from '../grid-settings/grid-settings.forms'; import { extendWidgetForm } from '../common/display-settings/extendWidgetForm'; import isNil from 'lodash/isNil'; import { mutuallyExclusive } from '../../../utils/validators/mutuallyExclusive.validator'; import { createAutomationForm } from '../../../forms/automation.forms'; - -/** Creating a new instance of the FormBuilder class. */ -const fb = new FormBuilder(); - -/** - * Create a summary card form from definition - * - * @param id id of the widget - * @param configuration Widget configuration - * @returns Summary card widget form - */ -export const createSummaryCardForm = (id: string, configuration: any) => { - const formGroup = fb.group( - { - id, - title: get(configuration, 'title', ''), - card: createCardForm(get(configuration, 'card', null)), - sortFields: new FormArray([]), - contextFilters: get( - configuration, - 'contextFilters', - DEFAULT_CONTEXT_FILTER - ), - actions: createGridActionsFormGroup(configuration), - at: get(configuration, 'at', ''), - // Automation - automationRules: fb.array>( - get(configuration, 'automationRules', []).map((rule: any) => - createAutomationForm(rule) - ) - ), - }, - { - validators: [templateRequiredWhenAddRecord], - } - ); - - const isUsingAggregation = !!get(configuration, 'card.aggregation', null); - const searchable = isUsingAggregation - ? false - : get(configuration, 'widgetDisplay.searchable', false); - - const extendedForm = extendWidgetForm( - formGroup, - configuration?.widgetDisplay, - { - searchable: new FormControl(searchable), - usePagination: new FormControl( - get(configuration, 'widgetDisplay.usePagination', false) - ), - usePadding: new FormControl( - get(configuration, 'widgetDisplay.usePadding', true) - ), - exportable: new FormControl( - get(configuration, 'widgetDisplay.exportable', true) - ), - gridMode: new FormControl( - get(configuration, 'widgetDisplay.gridMode', true) - ), - } - ); - - // disable searchable if aggregation is selected - if (isUsingAggregation) - extendedForm.get('widgetDisplay.searchable')?.disable(); - - return extendedForm; -}; +import { Injector } from '@angular/core'; +import { Subject } from 'rxjs'; // todo: put in common /** Default context filter value. */ @@ -88,102 +22,189 @@ const DEFAULT_CONTEXT_FILTER = `{ }`; /** - * Validators for checking that a template is selected when configuring "add record" action. - * - * @param group form group - * @returns validation errors + * Factory for creating summary card settings forms */ -export const templateRequiredWhenAddRecord = ( - group: AbstractControl -): ValidationErrors | null => { - const templateControl = group.get('card.template'); - const addRecordControl = group.get('actions.addRecord'); - - if (templateControl && addRecordControl) { - const templateValue = templateControl.value; - const addRecordValue = addRecordControl.value; +export class SummaryCardSettingsFormFactory { + /** Form Builder */ + private fb!: FormBuilder; - if (addRecordValue && !templateValue) { - addRecordControl.setErrors({ - missingTemplate: true, - }); - return { - actions: { - addRecord: { - missingTemplate: true, - }, - }, - }; - } else { - addRecordControl.setErrors(null); - } + /** + * Factory for creating summary card settings forms + * + * @param injector Angular injector + * @param destroy$ Destroy reference + */ + constructor(private injector: Injector, public destroy$: Subject) { + this.fb = this.injector.get(FormBuilder); } - return null; -}; -/** - * Create a card form - * - * @param value card value, optional - * @returns card as form group - */ -const createCardForm = (value?: any) => { - const formGroup = fb.group( - { - title: get(value, 'title', 'New Card'), - referenceData: get(value, 'referenceData', null), - referenceDataVariableMapping: [ - get(value, 'referenceDataVariableMapping', null), - ], - resource: get(value, 'resource', null), - template: get(value, 'template', null), - layout: get(value, 'layout', null), - aggregation: get(value, 'aggregation', null), - html: get(value, 'html', null), - showDataSourceLink: [ - { - value: get(value, 'showDataSourceLink', false), - disabled: !isNil(get(value, 'referenceData', null)), - }, - ], - useStyles: get(value, 'useStyles', true), - wholeCardStyles: get(value, 'wholeCardStyles', false), - usePadding: get(value, 'usePadding', true), - }, - { - validators: [ - mutuallyExclusive({ - required: true, - fields: ['resource', 'referenceData'], - }), - ], - } - ); - if (formGroup.value.resource) { - formGroup.addValidators( - mutuallyExclusive({ - required: true, - fields: ['layout', 'aggregation'], - }) + /** + * Create a summary card form from definition + * + * @param id id of the widget + * @param configuration Widget configuration + * @returns Summary card widget form + */ + public createSummaryCardForm = (id: string, configuration: any) => { + const gridFactory = new GridSettingsFormFactory( + this.injector, + this.destroy$ ); - } - formGroup.controls.resource.valueChanges.subscribe((value) => { - if (value) { + const formGroup = this.fb.group( + { + id, + title: get(configuration, 'title', ''), + card: this.createCardForm(get(configuration, 'card', null)), + sortFields: new FormArray([]), + contextFilters: get( + configuration, + 'contextFilters', + DEFAULT_CONTEXT_FILTER + ), + actions: gridFactory.createGridActionsFormGroup(configuration), + at: get(configuration, 'at', ''), + // Automation + automationRules: this.fb.array>( + get(configuration, 'automationRules', []).map((rule: any) => + createAutomationForm(rule) + ) + ), + }, + { + validators: [this.templateRequiredWhenAddRecord], + } + ); + + const isUsingAggregation = !!get(configuration, 'card.aggregation', null); + const searchable = isUsingAggregation + ? false + : get(configuration, 'widgetDisplay.searchable', false); + + const extendedForm = extendWidgetForm( + formGroup, + configuration?.widgetDisplay, + { + searchable: new FormControl(searchable), + usePagination: new FormControl( + get(configuration, 'widgetDisplay.usePagination', false) + ), + usePadding: new FormControl( + get(configuration, 'widgetDisplay.usePadding', true) + ), + exportable: new FormControl( + get(configuration, 'widgetDisplay.exportable', true) + ), + gridMode: new FormControl( + get(configuration, 'widgetDisplay.gridMode', true) + ), + } + ); + + // disable searchable if aggregation is selected + if (isUsingAggregation) + extendedForm.get('widgetDisplay.searchable')?.disable(); + + return extendedForm; + }; + + /** + * Create new card form group + * + * @param value initial value + * @returns Form Group + */ + private createCardForm = (value?: any) => { + const formGroup = this.fb.group( + { + title: get(value, 'title', 'New Card'), + referenceData: get(value, 'referenceData', null), + referenceDataVariableMapping: [ + get(value, 'referenceDataVariableMapping', null), + ], + resource: get(value, 'resource', null), + template: get(value, 'template', null), + layout: get(value, 'layout', null), + aggregation: get(value, 'aggregation', null), + html: get(value, 'html', null), + showDataSourceLink: [ + { + value: get(value, 'showDataSourceLink', false), + disabled: !isNil(get(value, 'referenceData', null)), + }, + ], + useStyles: get(value, 'useStyles', true), + wholeCardStyles: get(value, 'wholeCardStyles', false), + usePadding: get(value, 'usePadding', true), + }, + { + validators: [ + mutuallyExclusive({ + required: true, + fields: ['resource', 'referenceData'], + }), + ], + } + ); + if (formGroup.value.resource) { formGroup.addValidators( mutuallyExclusive({ required: true, fields: ['layout', 'aggregation'], }) ); - } else { - formGroup.setValidators([ - mutuallyExclusive({ - required: true, - fields: ['resource', 'referenceData'], - }), - ]); } - formGroup.updateValueAndValidity(); - }); - return formGroup; -}; + formGroup.controls.resource.valueChanges.subscribe((value) => { + if (value) { + formGroup.addValidators( + mutuallyExclusive({ + required: true, + fields: ['layout', 'aggregation'], + }) + ); + } else { + formGroup.setValidators([ + mutuallyExclusive({ + required: true, + fields: ['resource', 'referenceData'], + }), + ]); + } + formGroup.updateValueAndValidity(); + }); + return formGroup; + }; + + /** + * Validators for checking that a template is selected when configuring "add record" action. + * + * @param group form group + * @returns validation errors + */ + templateRequiredWhenAddRecord = ( + group: AbstractControl + ): ValidationErrors | null => { + const templateControl = group.get('card.template'); + const addRecordControl = group.get('actions.addRecord'); + + if (templateControl && addRecordControl) { + const templateValue = templateControl.value; + const addRecordValue = addRecordControl.value; + + if (addRecordValue && !templateValue) { + addRecordControl.setErrors({ + missingTemplate: true, + }); + return { + actions: { + addRecord: { + missingTemplate: true, + }, + }, + }; + } else { + addRecordControl.setErrors(null); + } + } + return null; + }; +} From ea2d2b7692e8629f087b916cbd0e127e88d7167a Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 13:08:46 +0200 Subject: [PATCH 03/10] modify for edition --- .../custom-row-actions.component.ts | 36 +++++- .../edit-custom-row-action-modal.component.ts | 2 +- .../grid-settings.component.html | 2 +- .../grid-settings/grid-settings.forms.ts | 119 ++++++++++++++++-- 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts index 4820becd83..7629e97026 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, Input } from '@angular/core'; +import { Component, inject, Injector, Input, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; @@ -20,6 +20,7 @@ import { Role } from '../../../../models/user.model'; import { ActionButton } from '../../grid/action-button.type'; import { Dialog } from '@angular/cdk/dialog'; import { Resource } from '../../../../models/resource.model'; +import { GridSettingsFormFactory } from '../grid-settings.forms'; /** * Custom row actions settings component. @@ -44,7 +45,14 @@ import { Resource } from '../../../../models/resource.model'; templateUrl: './custom-row-actions.component.html', styleUrls: ['./custom-row-actions.component.scss'], }) -export class CustomRowActionsComponent extends UnsubscribeComponent { +export class CustomRowActionsComponent + extends UnsubscribeComponent + implements OnInit +{ + /** Widget form group */ + @Input() formGroup!: ReturnType< + typeof GridSettingsFormFactory.prototype.createGridWidgetFormGroup + >; /** Resource associated with the grid */ @Input() resource: Resource | null = null; /** List of action buttons from dashboard */ @@ -61,6 +69,19 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { private translate = inject(TranslateService); /** Dialog service */ private dialog = inject(Dialog); + /** Angular injector */ + private injector = inject(Injector); + /** Form factory */ + private formFactory = new GridSettingsFormFactory( + this.injector, + this.destroy$ + ); + + ngOnInit(): void { + this.actionButtons = + (this.formGroup.controls.customRowActions.value as any[]) || []; + this.updateTable(); + } /** * Add new action button @@ -83,6 +104,9 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { .pipe(takeUntil(this.destroy$)) .subscribe(async (button) => { if (!button) return; + this.formGroup.controls.customRowActions.push( + this.formFactory.createCustomRowActionFormGroup(button) + ); this.actionButtons.push(button); this.searchTerm = ''; this.updateTable(); @@ -115,6 +139,10 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { if (!button) return; const index = this.actionButtons.indexOf(actionButton); if (index > -1) { + this.formGroup.controls.customRowActions.setControl( + index, + this.formFactory.createCustomRowActionFormGroup(button) + ); this.actionButtons[index] = button; this.updateTable(); } @@ -148,6 +176,7 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { if (value) { const index = this.actionButtons.indexOf(actionButton); if (index > -1) { + this.formGroup.controls.customRowActions.removeAt(index); this.actionButtons.splice(index, 1); this.searchTerm = ''; this.updateTable(); @@ -166,6 +195,9 @@ export class CustomRowActionsComponent extends UnsubscribeComponent { newActionButton.text = `${newActionButton.text} (${this.translate.instant( 'common.copy' )})`; + this.formGroup.controls.customRowActions.push( + this.formFactory.createCustomRowActionFormGroup(newActionButton) + ); this.actionButtons.push(newActionButton); this.searchTerm = ''; this.updateTable(); diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts index 6115120911..c52c6731fa 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts @@ -118,7 +118,7 @@ export class EditCustomRowActionModalComponent super(); this.roles = this.applicationService.application.value?.roles || []; const factory = new GridSettingsFormFactory(this.injector, this.destroy$); - this.form = factory.createCustomRowActionFormGroup(data.button); + this.form = factory.createCustomRowActionFormGroupForEdition(data.button); this.isNew = !data.button; // Set the editor base url based on the environment file diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html index 5f8f77aab6..e617e2af77 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.component.html @@ -53,7 +53,7 @@ - + diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts index 3eb516cf58..31f8fac29d 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts @@ -181,14 +181,13 @@ export class GridSettingsFormFactory { ) : [this.createGridActionFormGroup(null)] ) as FormArray>, - customRowActions: this.fb.array( - configuration.customRowActions && - configuration.customRowActions.length - ? configuration.customRowActions.map((x: any) => - this.createCustomRowActionFormGroup(x) - ) - : [] - ) as FormArray>, + customRowActions: this.fb.array< + ReturnType + >( + (configuration.customRowActions || []).map((x: any) => + this.createCustomRowActionFormGroup(x) + ) + ), sortFields: new FormArray([]), contextFilters: [ get(configuration, 'contextFilters', DEFAULT_CONTEXT_FILTER), @@ -286,12 +285,114 @@ export class GridSettingsFormFactory { }; /** - * Create new custom row action form group + * Creates a form group for a custom row action, this is the model we save in the database * * @param value initial value * @returns Form group */ public createCustomRowActionFormGroup = (value: ActionButton) => { + const form = this.fb.group({ + text: [get(value, 'text', ''), Validators.required], + hasRoleRestriction: [ + get(value, 'hasRoleRestriction', false), + Validators.required, + ], + roles: [get(value, 'roles', [])], + category: [get(value, 'category', 'secondary')], + variant: [get(value, 'variant', 'primary')], + ...((!!get(value, 'href', false) || get(value, 'previousPage')) && { + navigateTo: this.fb.group({ + previousPage: [get(value, 'previousPage', false)], + ...(!!get(value, 'href', false) && { + targetUrl: this.fb.group({ + href: [get(value, 'href', '')], + openInNewTab: [get(value, 'openInNewTab', true)], + }), + }), + }), + }), + ...(!!get(value, 'editRecord', false) && { + editRecord: this.fb.group({ + template: [get(value, 'editRecord.template', '')], + autoReload: [get(value, 'editRecord.autoReload', false)], + }), + }), + ...(!!get(value, 'cloneRecord', false) && { + cloneRecord: this.fb.group({ + template: [get(value, 'cloneRecord.template', '')], + autoReload: [get(value, 'cloneRecord.autoReload', false)], + onSave: this.fb.group({ + ...((!!get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.href', + false + ) || + !!get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.pageUrl', + false + )) && { + navigateTo: this.fb.group({ + ...(!!get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.href', + false + ) && { + targetUrl: this.fb.group({ + href: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.href', + '' + ), + ], + openInNewTab: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetUrl.openInNewTab', + true + ), + ], + }), + }), + ...(!!get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.pageUrl', + false + ) && { + targetPage: this.fb.group({ + pageUrl: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.pageUrl', + '' + ), + ], + field: [ + get( + value, + 'cloneRecord.onSave.navigateTo.targetPage.field', + '' + ), + ], + }), + }), + }), + }), + }), + }), + }), + }); + return form; + }; + + /** + * Create new custom row action form group, optimized for edition modal + * + * @param value initial value + * @returns Form group + */ + public createCustomRowActionFormGroupForEdition = (value: ActionButton) => { const form = this.fb.group({ general: this.fb.group({ buttonText: [get(value, 'text', ''), Validators.required], From 4a033de1ace5c3dd7ea112e11583bc891c65be77 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 15:53:28 +0200 Subject: [PATCH 04/10] refactor action button to support grid --- .../action-button/action-button.component.ts | 66 ++++++------------- .../action-buttons.component.html | 5 +- .../action-buttons.component.ts | 32 ++++++++- 3 files changed, 51 insertions(+), 52 deletions(-) diff --git a/libs/shared/src/lib/components/action-button/action-button.component.ts b/libs/shared/src/lib/components/action-button/action-button.component.ts index 0d4be510d0..47f854f0c9 100644 --- a/libs/shared/src/lib/components/action-button/action-button.component.ts +++ b/libs/shared/src/lib/components/action-button/action-button.component.ts @@ -12,7 +12,7 @@ import { ButtonModule, TooltipModule } from '@oort-front/ui'; import { TranslateModule } from '@ngx-translate/core'; import { Dialog } from '@angular/cdk/dialog'; import { DataTemplateService } from '../../services/data-template/data-template.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { EmailService } from '../email/email.service'; import { Apollo } from 'apollo-angular'; import { EmailService as SharedEmailService } from '../../services/email/email.service'; @@ -26,7 +26,6 @@ import { lastValueFrom, map, of, Subject, takeUntil, tap } from 'rxjs'; import { Resource, ResourceQueryResponse } from '../../models/resource.model'; import { GET_RECORD_BY_ID, GET_RESOURCE_BY_ID } from './graphql/queries'; import { EDIT_RECORD } from './graphql/mutations'; -import { Dashboard } from '../../models/dashboard.model'; import { EditRecordMutationResponse, RecordQueryResponse, @@ -52,14 +51,14 @@ export class ActionButtonComponent { /** Action button definition */ @Input() actionButton!: ActionButton; - /** Dashboard */ - @Input() dashboard?: Dashboard; /** Should refresh button, some of them ( subscribe / unsubscribe ) can depend on other buttons */ @Input() refresh!: Subject; - /** Reload dashboard event emitter */ - @Output() reloadDashboard = new EventEmitter(); - /** Context id of the current dashboard */ - public contextId!: string; + /** Record id */ + @Input() recordId?: string; + /** Resource id */ + @Input() resourceId?: string; + /** Reload parent event emitter */ + @Output() reloadParent = new EventEmitter(); /** Email notification, for subscribe & unsubscribe actions */ private emailNotification?: EmailNotification; /** Current environment */ @@ -67,10 +66,10 @@ export class ActionButtonComponent /** @returns Should hide button */ get showButton(): boolean { - if (this.actionButton.editRecord && !this.contextId) { + if (this.actionButton.editRecord && !this.recordId) { return false; } - if (this.actionButton.cloneRecord && !this.contextId) { + if (this.actionButton.cloneRecord && !this.recordId) { return false; } if (this.actionButton.subscribeToNotification) { @@ -98,7 +97,6 @@ export class ActionButtonComponent * @param dataTemplateService DataTemplate service * @param router Angular router * @param emailService Email service - * @param activatedRoute Activated route * @param apollo Apollo * @param location Angular location * @param sharedEmailService Shared email service @@ -114,7 +112,6 @@ export class ActionButtonComponent private dataTemplateService: DataTemplateService, private router: Router, private emailService: EmailService, - private activatedRoute: ActivatedRoute, private apollo: Apollo, private location: Location, private sharedEmailService: SharedEmailService, @@ -126,11 +123,6 @@ export class ActionButtonComponent ) { super(); this.environment = environment; - this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe({ - next: ({ id }) => { - this.contextId = id; - }, - }); } ngOnInit(): void { @@ -218,7 +210,7 @@ export class ActionButtonComponent this.actionButton.sendNotification.distributionList ) { try { - const selectedIds = !isNil(this.contextId) ? [this.contextId] : []; + const selectedIds = !isNil(this.recordId) ? [this.recordId] : []; const templates = await this.getSelectedNotificationTemplates( this.actionButton.sendNotification.templates || [] ); @@ -246,10 +238,8 @@ export class ActionButtonComponent ); const snackBarSpinner = snackBarRef.instance.nestedComponent; let resource!: Resource; - if (this.dashboard?.page?.context?.resource) { - resource = (await this.getResourceById( - this.dashboard?.page?.context?.resource - )) as Resource; + if (this.resourceId) { + resource = (await this.getResourceById(this.resourceId)) as Resource; } const distributionList = await this.getSelectedDistributionListData( this.actionButton.sendNotification.distributionList @@ -315,11 +305,11 @@ export class ActionButtonComponent // Prefill data for addRecord & cloneRecord const loadPrefillData$ = () => { - if (this.actionButton.cloneRecord && this.contextId) { + if (this.actionButton.cloneRecord && this.recordId) { return this.apollo .query({ query: GET_RECORD_BY_ID, - variables: { id: this.contextId, includeResource: false }, + variables: { id: this.recordId, includeResource: false }, }) .pipe( takeUntil(this.destroy$), @@ -350,13 +340,13 @@ export class ActionButtonComponent // Callback to be executed at the end of action const callback = () => { if (shouldReload) { - this.reloadDashboard.emit(); + this.reloadParent.emit(); } }; const dialogRef = this.dialog.open(FormModalComponent, { disableClose: true, data: { - ...(this.actionButton.editRecord && { recordId: this.contextId }), // Modal will open current record + ...(this.actionButton.editRecord && { recordId: this.recordId }), // Modal will open current record ...(template && { template }), actionButtonCtx: true, prefillData, @@ -374,7 +364,7 @@ export class ActionButtonComponent this.actionButton.addRecord.fieldsForUpdate || []; // Execute callback if possible if ( - this.contextId && + this.recordId && Array.isArray(fieldsForUpdate) && fieldsForUpdate.length > 0 ) { @@ -382,7 +372,7 @@ export class ActionButtonComponent .query({ query: GET_RECORD_BY_ID, variables: { - id: this.contextId, + id: this.recordId, includeResource: true, }, }) @@ -420,7 +410,7 @@ export class ActionButtonComponent .mutate({ mutation: EDIT_RECORD, variables: { - id: this.contextId, + id: this.recordId, data: update, }, }) @@ -519,24 +509,6 @@ export class ActionButtonComponent return distributionListResponse.emailDistributionLists.edges[0].node; } - /** - * Get default resource meta data - * - * @param fields Selected resource fields for the given action button - * @returns default resource meta data - */ - private async getResourceMetaData(fields: string[]) { - const { data: resourceMetaDataResponse } = await lastValueFrom( - // Fetch resource metadata for email sending - this.queryBuilder.getQueryMetaData( - this.dashboard?.page?.context?.resource as string - ) - ); - return resourceMetaDataResponse.resource.metadata?.filter((md) => - fields.includes(md.name) - ); - } - /** * Fetch resource data needed for field display * diff --git a/libs/shared/src/lib/components/action-buttons/action-buttons.component.html b/libs/shared/src/lib/components/action-buttons/action-buttons.component.html index dc975d1174..2ee4201991 100644 --- a/libs/shared/src/lib/components/action-buttons/action-buttons.component.html +++ b/libs/shared/src/lib/components/action-buttons/action-buttons.component.html @@ -3,8 +3,9 @@ diff --git a/libs/shared/src/lib/components/action-buttons/action-buttons.component.ts b/libs/shared/src/lib/components/action-buttons/action-buttons.component.ts index 4e8e5a78a3..b094bcd138 100644 --- a/libs/shared/src/lib/components/action-buttons/action-buttons.component.ts +++ b/libs/shared/src/lib/components/action-buttons/action-buttons.component.ts @@ -1,9 +1,18 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + Component, + EventEmitter, + inject, + Input, + OnInit, + Output, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActionButtonComponent } from '../action-button/action-button.component'; import { ActionButton } from '../action-button/action-button.type'; import { Dashboard } from '../../models/dashboard.model'; -import { Subject } from 'rxjs'; +import { Subject, takeUntil } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { UnsubscribeComponent } from '../utils/unsubscribe/unsubscribe.component'; /** * Dashboard action buttons component. @@ -15,7 +24,10 @@ import { Subject } from 'rxjs'; templateUrl: './action-buttons.component.html', styleUrls: ['./action-buttons.component.scss'], }) -export class ActionButtonsComponent { +export class ActionButtonsComponent + extends UnsubscribeComponent + implements OnInit +{ /** List of action buttons */ @Input() actionButtons: ActionButton[] = []; /** Dashboard */ @@ -24,4 +36,18 @@ export class ActionButtonsComponent { @Output() reloadDashboard = new EventEmitter(); /** Should refresh buttons, some of them ( subscribe / unsubscribe ) can depend on other buttons */ public refresh = new Subject(); + /** Context id of the current dashboard, if available */ + public contextId?: string; + /** Activated route */ + private activatedRoute = inject(ActivatedRoute); + + ngOnInit(): void { + if (this.dashboard) { + this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe({ + next: ({ id }) => { + this.contextId = id; + }, + }); + } + } } From 8bfe1b93969a07cf236612e4799aaaf80b4d1f86 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 16:35:32 +0200 Subject: [PATCH 05/10] custom row actions now injectable in grid --- .../ui/core-grid/core-grid.component.html | 1 + .../ui/core-grid/grid/grid.component.html | 18 +++++++++++++++++ .../ui/core-grid/grid/grid.component.ts | 20 +++++++++++++++++++ .../ui/core-grid/grid/grid.module.ts | 2 ++ ...dit-custom-row-action-modal.component.html | 18 ----------------- .../edit-custom-row-action-modal.component.ts | 4 ++-- .../grid-settings/grid-settings.forms.ts | 16 +++------------ .../widgets/grid/action-button.type.ts | 1 - 8 files changed, 46 insertions(+), 34 deletions(-) diff --git a/libs/shared/src/lib/components/ui/core-grid/core-grid.component.html b/libs/shared/src/lib/components/ui/core-grid/core-grid.component.html index 55f915d45a..db454c81c1 100644 --- a/libs/shared/src/lib/components/ui/core-grid/core-grid.component.html +++ b/libs/shared/src/lib/components/ui/core-grid/core-grid.component.html @@ -31,5 +31,6 @@ [canAdd]="canCreateRecords" [canDownload]="canDownloadRecords" [searchable]="searchable" + (reload)="reloadData()" > diff --git a/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html b/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html index 2967f14008..5818ba9825 100644 --- a/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html +++ b/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html @@ -1129,6 +1129,24 @@ + + + + + + { + if (action.cloneRecord) { + action.cloneRecord.autoReload = true; + } + if (action.editRecord) { + action.editRecord.autoReload = true; + } + return action; + }); } } diff --git a/libs/shared/src/lib/components/ui/core-grid/grid/grid.module.ts b/libs/shared/src/lib/components/ui/core-grid/grid/grid.module.ts index 3d9252abde..09aad62cb9 100644 --- a/libs/shared/src/lib/components/ui/core-grid/grid/grid.module.ts +++ b/libs/shared/src/lib/components/ui/core-grid/grid/grid.module.ts @@ -24,6 +24,7 @@ import { GridColumnChooserModule } from '../grid-column-chooser/grid-column-choo import { GridRowActionsModule } from '../row-actions/row-actions.module'; import { GridToolbarModule } from '../toolbar/toolbar.module'; import { GridComponent } from './grid.component'; +import { ActionButtonComponent } from '../../../action-button/action-button.component'; /** Module for the grid component */ @NgModule({ @@ -61,6 +62,7 @@ import { GridComponent } from './grid.component'; uiButtonModule, // === Pipes == StripHtmlPipe, + ActionButtonComponent, ], exports: [GridComponent], }) diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html index 2b911ac5d8..e296ff1b73 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html @@ -219,15 +219,6 @@

{{ 'common.preview' | translate }}

" > - - - - {{ - 'models.dashboard.actionButtons.actions.autoReload' - | translate - }} - - @@ -420,15 +411,6 @@

{{ 'common.preview' | translate }}

- - - - {{ - 'models.dashboard.actionButtons.actions.autoReload' - | translate - }} - - diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts index c52c6731fa..dc65e97437 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts @@ -191,14 +191,12 @@ export class EditCustomRowActionModalComponent ...(this.form.get('action.editRecord.enabled')?.value && { editRecord: { template: this.form.get('action.editRecord.template')?.value, - autoReload: this.form.get('action.editRecord.autoReload')?.value, }, }), // If cloneRecord enabled ...(this.form.get('action.cloneRecord.enabled')?.value && { cloneRecord: { template: this.form.get('action.cloneRecord.template')?.value, - autoReload: this.form.get('action.cloneRecord.autoReload')?.value, onSave: { ...(this.form.get('action.cloneRecord.onSave.navigateTo.enabled') ?.value && { @@ -236,6 +234,8 @@ export class EditCustomRowActionModalComponent }), }; + console.log(button); + this.dialogRef.close(button); } diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts index 31f8fac29d..ab568ef7dd 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts @@ -300,27 +300,17 @@ export class GridSettingsFormFactory { roles: [get(value, 'roles', [])], category: [get(value, 'category', 'secondary')], variant: [get(value, 'variant', 'primary')], - ...((!!get(value, 'href', false) || get(value, 'previousPage')) && { - navigateTo: this.fb.group({ - previousPage: [get(value, 'previousPage', false)], - ...(!!get(value, 'href', false) && { - targetUrl: this.fb.group({ - href: [get(value, 'href', '')], - openInNewTab: [get(value, 'openInNewTab', true)], - }), - }), - }), - }), + previousPage: [get(value, 'previousPage', false)], + href: [get(value, 'href', '')], + openInNewTab: [get(value, 'openInNewTab', true)], ...(!!get(value, 'editRecord', false) && { editRecord: this.fb.group({ template: [get(value, 'editRecord.template', '')], - autoReload: [get(value, 'editRecord.autoReload', false)], }), }), ...(!!get(value, 'cloneRecord', false) && { cloneRecord: this.fb.group({ template: [get(value, 'cloneRecord.template', '')], - autoReload: [get(value, 'cloneRecord.autoReload', false)], onSave: this.fb.group({ ...((!!get( value, diff --git a/libs/shared/src/lib/components/widgets/grid/action-button.type.ts b/libs/shared/src/lib/components/widgets/grid/action-button.type.ts index accbe324cc..52ba3bd8b4 100644 --- a/libs/shared/src/lib/components/widgets/grid/action-button.type.ts +++ b/libs/shared/src/lib/components/widgets/grid/action-button.type.ts @@ -22,7 +22,6 @@ export type ActionButton = { // Clone Record cloneRecord?: { template?: string; - autoReload?: boolean; onSave?: { navigateTo?: { targetUrl?: { From fadd590062ab269e480ddca537ff452a0a1ffa34 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 16:44:26 +0200 Subject: [PATCH 06/10] improve cdk preview --- .../custom-row-actions.component.scss | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss index e69de29bb2..77c3981dad 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss @@ -0,0 +1,26 @@ +.cdk-drag-preview { + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); + + td { + display: flex; + align-items: center; + } +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} From f2c681c7be7d9beb263b31b4768367bf070edf44 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Tue, 21 Oct 2025 16:45:49 +0200 Subject: [PATCH 07/10] apply similar changes to edit action buttons modal --- .../edit-action-buttons-modal.component.scss | 28 +++++++++++++++++++ .../custom-row-actions.component.scss | 2 ++ 2 files changed, 30 insertions(+) diff --git a/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.scss b/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.scss index e69de29bb2..ebd97e3d49 100644 --- a/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.scss +++ b/apps/back-office/src/app/components/edit-action-buttons-modal/edit-action-buttons-modal.component.scss @@ -0,0 +1,28 @@ +// === Drag and Drop Styles === +.cdk-drag-preview { + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); + + td { + display: flex; + align-items: center; + } +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} +// === End Drag and Drop Styles === diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss index 77c3981dad..ebd97e3d49 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.scss @@ -1,3 +1,4 @@ +// === Drag and Drop Styles === .cdk-drag-preview { display: flex; align-items: center; @@ -24,3 +25,4 @@ .cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } +// === End Drag and Drop Styles === From 429fee008499482f84891dff2468de18995a2483 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Fri, 24 Oct 2025 09:43:46 +0200 Subject: [PATCH 08/10] Add column label to custom row actions --- libs/shared/src/i18n/en.json | 7 ++++++ .../ui/core-grid/grid/grid.component.html | 21 +++++++++------- .../ui/core-grid/grid/grid.component.ts | 25 +++++++++---------- ...dit-custom-row-action-modal.component.html | 8 ++++++ .../edit-custom-row-action-modal.component.ts | 1 + .../grid-settings/grid-settings.forms.ts | 2 ++ .../widgets/grid/action-button.type.ts | 1 + 7 files changed, 43 insertions(+), 22 deletions(-) diff --git a/libs/shared/src/i18n/en.json b/libs/shared/src/i18n/en.json index 1ec2464275..b3ba429e70 100644 --- a/libs/shared/src/i18n/en.json +++ b/libs/shared/src/i18n/en.json @@ -1870,6 +1870,13 @@ }, "openEditor": "Open html", "openSettings": "Open settings", + "rowAction": { + "fields": { + "columnLabel": { + "text": "Column label" + } + } + }, "tooltip": { "displayEditor": "Display html in modal", "displayMap": "Display map", diff --git a/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html b/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html index 5818ba9825..6a8b5043a9 100644 --- a/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html +++ b/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.html @@ -1131,20 +1131,23 @@ - +
+ + + +
diff --git a/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.ts b/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.ts index 8ad9a993d3..24fe0dc760 100644 --- a/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.ts +++ b/libs/shared/src/lib/components/ui/core-grid/grid/grid.component.ts @@ -36,7 +36,7 @@ import { CompositeFilterDescriptor, SortDescriptor, } from '@progress/kendo-data-query'; -import { get, has, intersection, isEqual, isNil } from 'lodash'; +import { get, groupBy, has, intersection, isEqual, isNil, map } from 'lodash'; import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { DownloadService } from '../../../../services/download/download.service'; import { GridDataFormatterService } from '../../../../services/grid-data-formatter/grid-data-formatter.service'; @@ -54,7 +54,7 @@ import { SELECTABLE_SETTINGS, } from './grid.constants'; import { DocumentManagementService } from '../../../../services/document-management/document-management.service'; -import { ActionButton } from '../../../action-button/action-button.type'; +import { ActionButton } from '../../../widgets/grid/action-button.type'; /** Minimum column width */ const MIN_COLUMN_WIDTH = 100; @@ -233,8 +233,9 @@ export class GridComponent private columnChooserRef: PopupRef | null = null; /** Prevent next column reset */ private preventColumnResize = false; - /** Custom row action buttons */ - public customRowActions: ActionButton[] = []; + /** Custom row action button groups */ + public customRowActionGroups: { label: string; actions: ActionButton[] }[] = + []; /** @returns show border of grid */ get showBorder(): boolean { @@ -414,15 +415,13 @@ export class GridComponent 'settings.customRowActions', [] ); - this.customRowActions = customRowActions.map((action) => { - if (action.cloneRecord) { - action.cloneRecord.autoReload = true; - } - if (action.editRecord) { - action.editRecord.autoReload = true; - } - return action; - }); + this.customRowActionGroups = map( + groupBy(customRowActions, 'columnLabel'), + (actions, label) => ({ + label, + actions, + }) + ); } } diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html index e296ff1b73..86f5b06710 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html @@ -25,6 +25,14 @@

+ +
+ + +
+

+ {{ 'models.dashboard.actionButtons.visibleTo' | translate }} @@ -73,6 +92,7 @@

Custom Row Actions

+ diff --git a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts index 7629e97026..7446808e95 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/custom-row-actions/custom-row-actions.component.ts @@ -62,7 +62,13 @@ export class CustomRowActionsComponent /** Current search string */ public searchTerm = ''; /** Columns to display */ - public displayedColumns = ['dragDrop', 'name', 'roles', 'actions']; + public displayedColumns = [ + 'dragDrop', + 'columnLabel', + 'text', + 'roles', + 'actions', + ]; /** Shared application service */ private applicationService = inject(ApplicationService); /** Translate service */ @@ -238,8 +244,12 @@ export class CustomRowActionsComponent let actionButtons: any[]; if (this.searchTerm !== '') { - actionButtons = this.actionButtons.filter((action) => - action.text.toLowerCase().includes(this.searchTerm.toLowerCase()) + actionButtons = this.actionButtons.filter( + (action) => + action.columnLabel + .toLowerCase() + .includes(this.searchTerm.toLowerCase()) || + action.text.toLowerCase().includes(this.searchTerm.toLowerCase()) ); } else { actionButtons = this.actionButtons; From bcc7a72832b1e0556ff8637c2eb55e4f4d4206e5 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Fri, 24 Oct 2025 10:27:55 +0200 Subject: [PATCH 10/10] Add filter condition to show / hide action buttons --- ...dit-custom-row-action-modal.component.html | 23 +++++++++- .../edit-custom-row-action-modal.component.ts | 45 +++++++++++++++++-- .../graphql/queries.ts | 31 +++++++++++++ .../grid-settings/grid-settings.forms.ts | 3 ++ .../widgets/grid/action-button.type.ts | 2 + 5 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/graphql/queries.ts diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html index 86f5b06710..b1684234c1 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.html @@ -28,7 +28,8 @@

@@ -424,6 +425,26 @@

{{ 'common.preview' | translate }}

+ + + + + + {{ 'common.filter.one' | translate }} + + + + + +
diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts index cd74e46dc2..2bf29bf397 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/edit-custom-row-action-modal.component.ts @@ -13,6 +13,7 @@ import { TooltipModule, categories as ButtonCategories, variants as ButtonVariants, + SpinnerModule, } from '@oort-front/ui'; import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; @@ -33,8 +34,15 @@ import { QueryBuilderService } from '../../../../services/query-builder/query-bu import { Application } from '../../../../models/application.model'; import { ContentType, Page } from '../../../../models/page.model'; import { Form } from '../../../../models/form.model'; -import { Resource } from '../../../../models/resource.model'; +import { + Resource, + ResourceQueryResponse, +} from '../../../../models/resource.model'; import { GridSettingsFormFactory } from '../grid-settings.forms'; +import { FilterModule } from '../../../filter/filter.module'; +import { Apollo } from 'apollo-angular'; +import { takeUntil } from 'rxjs'; +import { GET_RESOURCE_METADATA } from './graphql/queries'; /** Dialog data interface */ interface DialogData { @@ -66,6 +74,8 @@ interface DialogData { TooltipModule, QueryBuilderModule, AlertModule, + FilterModule, + SpinnerModule, ], templateUrl: './edit-custom-row-action-modal.component.html', styleUrls: ['./edit-custom-row-action-modal.component.scss'], @@ -92,6 +102,10 @@ export class EditCustomRowActionModalComponent public editRecordTemplates: Form[] = []; /** Available pages from the application for targetPage selection */ public pages: any[] = []; + /** Filters available to set filters from */ + public filterFields: any[] = []; + /** Loading state */ + public loading = true; /** * Edit custom row action modal component. @@ -104,6 +118,7 @@ export class EditCustomRowActionModalComponent * @param applicationService Shared application service * @param queryBuilder Shared query builder * @param injector Angular injector + * @param apollo Apollo service */ constructor( public dialogRef: DialogRef, @@ -113,7 +128,8 @@ export class EditCustomRowActionModalComponent private router: Router, public applicationService: ApplicationService, private queryBuilder: QueryBuilderService, - private injector: Injector + private injector: Injector, + private apollo: Apollo ) { super(); this.roles = this.applicationService.application.value?.roles || []; @@ -140,6 +156,8 @@ export class EditCustomRowActionModalComponent this.gridResourceFields = this.queryBuilder.getFields( this.data.resource.queryName as string ); + // Get filter fields + this.getFilterFields(); } /** On click on the preview button open the href */ @@ -233,10 +251,10 @@ export class EditCustomRowActionModalComponent }, }, }), + // Filter + filter: this.form.get('filter')?.value, }; - console.log(button); - this.dialogRef.close(button); } @@ -271,4 +289,23 @@ export class EditCustomRowActionModalComponent ? `${applicationPath}/${page.type}/${page.id}` : `${applicationPath}/${page.type}/${page.content}`; } + + /** + * Get filter fields for the resource + */ + private getFilterFields() { + this.apollo + .query({ + query: GET_RESOURCE_METADATA, + variables: { + id: this.data.resource.id, + }, + fetchPolicy: 'cache-first', + }) + .pipe(takeUntil(this.destroy$)) + .subscribe(({ data }) => { + this.filterFields = data.resource.metadata ?? []; + this.loading = false; + }); + } } diff --git a/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/graphql/queries.ts b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/graphql/queries.ts new file mode 100644 index 0000000000..d317ecf743 --- /dev/null +++ b/libs/shared/src/lib/components/widgets/grid-settings/edit-custom-row-action-modal/graphql/queries.ts @@ -0,0 +1,31 @@ +import { gql } from 'apollo-angular'; + +/** Graphql request to get resource metadata */ +export const GET_RESOURCE_METADATA = gql` + query GetResourceMetadata($id: ID!) { + resource(id: $id) { + id + name + metadata { + name + automated + type + editor + filter + multiSelect + filterable + options + fields { + name + automated + type + editor + filter + multiSelect + filterable + options + } + } + } + } +`; diff --git a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts index 9a3d42776c..9c3d2b0e20 100644 --- a/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts +++ b/libs/shared/src/lib/components/widgets/grid-settings/grid-settings.forms.ts @@ -10,6 +10,7 @@ import { import get from 'lodash/get'; import { addNewField, + createFilterGroup, createQueryForm, } from '../../query-builder/query-builder-forms'; import { extendWidgetForm } from '../common/display-settings/extendWidgetForm'; @@ -373,6 +374,7 @@ export class GridSettingsFormFactory { }), }), }), + filter: createFilterGroup(get(value, 'filter', null)), }); return form; }; @@ -497,6 +499,7 @@ export class GridSettingsFormFactory { }, { validator: this.actionValidator } ), + filter: createFilterGroup(get(value, 'filter', null)), }); // Set up mutual exclusivity diff --git a/libs/shared/src/lib/components/widgets/grid/action-button.type.ts b/libs/shared/src/lib/components/widgets/grid/action-button.type.ts index 0877ebf330..317b8bf99d 100644 --- a/libs/shared/src/lib/components/widgets/grid/action-button.type.ts +++ b/libs/shared/src/lib/components/widgets/grid/action-button.type.ts @@ -36,4 +36,6 @@ export type ActionButton = { }; }; }; + // Visibility filter + filter: any; };