From 4082ff52fb7ffe1d1280947d22b8d1f6908e8c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Collin?= Date: Sat, 27 Jun 2026 10:46:24 +0200 Subject: [PATCH 01/14] features: carousel workflow --- .../src/lib/abstract/abstract-dc-workflow.ts | 66 +++++++++++++++- .../src/lib/plugin/xt-plugin-info.ts | 1 - .../lib/registry/xt-plugin-registry.spec.ts | 1 - .../src/lib/carousel/carousel.component.css | 63 +++++++++++++++ .../src/lib/carousel/carousel.component.html | 38 +++++++++ .../lib/carousel/carousel.component.spec.ts | 79 +++++++++++++++++++ .../src/lib/carousel/carousel.component.ts | 26 ++++++ .../list-details.component.spec.ts | 1 - .../list-details/list-details.component.ts | 39 +-------- 9 files changed, 273 insertions(+), 41 deletions(-) create mode 100644 plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.css create mode 100644 plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html create mode 100644 plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.spec.ts create mode 100644 plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.ts diff --git a/libs/dc-workflow/projects/dc-workflow/src/lib/abstract/abstract-dc-workflow.ts b/libs/dc-workflow/projects/dc-workflow/src/lib/abstract/abstract-dc-workflow.ts index a26740e3..e31510aa 100644 --- a/libs/dc-workflow/projects/dc-workflow/src/lib/abstract/abstract-dc-workflow.ts +++ b/libs/dc-workflow/projects/dc-workflow/src/lib/abstract/abstract-dc-workflow.ts @@ -1,7 +1,7 @@ import { DcWorkflow } from '../definition/dc-workflow'; -import { Component, computed, effect, inject, input } from '@angular/core'; +import { Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core'; import { DcWorkflowModel, DcWorkflowSortOption } from '../models/dc-workflow-model'; -import { XtCompositeComponent, XtResolverService } from 'xt-components'; +import { XtCompositeComponent, XtMessageHandler, XtResolverService } from 'xt-components'; import { XtSignalStore, XtSortBy, @@ -10,6 +10,7 @@ import { XtStoreManagerService } from 'xt-store'; import { ManagedData } from 'xt-type'; +import { FormGroup } from '@angular/forms'; /** * A workflow base class based on the xt-store signalStore @@ -28,9 +29,37 @@ export class AbstractDcWorkflow extends XtCom protected readonly resolver = inject (XtResolverService); protected readonly storeMgr = inject(XtStoreManagerService); + protected readonly errorHandler = inject(XtMessageHandler); + + /** + * The store that manages the access to the data + * @protected + */ protected store : XtSignalStore | null | undefined = undefined; + /** + * True whenever the store is updating something + */ + updating = signal (false); + + /** + * A shortcut to the entityname being managed + */ + entityName = linkedSignal( () => { + return this.config().entity; + }); + + /** + * A simple signal telling when a new store is being used. For example, when the entity handled has changed + * @protected + */ + protected storeChanged=signal(false); + + /** + * Triggered when the workflow config has been updated. Do we need another store, or just reconfigure the sort / filtering ? + * @protected + */ protected configUpdated = effect( async ()=> { const config = this.config(); const curStore = this.store; @@ -68,6 +97,7 @@ export class AbstractDcWorkflow extends XtCom * @protected */ protected displayableElements = computed(() => { + const enforceStoreChange = this.storeChanged(); // Recalculate in case the store has changed const config = this.config(); const entities = this.safeFindStore().entities(); const selectConfig = config.display; @@ -87,7 +117,37 @@ export class AbstractDcWorkflow extends XtCom return true; }); }); - /** + + /** + * Safely loads the data from the store. + */ + protected fetchFromStore () { + //console.debug("ListDetails fetchFromStore"); + const entityName = this.entityName(); + if (entityName!=null) { + try { + this.updating.set(true); + // If the entity is different, then we must change the whole store + if (this.store?.entityName()!=entityName) { + this.store = this.safeFindStore(); + this.storeChanged.update((oldVal)=> !oldVal); // Force update whenever the store changed + } + // console.debug("Store set to "+this.store.entityName()); + this.store.fetchEntities().catch((error) => { + this.errorHandler.errorOccurred(error, "Error loading entities "+entityName); + }).finally(() => { + this.updating.set(false); +// console.debug("Store fetched values ",this.store?.entities()); + });//.then(() => {console.debug('Yes')}).finally(() => {console.debug('Finish')}); + } catch (error) { + this.updating.set(false); + } + } else { + this.store = null; + } + } + + /** * Returns the element that should be selected given the config and the store content * @protected */ diff --git a/libs/xt-components/projects/xt-components/src/lib/plugin/xt-plugin-info.ts b/libs/xt-components/projects/xt-components/src/lib/plugin/xt-plugin-info.ts index 412111cf..2606985c 100644 --- a/libs/xt-components/projects/xt-components/src/lib/plugin/xt-plugin-info.ts +++ b/libs/xt-components/projects/xt-components/src/lib/plugin/xt-plugin-info.ts @@ -1,6 +1,5 @@ import { XtTypeInfo } from 'xt-type'; import { XtOutputType } from '../xt-component'; -import { XtActionHandler } from '../action/xt-action-handler'; import { XtWorkflow } from '../workflow/xt-workflow'; /** Information about a registered component plugin */ diff --git a/libs/xt-components/projects/xt-components/src/lib/registry/xt-plugin-registry.spec.ts b/libs/xt-components/projects/xt-components/src/lib/registry/xt-plugin-registry.spec.ts index fd27e77d..5bee9489 100644 --- a/libs/xt-components/projects/xt-components/src/lib/registry/xt-plugin-registry.spec.ts +++ b/libs/xt-components/projects/xt-components/src/lib/registry/xt-plugin-registry.spec.ts @@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { xtPluginRegistry } from '../../globals'; import { XtActionHandler, XtActionResult } from '../action/xt-action-handler'; import { XtContext } from '../xt-context'; -import { IStoreProvider } from '../store/store-support'; describe('XtPluginRegistry', () => { diff --git a/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.css b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.css new file mode 100644 index 00000000..e119852b --- /dev/null +++ b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.css @@ -0,0 +1,63 @@ +:host { + display: block; +} + +.carousel { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.carousel--empty { + padding-top: 0.5rem; +} + +:host-context(.app-dark) .carousel__panel, +:host-context(.app-dark) .carousel__state { + background: color-mix(in srgb, var(--p-surface-900) 94%, var(--p-primary-950) 6%); +} + +@media (max-width: 767px) { + + .carousel__state { + min-height: 14rem; + padding: 1.5rem; + } +} + +.carousel__panel { + border-radius: 1.5rem; + background: color-mix(in srgb, var(--p-surface-0) 92%, var(--p-primary-50) 8%); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 14px 40px rgb(0 0 0 / 0.06); +} + +.carousel__state { + min-height: 18rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.75rem; + text-align: center; + border-radius: 1.5rem; + padding: 2rem; + background: color-mix(in srgb, var(--p-surface-0) 94%, var(--p-primary-50) 6%); + color: var(--p-text-color); +} + +.carousel__state i { + font-size: 2rem; + opacity: 0.55; +} + +.carousel__state span { + font-weight: 600; +} + +.carousel__state small { + opacity: 0.7; +} + +.carousel__state--empty { + min-height: 16rem; +} diff --git a/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html new file mode 100644 index 00000000..b92967e8 --- /dev/null +++ b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html @@ -0,0 +1,38 @@ +@let entName = entityName(); + +@if (entName != null) { + + @if (store?.loading()) { + + } @else if ((store?.entities()?.length ?? 0) === 0) { + + } @else { + + + + + + + } + +} @else { + +} diff --git a/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.spec.ts b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.spec.ts new file mode 100644 index 00000000..197afacc --- /dev/null +++ b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CarouselComponent } from './carousel.component'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { registerDefaultPlugin } from 'xt-plugin-default'; +import { StoreTestBed } from 'xt-store'; +import { XtResolverService, XtUnitTestHelper } from 'xt-components'; +import { DcWorkflowModel } from 'dc-workflow'; +import { By } from '@angular/platform-browser'; +import { Carousel } from 'primeng/carousel'; + +describe('Carousel Component', () => { + let component: CarouselComponent; + let fixture: ComponentFixture>; + let storeTestBed: StoreTestBed; + let resolver: XtResolverService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CarouselComponent], + }).compileComponents(); + + registerDefaultPlugin(TestBed.inject(XtResolverService)); + storeTestBed = new StoreTestBed(); + StoreTestBed.ensureMemoryProviderOnly(); + + }); + + it('should create', async () => { + fixture = TestBed.createComponent(CarouselComponent); + fixture.componentRef.setInput('config',{}); + component = fixture.componentInstance; + await fixture.whenStable(); + expect(component).toBeTruthy(); + }); + + it ('should display carousel', async () => { + + await storeTestBed.defineTestDataFor('CarouselTest', [{ + name: 'Test Book', + published: new Date(1970,10, 5) + }, { + name: 'Another Book', + published: new Date(2010,7, 15) + } + ]); + + fixture = TestBed.createComponent(CarouselComponent); + fixture.componentRef.setInput ('config', { + entity: 'CarouselTest', + workflow: 'carousel', + data: { + sort: { + 'published':'descending' + } + }, + selection: { + field: { + key: 'published', + type: 'closest-before' + } + } + } as DcWorkflowModel); + + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + await XtUnitTestHelper.waitFor(() => { + const carouselComponent = fixture.debugElement.query(By.directive(Carousel)); + return carouselComponent != null; + }); + + let carouselComponent = fixture.debugElement.query(By.directive(Carousel)); + expect(carouselComponent).toBeTruthy(); + + expect(carouselComponent.nativeElement.textContent?.indexOf('Another Book')).not.toBe(-1); + }); +}); diff --git a/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.ts b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.ts new file mode 100644 index 00000000..4f58a64e --- /dev/null +++ b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.ts @@ -0,0 +1,26 @@ +import { Component, inject, linkedSignal, OnInit } from '@angular/core'; +import { AbstractDcWorkflow } from 'dc-workflow'; +import { XtMessageHandler, XtRenderComponent } from 'xt-components'; +import { ManagedData } from 'xt-type'; +import { Carousel } from 'primeng/carousel'; +import { ProgressSpinner } from 'primeng/progressspinner'; + +/** + * A workflow displaying the list through a carousel. It automatically selects the first element to display + */ +@Component({ + selector: 'wfw-carousel', + imports: [ + Carousel, + ProgressSpinner, XtRenderComponent + ], + templateUrl: './carousel.component.html', + styleUrl: './carousel.component.css', +}) +export class CarouselComponent extends AbstractDcWorkflow implements OnInit { + + override ngOnInit() { + super.ngOnInit(); + this.fetchFromStore(); + } +} diff --git a/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.spec.ts b/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.spec.ts index 6ea726b9..8fabc0fa 100644 --- a/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.spec.ts +++ b/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.spec.ts @@ -10,7 +10,6 @@ import { By } from '@angular/platform-browser'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { MessageService } from 'primeng/api'; import { DcWorkflowModel } from 'dc-workflow'; -import { RouterTestingHarness } from '@angular/router/testing'; import { FormGroup } from '@angular/forms'; import { Table } from 'primeng/table'; diff --git a/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.ts b/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.ts index df00ef21..afde5d42 100644 --- a/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.ts +++ b/plugins/xt-workflow/projects/workflow/src/lib/list-details/list-details.component.ts @@ -30,11 +30,6 @@ import { AbstractDcWorkflow } from 'dc-workflow'; }) export class ListDetailsComponent extends AbstractDcWorkflow implements OnInit, OnDestroy { protected readonly formBuilder = inject(FormBuilder); - protected readonly errorHandler = inject(XtMessageHandler); - - entityName = linkedSignal( () => { - return this.config().entity; - }); editForm = signal(this.formBuilder.group ({ editor: this.formBuilder.group({}) })); @@ -64,12 +59,12 @@ export class ListDetailsComponent extends AbstractDcWorkf formValue = signal(null); - protected storeChanged=signal(false); - + /** + * Display in the list the selected item being edited if any + */ displayedEntities = computed(() => { // console.debug("ListDetails init Displayed Entities"); - const storeChanged=this.storeChanged(); // Enforce recalculation whenever the store is changed - const entities = this.store?.entities() ?? []; + const entities = this.displayableElements(); const value = this.formValue(); const selected = this.selectedEntity(); if (value != null && selected != null && (selected as any)._id != null) { @@ -89,7 +84,6 @@ export class ListDetailsComponent extends AbstractDcWorkf saving = signal (false); newing = signal (false); deleting = signal (false); - updating = signal (false); selectedEntity= model(); @@ -119,31 +113,6 @@ export class ListDetailsComponent extends AbstractDcWorkf this.fetchFromStore(); } - fetchFromStore () { - //console.debug("ListDetails fetchFromStore"); - const entityName = this.entityName(); - if (entityName!=null) { - try { - this.updating.set(true); - // If the entity is different, then we must change the whole store - if (this.store?.entityName()!=entityName) { - this.store = this.safeFindStore(); - this.storeChanged.update((oldVal)=> !oldVal); // Force update whenever the store changed - } - // console.debug("Store set to "+this.store.entityName()); - this.store.fetchEntities().catch((error) => { - this.errorHandler.errorOccurred(error, "Error loading entities "+entityName); - }).finally(() => { - this.updating.set(false); -// console.debug("Store fetched values ",this.store?.entities()); - });//.then(() => {console.debug('Yes')}).finally(() => {console.debug('Finish')}); - } catch (error) { - this.updating.set(false); - } - } else { - this.store = null; - } - } updateEditForm () { const entity = this.selectedEntity(); From f88db6781ee39115ed349cf9e2d6d86c4ef8e10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Collin?= Date: Sun, 28 Jun 2026 09:13:54 +0200 Subject: [PATCH 02/14] features: carousel workflow --- .../test-workflow/test-workflow.component.html | 8 +++++++- .../app/test-workflow/test-workflow.component.ts | 15 +++++++++++++-- .../src/lib/carousel/carousel.component.html | 2 +- .../projects/workflow/src/lib/register.ts | 7 ++++++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.html b/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.html index 9127ae8b..bdd0847b 100644 --- a/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.html +++ b/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.html @@ -1,4 +1,9 @@ -Testing the default List Details workflow +Testing the workflows + +
+ Select the workflow type: + +
Select the default sort: @@ -7,6 +12,7 @@
+ @if (dataLoaded()) { } diff --git a/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.ts b/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.ts index a6103ad6..690609e9 100644 --- a/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.ts +++ b/plugins/xt-workflow/projects/workflow-plugin/src/app/test-workflow/test-workflow.component.ts @@ -6,6 +6,7 @@ import { ListDetailsComponent } from '../../../../workflow/src/lib/list-details/ import { Button } from 'primeng/button'; import { DcWorkflowModel, WfwRender } from 'dc-workflow'; import { StoreTestBed } from 'xt-store'; +import { Select, SelectChangeEvent } from 'primeng/select'; @Component({ selector: 'app-test', @@ -13,7 +14,8 @@ import { StoreTestBed } from 'xt-store'; FormsModule, ReactiveFormsModule, Button, - WfwRender + WfwRender, + Select ], templateUrl: './test-workflow.component.html', styleUrl: './test-workflow.component.css' @@ -58,7 +60,7 @@ export class TestWorkflowComponent implements OnInit, OnDestroy { protected workflowConfig = computed(() => ({ entity:'test', - workflow:'list-detail', + workflow:this.workflowType() as 'list-detail'|'carousel', data: { sort: { [this.sortField()]: this.sortDir() @@ -66,6 +68,8 @@ export class TestWorkflowComponent implements OnInit, OnDestroy { } })); + protected workflowType = signal('list-detail'); + protected toggleSort(field: string) { if (this.sortField() === field) { this.sortDir.update(d => d === 'ascending' ? 'descending' : 'ascending'); @@ -79,4 +83,11 @@ export class TestWorkflowComponent implements OnInit, OnDestroy { this.subscriptions.unsubscribe(); } + protected setWorkflowType(value: string) { + this.workflowType.set(value); + } + + protected listWorkflowTypes():string[] { + return ['list-detail','carousel'] + } } diff --git a/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html index b92967e8..db6de6d9 100644 --- a/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html +++ b/plugins/xt-workflow/projects/workflow/src/lib/carousel/carousel.component.html @@ -15,7 +15,7 @@ } @else { - + } @else { - + } @else { - + - - +
+ List View Selected element is {{selectedModel() | json}} - +
diff --git a/plugins/xt-default/projects/default/src/lib/object-set/carousel-object-set.component.css b/plugins/xt-default/projects/default/src/lib/object-set/carousel-object-set.component.css index 06cafa46..0a168aab 100644 --- a/plugins/xt-default/projects/default/src/lib/object-set/carousel-object-set.component.css +++ b/plugins/xt-default/projects/default/src/lib/object-set/carousel-object-set.component.css @@ -1,6 +1,8 @@ :host { display: block; margin: 0 auto; + max-width: 100vw; + } :host ::ng-deep .p-carousel-viewport { @@ -8,21 +10,6 @@ padding: 1.5rem 0.5rem; } -:host ::ng-deep .p-carousel-item { - display: flex; - flex-direction: column; - min-width: 0; - padding: 0 0.5rem; - box-sizing: border-box; -} - -:host ::ng-deep .p-carousel-item:not(.p-carousel-item-active) { - flex: 0 0 0 !important; - min-width: 0 !important; - overflow: hidden !important; - padding: 0 !important; -} - .carousel-object-set__panel { cursor: pointer; border: 2px solid transparent;