N°8641 - Dashboard editor front-end first commit for Form SDK integration.

* No dashlet edition
* Dashboard are not persisted
* Unable to load a dashboard from an endpoint (refresh)
* Grid library need proper npm integration
This commit is contained in:
Stephen Abello
2026-01-06 15:23:51 +01:00
parent 3e879c64a7
commit a713e1b56e
167 changed files with 32266 additions and 763 deletions

95
node_modules/gridstack/dist/angular/src/base-widget.ts generated vendored Normal file
View File

@@ -0,0 +1,95 @@
/**
* gridstack-item.component.ts 12.3.3
* Copyright (c) 2022-2024 Alain Dumesny - see GridStack root license
*/
/**
* Abstract base class that all custom widgets should extend.
*
* This class provides the interface needed for GridstackItemComponent to:
* - Serialize/deserialize widget data
* - Save/restore widget state
* - Integrate with Angular lifecycle
*
* Extend this class when creating custom widgets for dynamic grids.
*
* @example
* ```typescript
* @Component({
* selector: 'my-custom-widget',
* template: '<div>{{data}}</div>'
* })
* export class MyCustomWidget extends BaseWidget {
* @Input() data: string = '';
*
* serialize() {
* return { data: this.data };
* }
* }
* ```
*/
import { Injectable } from '@angular/core';
import { NgCompInputs, NgGridStackWidget } from './types';
/**
* Base widget class for GridStack Angular integration.
*/
@Injectable()
export abstract class BaseWidget {
/**
* Complete widget definition including position, size, and Angular-specific data.
* Populated automatically when the widget is loaded or saved.
*/
public widgetItem?: NgGridStackWidget;
/**
* Override this method to return serializable data for this widget.
*
* Return an object with properties that map to your component's @Input() fields.
* The selector is handled automatically, so only include component-specific data.
*
* @returns Object containing serializable component data
*
* @example
* ```typescript
* serialize() {
* return {
* title: this.title,
* value: this.value,
* settings: this.settings
* };
* }
* ```
*/
public serialize(): NgCompInputs | undefined { return; }
/**
* Override this method to handle widget restoration from saved data.
*
* Use this for complex initialization that goes beyond simple @Input() mapping.
* The default implementation automatically assigns input data to component properties.
*
* @param w The saved widget data including input properties
*
* @example
* ```typescript
* deserialize(w: NgGridStackWidget) {
* super.deserialize(w); // Call parent for basic setup
*
* // Custom initialization logic
* if (w.input?.complexData) {
* this.processComplexData(w.input.complexData);
* }
* }
* ```
*/
public deserialize(w: NgGridStackWidget) {
// save full description for meta data
this.widgetItem = w;
if (!w) return;
if (w.input) Object.assign(this, w.input);
}
}

View File

@@ -0,0 +1,125 @@
/**
* gridstack-item.component.ts 12.3.3
* Copyright (c) 2022-2024 Alain Dumesny - see GridStack root license
*/
import { Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy, ComponentRef } from '@angular/core';
import { GridItemHTMLElement, GridStackNode } from 'gridstack';
import { BaseWidget } from './base-widget';
/**
* Extended HTMLElement interface for grid items.
* Stores a back-reference to the Angular component for integration.
*/
export interface GridItemCompHTMLElement extends GridItemHTMLElement {
/** Back-reference to the Angular GridStackItem component */
_gridItemComp?: GridstackItemComponent;
}
/**
* Angular component wrapper for individual GridStack items.
*
* This component represents a single grid item and handles:
* - Dynamic content creation and management
* - Integration with parent GridStack component
* - Component lifecycle and cleanup
* - Widget options and configuration
*
* Use in combination with GridstackComponent for the parent grid.
*
* @example
* ```html
* <gridstack>
* <gridstack-item [options]="{x: 0, y: 0, w: 2, h: 1}">
* <my-widget-component></my-widget-component>
* </gridstack-item>
* </gridstack>
* ```
*/
@Component({
selector: 'gridstack-item',
template: `
<div class="grid-stack-item-content">
<!-- where dynamic items go based on component selector (recommended way), or sub-grids, etc...) -->
<ng-template #container></ng-template>
<!-- any static (defined in DOM - not recommended) content goes here -->
<ng-content></ng-content>
<!-- fallback HTML content from GridStackWidget.content if used instead (not recommended) -->
{{options.content}}
</div>`,
styles: [`
:host { display: block; }
`],
standalone: true,
// changeDetection: ChangeDetectionStrategy.OnPush, // IFF you want to optimize and control when ChangeDetection needs to happen...
})
export class GridstackItemComponent implements OnDestroy {
/**
* Container for dynamic component creation within this grid item.
* Used to append child components programmatically.
*/
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
/**
* Component reference for dynamic component removal.
* Used internally when this component is created dynamically.
*/
public ref: ComponentRef<GridstackItemComponent> | undefined;
/**
* Reference to child widget component for serialization.
* Used to save/restore additional data along with grid position.
*/
public childWidget: BaseWidget | undefined;
/**
* Grid item configuration options.
* Defines position, size, and behavior of this grid item.
*
* @example
* ```typescript
* itemOptions: GridStackNode = {
* x: 0, y: 0, w: 2, h: 1,
* noResize: true,
* content: 'Item content'
* };
* ```
*/
@Input() public set options(val: GridStackNode) {
const grid = this.el.gridstackNode?.grid;
if (grid) {
// already built, do an update...
grid.update(this.el, val);
} else {
// store our custom element in options so we can update it and not re-create a generic div!
this._options = {...val, el: this.el};
}
}
/** return the latest grid options (from GS once built, otherwise initial values) */
public get options(): GridStackNode {
return this.el.gridstackNode || this._options || {el: this.el};
}
protected _options?: GridStackNode;
/** return the native element that contains grid specific fields as well */
public get el(): GridItemCompHTMLElement { return this.elementRef.nativeElement; }
/** clears the initial options now that we've built */
public clearOptions() {
delete this._options;
}
constructor(protected readonly elementRef: ElementRef<GridItemCompHTMLElement>) {
this.el._gridItemComp = this;
}
public ngOnDestroy(): void {
this.clearOptions();
delete this.childWidget
delete this.el._gridItemComp;
delete this.container;
delete this.ref;
}
}

View File

@@ -0,0 +1,460 @@
/**
* gridstack.component.ts 12.3.3
* Copyright (c) 2022-2024 Alain Dumesny - see GridStack root license
*/
import {
AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, Input,
OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, reflectComponentType, ComponentRef
} from '@angular/core';
import { NgIf } from '@angular/common';
import { Subscription } from 'rxjs';
import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
import { NgGridStackNode, NgGridStackWidget } from './types';
import { BaseWidget } from './base-widget';
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
/**
* Event handler callback signatures for different GridStack events.
* These types define the structure of data passed to Angular event emitters.
*/
/** Callback for general events (enable, disable, etc.) */
export type eventCB = {event: Event};
/** Callback for element-specific events (resize, drag, etc.) */
export type elementCB = {event: Event, el: GridItemHTMLElement};
/** Callback for events affecting multiple nodes (change, etc.) */
export type nodesCB = {event: Event, nodes: GridStackNode[]};
/** Callback for drop events with before/after node state */
export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: GridStackNode};
/**
* Extended HTMLElement interface for the grid container.
* Stores a back-reference to the Angular component for integration purposes.
*/
export interface GridCompHTMLElement extends GridHTMLElement {
/** Back-reference to the Angular GridStack component */
_gridComp?: GridstackComponent;
}
/**
* Mapping of selector strings to Angular component types.
* Used for dynamic component creation based on widget selectors.
*/
export type SelectorToType = {[key: string]: Type<Object>};
/**
* Angular component wrapper for GridStack.
*
* This component provides Angular integration for GridStack grids, handling:
* - Grid initialization and lifecycle
* - Dynamic component creation and management
* - Event binding and emission
* - Integration with Angular change detection
*
* Use in combination with GridstackItemComponent for individual grid items.
*
* @example
* ```html
* <gridstack [options]="gridOptions" (change)="onGridChange($event)">
* <div empty-content>Drag widgets here</div>
* </gridstack>
* ```
*/
@Component({
selector: 'gridstack',
template: `
<!-- content to show when when grid is empty, like instructions on how to add widgets -->
<ng-content select="[empty-content]" *ngIf="isEmpty"></ng-content>
<!-- where dynamic items go -->
<ng-template #container></ng-template>
<!-- where template items go -->
<ng-content></ng-content>
`,
styles: [`
:host { display: block; }
`],
standalone: true,
imports: [NgIf]
// changeDetection: ChangeDetectionStrategy.OnPush, // IFF you want to optimize and control when ChangeDetection needs to happen...
})
export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
/**
* List of template-based grid items (not recommended approach).
* Used to sync between DOM and GridStack internals when items are defined in templates.
* Prefer dynamic component creation instead.
*/
@ContentChildren(GridstackItemComponent) public gridstackItems?: QueryList<GridstackItemComponent>;
/**
* Container for dynamic component creation (recommended approach).
* Used to append grid items programmatically at runtime.
*/
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
/**
* Grid configuration options.
* Can be set before grid initialization or updated after grid is created.
*
* @example
* ```typescript
* gridOptions: GridStackOptions = {
* column: 12,
* cellHeight: 'auto',
* animate: true
* };
* ```
*/
@Input() public set options(o: GridStackOptions) {
if (this._grid) {
this._grid.updateOptions(o);
} else {
this._options = o;
}
}
/** Get the current running grid options */
public get options(): GridStackOptions { return this._grid?.opts || this._options || {}; }
/**
* Controls whether empty content should be displayed.
* Set to true to show ng-content with 'empty-content' selector when grid has no items.
*
* @example
* ```html
* <gridstack [isEmpty]="gridItems.length === 0">
* <div empty-content>Drag widgets here to get started</div>
* </gridstack>
* ```
*/
@Input() public isEmpty?: boolean;
/**
* GridStack event emitters for Angular integration.
*
* These provide Angular-style event handling for GridStack events.
* Alternatively, use `this.grid.on('event1 event2', callback)` for multiple events.
*
* Note: 'CB' suffix prevents conflicts with native DOM events.
*
* @example
* ```html
* <gridstack (changeCB)="onGridChange($event)" (droppedCB)="onItemDropped($event)">
* </gridstack>
* ```
*/
/** Emitted when widgets are added to the grid */
@Output() public addedCB = new EventEmitter<nodesCB>();
/** Emitted when grid layout changes */
@Output() public changeCB = new EventEmitter<nodesCB>();
/** Emitted when grid is disabled */
@Output() public disableCB = new EventEmitter<eventCB>();
/** Emitted during widget drag operations */
@Output() public dragCB = new EventEmitter<elementCB>();
/** Emitted when widget drag starts */
@Output() public dragStartCB = new EventEmitter<elementCB>();
/** Emitted when widget drag stops */
@Output() public dragStopCB = new EventEmitter<elementCB>();
/** Emitted when widget is dropped */
@Output() public droppedCB = new EventEmitter<droppedCB>();
/** Emitted when grid is enabled */
@Output() public enableCB = new EventEmitter<eventCB>();
/** Emitted when widgets are removed from the grid */
@Output() public removedCB = new EventEmitter<nodesCB>();
/** Emitted during widget resize operations */
@Output() public resizeCB = new EventEmitter<elementCB>();
/** Emitted when widget resize starts */
@Output() public resizeStartCB = new EventEmitter<elementCB>();
/** Emitted when widget resize stops */
@Output() public resizeStopCB = new EventEmitter<elementCB>();
/**
* Get the native DOM element that contains grid-specific fields.
* This element has GridStack properties attached to it.
*/
public get el(): GridCompHTMLElement { return this.elementRef.nativeElement; }
/**
* Get the underlying GridStack instance.
* Use this to access GridStack API methods directly.
*
* @example
* ```typescript
* this.gridComponent.grid.addWidget({x: 0, y: 0, w: 2, h: 1});
* ```
*/
public get grid(): GridStack | undefined { return this._grid; }
/**
* Component reference for dynamic component removal.
* Used internally when this component is created dynamically.
*/
public ref: ComponentRef<GridstackComponent> | undefined;
/**
* Mapping of component selectors to their types for dynamic creation.
*
* This enables dynamic component instantiation from string selectors.
* Angular doesn't provide public access to this mapping, so we maintain our own.
*
* @example
* ```typescript
* GridstackComponent.addComponentToSelectorType([MyWidgetComponent]);
* ```
*/
public static selectorToType: SelectorToType = {};
/**
* Register a list of Angular components for dynamic creation.
*
* @param typeList Array of component types to register
*
* @example
* ```typescript
* GridstackComponent.addComponentToSelectorType([
* MyWidgetComponent,
* AnotherWidgetComponent
* ]);
* ```
*/
public static addComponentToSelectorType(typeList: Array<Type<Object>>) {
typeList.forEach(type => GridstackComponent.selectorToType[ GridstackComponent.getSelector(type) ] = type);
}
/**
* Extract the selector string from an Angular component type.
*
* @param type The component type to get selector from
* @returns The component's selector string
*/
public static getSelector(type: Type<Object>): string {
return reflectComponentType(type)!.selector;
}
protected _options?: GridStackOptions;
protected _grid?: GridStack;
protected _sub: Subscription | undefined;
protected loaded?: boolean;
constructor(protected readonly elementRef: ElementRef<GridCompHTMLElement>) {
// set globally our method to create the right widget type
if (!GridStack.addRemoveCB) {
GridStack.addRemoveCB = gsCreateNgComponents;
}
if (!GridStack.saveCB) {
GridStack.saveCB = gsSaveAdditionalNgInfo;
}
if (!GridStack.updateCB) {
GridStack.updateCB = gsUpdateNgComponents;
}
this.el._gridComp = this;
}
public ngOnInit(): void {
// init ourself before any template children are created since we track them below anyway - no need to double create+update widgets
this.loaded = !!this.options?.children?.length;
this._grid = GridStack.init(this._options, this.el);
delete this._options; // GS has it now
this.checkEmpty();
}
/** wait until after all DOM is ready to init gridstack children (after angular ngFor and sub-components run first) */
public ngAfterContentInit(): void {
// track whenever the children list changes and update the layout...
this._sub = this.gridstackItems?.changes.subscribe(() => this.updateAll());
// ...and do this once at least unless we loaded children already
if (!this.loaded) this.updateAll();
this.hookEvents(this.grid);
}
public ngOnDestroy(): void {
this.unhookEvents(this._grid);
this._sub?.unsubscribe();
this._grid?.destroy();
delete this._grid;
delete this.el._gridComp;
delete this.container;
delete this.ref;
}
/**
* called when the TEMPLATE (not recommended) list of items changes - get a list of nodes and
* update the layout accordingly (which will take care of adding/removing items changed by Angular)
*/
public updateAll() {
if (!this.grid) return;
const layout: GridStackWidget[] = [];
this.gridstackItems?.forEach(item => {
layout.push(item.options);
item.clearOptions();
});
this.grid.load(layout); // efficient that does diffs only
}
/** check if the grid is empty, if so show alternative content */
public checkEmpty() {
if (!this.grid) return;
this.isEmpty = !this.grid.engine.nodes.length;
}
/** get all known events as easy to use Outputs for convenience */
protected hookEvents(grid?: GridStack) {
if (!grid) return;
// nested grids don't have events in v12.1+ so skip
if (grid.parentGridNode) return;
grid
.on('added', (event: Event, nodes: GridStackNode[]) => {
const gridComp = (nodes[0].grid?.el as GridCompHTMLElement)._gridComp || this;
gridComp.checkEmpty();
this.addedCB.emit({event, nodes});
})
.on('change', (event: Event, nodes: GridStackNode[]) => this.changeCB.emit({event, nodes}))
.on('disable', (event: Event) => this.disableCB.emit({event}))
.on('drag', (event: Event, el: GridItemHTMLElement) => this.dragCB.emit({event, el}))
.on('dragstart', (event: Event, el: GridItemHTMLElement) => this.dragStartCB.emit({event, el}))
.on('dragstop', (event: Event, el: GridItemHTMLElement) => this.dragStopCB.emit({event, el}))
.on('dropped', (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => this.droppedCB.emit({event, previousNode, newNode}))
.on('enable', (event: Event) => this.enableCB.emit({event}))
.on('removed', (event: Event, nodes: GridStackNode[]) => {
const gridComp = (nodes[0].grid?.el as GridCompHTMLElement)._gridComp || this;
gridComp.checkEmpty();
this.removedCB.emit({event, nodes});
})
.on('resize', (event: Event, el: GridItemHTMLElement) => this.resizeCB.emit({event, el}))
.on('resizestart', (event: Event, el: GridItemHTMLElement) => this.resizeStartCB.emit({event, el}))
.on('resizestop', (event: Event, el: GridItemHTMLElement) => this.resizeStopCB.emit({event, el}))
}
protected unhookEvents(grid?: GridStack) {
if (!grid) return;
// nested grids don't have events in v12.1+ so skip
if (grid.parentGridNode) return;
grid.off('added change disable drag dragstart dragstop dropped enable removed resize resizestart resizestop');
}
}
/**
* can be used when a new item needs to be created, which we do as a Angular component, or deleted (skip)
**/
export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, n: NgGridStackNode, add: boolean, isGrid: boolean): HTMLElement | undefined {
if (add) {
//
// create the component dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
//
if (!host) return;
if (isGrid) {
// TODO: figure out how to create ng component inside regular Div. need to access app injectors...
// if (!container) {
// const hostElement: Element = host;
// const environmentInjector: EnvironmentInjector;
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
// }
const gridItemComp = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp;
if (!gridItemComp) return;
// check if gridItem has a child component with 'container' exposed to create under..
const container = (gridItemComp.childWidget as any)?.container || gridItemComp.container;
const gridRef = container?.createComponent(GridstackComponent);
const grid = gridRef?.instance;
if (!grid) return;
grid.ref = gridRef;
grid.options = n;
return grid.el;
} else {
const gridComp = (host as GridCompHTMLElement)._gridComp;
const gridItemRef = gridComp?.container?.createComponent(GridstackItemComponent);
const gridItem = gridItemRef?.instance;
if (!gridItem) return;
gridItem.ref = gridItemRef
// define what type of component to create as child, OR you can do it GridstackItemComponent template, but this is more generic
const selector = n.selector;
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
if (type) {
// shared code to create our selector component
const createComp = () => {
const childWidget = gridItem.container?.createComponent(type)?.instance as BaseWidget;
// if proper BaseWidget subclass, save it and load additional data
if (childWidget && typeof childWidget.serialize === 'function' && typeof childWidget.deserialize === 'function') {
gridItem.childWidget = childWidget;
childWidget.deserialize(n);
}
}
const lazyLoad = n.lazyLoad || n.grid?.opts?.lazyLoad && n.lazyLoad !== false;
if (lazyLoad) {
if (!n.visibleObservable) {
n.visibleObservable = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) {
n.visibleObservable?.disconnect();
delete n.visibleObservable;
createComp();
}});
window.setTimeout(() => n.visibleObservable?.observe(gridItem.el)); // wait until callee sets position attributes
}
} else createComp();
}
return gridItem.el;
}
} else {
//
// REMOVE - have to call ComponentRef:destroy() for dynamic objects to correctly remove themselves
// Note: this will destroy all children dynamic components as well: gridItem -> childWidget
//
if (isGrid) {
const grid = (n.el as GridCompHTMLElement)?._gridComp;
if (grid?.ref) grid.ref.destroy();
else grid?.ngOnDestroy();
} else {
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
if (gridItem?.ref) gridItem.ref.destroy();
else gridItem?.ngOnDestroy();
}
}
return;
}
/**
* called for each item in the grid - check if additional information needs to be saved.
* Note: since this is options minus gridstack protected members using Utils.removeInternalForSave(),
* this typically doesn't need to do anything. However your custom Component @Input() are now supported
* using BaseWidget.serialize()
*/
export function gsSaveAdditionalNgInfo(n: NgGridStackNode, w: NgGridStackWidget) {
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
if (gridItem) {
const input = gridItem.childWidget?.serialize();
if (input) {
w.input = input;
}
return;
}
// else check if Grid
const grid = (n.el as GridCompHTMLElement)?._gridComp;
if (grid) {
//.... save any custom data
}
}
/**
* track when widgeta re updated (rather than created) to make sure we de-serialize them as well
*/
export function gsUpdateNgComponents(n: NgGridStackNode) {
const w: NgGridStackWidget = n;
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
if (gridItem?.childWidget && w.input) gridItem.childWidget.deserialize(w);
}

View File

@@ -0,0 +1,44 @@
/**
* gridstack.component.ts 12.3.3
* Copyright (c) 2022-2024 Alain Dumesny - see GridStack root license
*/
import { NgModule } from "@angular/core";
import { GridstackItemComponent } from "./gridstack-item.component";
import { GridstackComponent } from "./gridstack.component";
/**
* @deprecated Use GridstackComponent and GridstackItemComponent as standalone components instead.
*
* This NgModule is provided for backward compatibility but is no longer the recommended approach.
* Import components directly in your standalone components or use the new Angular module structure.
*
* @example
* ```typescript
* // Preferred approach - standalone components
* @Component({
* selector: 'my-app',
* imports: [GridstackComponent, GridstackItemComponent],
* template: '<gridstack></gridstack>'
* })
* export class AppComponent {}
*
* // Legacy approach (deprecated)
* @NgModule({
* imports: [GridstackModule]
* })
* export class AppModule {}
* ```
*/
@NgModule({
imports: [
GridstackItemComponent,
GridstackComponent,
],
exports: [
GridstackItemComponent,
GridstackComponent,
],
})
export class GridstackModule {}

54
node_modules/gridstack/dist/angular/src/types.ts generated vendored Normal file
View File

@@ -0,0 +1,54 @@
/**
* gridstack-item.component.ts 12.3.3
* Copyright (c) 2025 Alain Dumesny - see GridStack root license
*/
import { GridStackNode, GridStackOptions, GridStackWidget } from "gridstack";
/**
* Extended GridStackWidget interface for Angular integration.
* Adds Angular-specific properties for dynamic component creation.
*/
export interface NgGridStackWidget extends GridStackWidget {
/** Angular component selector for dynamic creation (e.g., 'my-widget') */
selector?: string;
/** Serialized data for component @Input() properties */
input?: NgCompInputs;
/** Configuration for nested sub-grids */
subGridOpts?: NgGridStackOptions;
}
/**
* Extended GridStackNode interface for Angular integration.
* Adds component selector for dynamic content creation.
*/
export interface NgGridStackNode extends GridStackNode {
/** Angular component selector for this node's content */
selector?: string;
}
/**
* Extended GridStackOptions interface for Angular integration.
* Supports Angular-specific widget definitions and nested grids.
*/
export interface NgGridStackOptions extends GridStackOptions {
/** Array of Angular widget definitions for initial grid setup */
children?: NgGridStackWidget[];
/** Configuration for nested sub-grids (Angular-aware) */
subGridOpts?: NgGridStackOptions;
}
/**
* Type for component input data serialization.
* Maps @Input() property names to their values for widget persistence.
*
* @example
* ```typescript
* const inputs: NgCompInputs = {
* title: 'My Widget',
* value: 42,
* config: { enabled: true }
* };
* ```
*/
export type NgCompInputs = {[key: string]: any};