@dragonworks/ngx-dashboard 20.0.4 → 20.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ng-package.json +7 -0
- package/package.json +34 -45
- package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
- package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
- package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
- package/src/lib/cell/cell-context-menu.component.ts +138 -0
- package/src/lib/cell/cell-context-menu.service.ts +36 -0
- package/src/lib/cell/cell.component.html +37 -0
- package/src/lib/cell/cell.component.scss +198 -0
- package/src/lib/cell/cell.component.ts +375 -0
- package/src/lib/dashboard/dashboard.component.html +18 -0
- package/src/lib/dashboard/dashboard.component.scss +17 -0
- package/src/lib/dashboard/dashboard.component.ts +187 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
- package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
- package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
- package/src/lib/drop-zone/drop-zone.component.html +20 -0
- package/src/lib/drop-zone/drop-zone.component.scss +67 -0
- package/src/lib/drop-zone/drop-zone.component.ts +122 -0
- package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
- package/src/lib/models/cell-data.ts +13 -0
- package/src/lib/models/cell-dialog.ts +7 -0
- package/src/lib/models/cell-id.ts +85 -0
- package/src/lib/models/cell-position.ts +15 -0
- package/src/lib/models/dashboard-data.dto.ts +44 -0
- package/src/lib/models/dashboard-data.utils.ts +49 -0
- package/src/lib/models/drag-data.ts +6 -0
- package/src/lib/models/index.ts +11 -0
- package/src/lib/models/reserved-space.ts +24 -0
- package/src/lib/models/widget-factory.ts +33 -0
- package/src/lib/models/widget-id.ts +70 -0
- package/src/lib/models/widget.ts +21 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
- package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
- package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
- package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
- package/src/lib/services/dashboard-bridge.service.ts +155 -0
- package/src/lib/services/dashboard-viewport.service.ts +148 -0
- package/src/lib/services/dashboard.service.ts +62 -0
- package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
- package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
- package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
- package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
- package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
- package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
- package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
- package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
- package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
- package/src/lib/store/dashboard-store.ts +239 -0
- package/src/lib/store/features/drag-drop.feature.ts +140 -0
- package/src/lib/store/features/grid-config.feature.ts +43 -0
- package/src/lib/store/features/resize.feature.ts +140 -0
- package/src/lib/store/features/utils/collision.utils.ts +89 -0
- package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
- package/src/lib/store/features/utils/resize.utils.ts +165 -0
- package/src/lib/store/features/widget-management.feature.ts +158 -0
- package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
- package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
- package/src/lib/widget-list/widget-list.component.html +22 -0
- package/src/lib/widget-list/widget-list.component.scss +154 -0
- package/src/lib/widget-list/widget-list.component.ts +106 -0
- package/src/public-api.ts +21 -0
- package/src/test-setup.ts +10 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +14 -0
- package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2192
- package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
- package/index.d.ts +0 -678
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// dashboard-bridge.service.ts
|
|
2
|
+
import { Injectable, computed, signal } from '@angular/core';
|
|
3
|
+
import { DashboardStore } from '../store/dashboard-store';
|
|
4
|
+
import { DragData } from '../models';
|
|
5
|
+
|
|
6
|
+
interface DashboardInstance {
|
|
7
|
+
store: InstanceType<typeof DashboardStore>;
|
|
8
|
+
dimensions: () => { width: number; height: number };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal bridge service that coordinates between component-scoped DashboardStore instances
|
|
13
|
+
* and standalone components like WidgetListComponent.
|
|
14
|
+
*
|
|
15
|
+
* This service is NOT part of the public API and should remain internal to the library.
|
|
16
|
+
*/
|
|
17
|
+
@Injectable({ providedIn: 'root' })
|
|
18
|
+
export class DashboardBridgeService {
|
|
19
|
+
// Map of registered dashboard instances with their reactive dimensions
|
|
20
|
+
private readonly dashboards = signal(new Map<string, DashboardInstance>());
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a dashboard store instance using its dashboard ID
|
|
24
|
+
*/
|
|
25
|
+
registerDashboard(store: InstanceType<typeof DashboardStore>): void {
|
|
26
|
+
const dashboardId = store.dashboardId();
|
|
27
|
+
|
|
28
|
+
// If dashboard ID is not set yet, we'll register later when it's available
|
|
29
|
+
if (!dashboardId) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.dashboards.update(dashboards => {
|
|
34
|
+
const newMap = new Map(dashboards);
|
|
35
|
+
newMap.set(dashboardId, {
|
|
36
|
+
store,
|
|
37
|
+
dimensions: store.gridCellDimensions
|
|
38
|
+
});
|
|
39
|
+
return newMap;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unregister a dashboard store instance
|
|
45
|
+
*/
|
|
46
|
+
unregisterDashboard(store: InstanceType<typeof DashboardStore>): void {
|
|
47
|
+
const dashboardId = store.dashboardId();
|
|
48
|
+
if (!dashboardId) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.dashboards.update(dashboards => {
|
|
53
|
+
const newMap = new Map(dashboards);
|
|
54
|
+
newMap.delete(dashboardId);
|
|
55
|
+
return newMap;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get cell dimensions for a specific dashboard instance
|
|
61
|
+
*/
|
|
62
|
+
getDashboardDimensions(dashboardId: string): { width: number; height: number } {
|
|
63
|
+
const dashboard = this.dashboards().get(dashboardId);
|
|
64
|
+
return dashboard?.dimensions() || { width: 100, height: 100 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get all available dashboard dimensions (for widget lists to choose from)
|
|
69
|
+
* Returns the first available dashboard's dimensions as fallback
|
|
70
|
+
*/
|
|
71
|
+
readonly availableDimensions = computed(() => {
|
|
72
|
+
const dashboardEntries = Array.from(this.dashboards().values());
|
|
73
|
+
if (dashboardEntries.length === 0) {
|
|
74
|
+
return { width: 100, height: 100 }; // fallback
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Return dimensions from first available dashboard with fallback for undefined
|
|
78
|
+
return dashboardEntries[0].dimensions() || { width: 100, height: 100 };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Start drag operation on the first available dashboard
|
|
83
|
+
* (Widget lists need some dashboard to coordinate with during drag)
|
|
84
|
+
*/
|
|
85
|
+
startDrag(dragData: DragData): void {
|
|
86
|
+
const dashboardEntries = Array.from(this.dashboards().values());
|
|
87
|
+
if (dashboardEntries.length > 0) {
|
|
88
|
+
dashboardEntries[0].store.startDrag(dragData);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* End drag operation on all dashboards
|
|
94
|
+
* (Safer to notify all in case drag state got distributed)
|
|
95
|
+
*/
|
|
96
|
+
endDrag(): void {
|
|
97
|
+
for (const dashboard of this.dashboards().values()) {
|
|
98
|
+
dashboard.store.endDrag();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all registered dashboard IDs
|
|
104
|
+
*/
|
|
105
|
+
readonly registeredDashboards = computed(() => Array.from(this.dashboards().keys()));
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the number of registered dashboards
|
|
109
|
+
*/
|
|
110
|
+
readonly dashboardCount = computed(() => this.dashboards().size);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if any dashboards are registered
|
|
114
|
+
*/
|
|
115
|
+
readonly hasDashboards = computed(() => this.dashboards().size > 0);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update registration for a dashboard store when its ID becomes available
|
|
119
|
+
*/
|
|
120
|
+
updateDashboardRegistration(store: InstanceType<typeof DashboardStore>): void {
|
|
121
|
+
this.registerDashboard(store);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get grid configuration for a specific dashboard
|
|
126
|
+
*/
|
|
127
|
+
getDashboardGridConfig(dashboardId: string): { rows: number; columns: number; gutterSize: string } | null {
|
|
128
|
+
const dashboard = this.dashboards().get(dashboardId);
|
|
129
|
+
if (!dashboard) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const store = dashboard.store;
|
|
134
|
+
return {
|
|
135
|
+
rows: store.rows(),
|
|
136
|
+
columns: store.columns(),
|
|
137
|
+
gutterSize: store.gutterSize()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the store instance for a specific dashboard ID
|
|
143
|
+
*/
|
|
144
|
+
getDashboardStore(dashboardId: string): InstanceType<typeof DashboardStore> | null {
|
|
145
|
+
const dashboard = this.dashboards().get(dashboardId);
|
|
146
|
+
return dashboard?.store || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get all registered dashboards (for viewport service integration)
|
|
151
|
+
*/
|
|
152
|
+
getAllDashboards(): Map<string, DashboardInstance> {
|
|
153
|
+
return this.dashboards();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Injectable, computed, signal, inject, DestroyRef, PLATFORM_ID } from '@angular/core';
|
|
2
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
3
|
+
import { DashboardStore } from '../store/dashboard-store';
|
|
4
|
+
import { ReservedSpace, DEFAULT_RESERVED_SPACE } from '../models/reserved-space';
|
|
5
|
+
|
|
6
|
+
export interface ViewportSize {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DashboardConstraints {
|
|
12
|
+
maxWidth: number;
|
|
13
|
+
maxHeight: number;
|
|
14
|
+
constrainedBy: 'width' | 'height' | 'none';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Internal component-scoped service that provides viewport-aware constraints for a single dashboard.
|
|
19
|
+
* Each dashboard component gets its own instance of this service.
|
|
20
|
+
*
|
|
21
|
+
* This service is NOT part of the public API and should remain internal to the library.
|
|
22
|
+
*/
|
|
23
|
+
@Injectable()
|
|
24
|
+
export class DashboardViewportService {
|
|
25
|
+
private readonly platformId = inject(PLATFORM_ID);
|
|
26
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
27
|
+
private readonly store = inject(DashboardStore);
|
|
28
|
+
|
|
29
|
+
private readonly viewportSize = signal<ViewportSize>({ width: 0, height: 0 });
|
|
30
|
+
private readonly reservedSpace = signal<ReservedSpace>(DEFAULT_RESERVED_SPACE);
|
|
31
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
35
|
+
this.initializeViewportTracking();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize viewport size tracking using ResizeObserver on the window
|
|
41
|
+
*/
|
|
42
|
+
private initializeViewportTracking(): void {
|
|
43
|
+
// Use ResizeObserver on document.documentElement for accurate viewport tracking
|
|
44
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
45
|
+
if (entries.length > 0) {
|
|
46
|
+
const entry = entries[0];
|
|
47
|
+
const { inlineSize, blockSize } = entry.contentBoxSize[0];
|
|
48
|
+
this.viewportSize.set({
|
|
49
|
+
width: inlineSize,
|
|
50
|
+
height: blockSize
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.resizeObserver.observe(document.documentElement);
|
|
56
|
+
|
|
57
|
+
// Initial size
|
|
58
|
+
this.viewportSize.set({
|
|
59
|
+
width: window.innerWidth,
|
|
60
|
+
height: window.innerHeight
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Cleanup on destroy
|
|
64
|
+
this.destroyRef.onDestroy(() => {
|
|
65
|
+
this.resizeObserver?.disconnect();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set reserved space that should be excluded from dashboard calculations
|
|
71
|
+
* (e.g., toolbar height, widget list width, padding)
|
|
72
|
+
*/
|
|
73
|
+
setReservedSpace(space: ReservedSpace): void {
|
|
74
|
+
this.reservedSpace.set(space);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get current viewport size
|
|
79
|
+
*/
|
|
80
|
+
readonly currentViewportSize = this.viewportSize.asReadonly();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get current reserved space
|
|
84
|
+
*/
|
|
85
|
+
readonly currentReservedSpace = this.reservedSpace.asReadonly();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calculate available space for dashboard after accounting for reserved areas
|
|
89
|
+
*/
|
|
90
|
+
readonly availableSpace = computed((): ViewportSize => {
|
|
91
|
+
const viewport = this.viewportSize();
|
|
92
|
+
const reserved = this.reservedSpace();
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
width: Math.max(0, viewport.width - reserved.left - reserved.right),
|
|
96
|
+
height: Math.max(0, viewport.height - reserved.top - reserved.bottom)
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Calculate dashboard constraints for this dashboard instance
|
|
102
|
+
*/
|
|
103
|
+
readonly constraints = computed((): DashboardConstraints => {
|
|
104
|
+
const availableSize = this.availableSpace();
|
|
105
|
+
|
|
106
|
+
// Get grid configuration from our component's store
|
|
107
|
+
const rows = this.store.rows();
|
|
108
|
+
const columns = this.store.columns();
|
|
109
|
+
|
|
110
|
+
if (rows === 0 || columns === 0) {
|
|
111
|
+
return {
|
|
112
|
+
maxWidth: availableSize.width,
|
|
113
|
+
maxHeight: availableSize.height,
|
|
114
|
+
constrainedBy: 'none'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Calculate aspect ratio
|
|
119
|
+
const aspectRatio = columns / rows;
|
|
120
|
+
|
|
121
|
+
// Calculate maximum size that fits within available space
|
|
122
|
+
const maxWidthFromHeight = availableSize.height * aspectRatio;
|
|
123
|
+
const maxHeightFromWidth = availableSize.width / aspectRatio;
|
|
124
|
+
|
|
125
|
+
let maxWidth: number;
|
|
126
|
+
let maxHeight: number;
|
|
127
|
+
let constrainedBy: 'width' | 'height';
|
|
128
|
+
|
|
129
|
+
if (maxWidthFromHeight <= availableSize.width) {
|
|
130
|
+
// Height is the limiting factor
|
|
131
|
+
maxWidth = maxWidthFromHeight;
|
|
132
|
+
maxHeight = availableSize.height;
|
|
133
|
+
constrainedBy = 'height';
|
|
134
|
+
} else {
|
|
135
|
+
// Width is the limiting factor
|
|
136
|
+
maxWidth = availableSize.width;
|
|
137
|
+
maxHeight = maxHeightFromWidth;
|
|
138
|
+
constrainedBy = 'width';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
maxWidth: Math.max(0, maxWidth),
|
|
143
|
+
maxHeight: Math.max(0, maxHeight),
|
|
144
|
+
constrainedBy
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// dashboard.service.ts
|
|
2
|
+
import { Injectable, signal } from '@angular/core';
|
|
3
|
+
import {
|
|
4
|
+
createFactoryFromComponent,
|
|
5
|
+
WidgetComponentClass,
|
|
6
|
+
WidgetFactory,
|
|
7
|
+
} from '../models';
|
|
8
|
+
import { UnknownWidgetComponent } from '../internal-widgets/unknown-widget/unknown-widget.component';
|
|
9
|
+
|
|
10
|
+
@Injectable({
|
|
11
|
+
providedIn: 'root',
|
|
12
|
+
})
|
|
13
|
+
export class DashboardService {
|
|
14
|
+
readonly #widgetTypes = signal<WidgetComponentClass[]>([]);
|
|
15
|
+
readonly #widgetFactoryMap = new Map<string, WidgetFactory>();
|
|
16
|
+
readonly #unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent);
|
|
17
|
+
readonly widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal
|
|
18
|
+
|
|
19
|
+
registerWidgetType(widget: WidgetComponentClass) {
|
|
20
|
+
if (
|
|
21
|
+
this.#widgetTypes().some(
|
|
22
|
+
(w) => w.metadata.widgetTypeid === widget.metadata.widgetTypeid
|
|
23
|
+
)
|
|
24
|
+
) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Widget type '${widget.metadata.widgetTypeid}' is already registered`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.#widgetFactoryMap.set(
|
|
31
|
+
widget.metadata.widgetTypeid,
|
|
32
|
+
createFactoryFromComponent(widget)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
this.#widgetTypes.set([...this.#widgetTypes(), widget]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getFactory(widgetTypeid: string): WidgetFactory {
|
|
39
|
+
const factory = this.#widgetFactoryMap.get(widgetTypeid);
|
|
40
|
+
|
|
41
|
+
if (factory) {
|
|
42
|
+
return factory;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Return fallback factory for unknown widget types
|
|
46
|
+
console.warn(
|
|
47
|
+
`Unknown widget type: ${widgetTypeid}, using fallback error widget`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Create a custom factory that preserves the original widget type ID in state
|
|
51
|
+
return {
|
|
52
|
+
...this.#unknownWidgetFactory,
|
|
53
|
+
createInstance: (container, state) => {
|
|
54
|
+
const ref = this.#unknownWidgetFactory.createInstance(container, {
|
|
55
|
+
originalWidgetTypeid: widgetTypeid,
|
|
56
|
+
...((state as any) || {}),
|
|
57
|
+
});
|
|
58
|
+
return ref;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|