@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,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signalStoreFeature,
|
|
3
|
+
withMethods,
|
|
4
|
+
withState,
|
|
5
|
+
withComputed,
|
|
6
|
+
patchState,
|
|
7
|
+
} from '@ngrx/signals';
|
|
8
|
+
import { computed } from '@angular/core';
|
|
9
|
+
import {
|
|
10
|
+
CellId,
|
|
11
|
+
CellIdUtils,
|
|
12
|
+
CellData,
|
|
13
|
+
WidgetFactory,
|
|
14
|
+
WidgetId,
|
|
15
|
+
WidgetIdUtils,
|
|
16
|
+
} from '../../models';
|
|
17
|
+
|
|
18
|
+
export interface WidgetManagementState {
|
|
19
|
+
widgetsById: Record<string, CellData>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const initialWidgetManagementState: WidgetManagementState = {
|
|
23
|
+
widgetsById: {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const withWidgetManagement = () =>
|
|
27
|
+
signalStoreFeature(
|
|
28
|
+
withState<WidgetManagementState>(initialWidgetManagementState),
|
|
29
|
+
|
|
30
|
+
// Computed cells array - lazy evaluation, automatic memoization
|
|
31
|
+
withComputed((store) => ({
|
|
32
|
+
cells: computed(() => Object.values(store.widgetsById())),
|
|
33
|
+
})),
|
|
34
|
+
|
|
35
|
+
withMethods((store) => ({
|
|
36
|
+
addWidget(cell: CellData) {
|
|
37
|
+
const widgetKey = WidgetIdUtils.toString(cell.widgetId);
|
|
38
|
+
patchState(store, {
|
|
39
|
+
widgetsById: { ...store.widgetsById(), [widgetKey]: cell },
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
removeWidget(widgetId: WidgetId) {
|
|
44
|
+
const widgetKey = WidgetIdUtils.toString(widgetId);
|
|
45
|
+
const { [widgetKey]: removed, ...remaining } = store.widgetsById();
|
|
46
|
+
patchState(store, { widgetsById: remaining });
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
updateWidgetPosition(widgetId: WidgetId, row: number, col: number) {
|
|
50
|
+
const widgetKey = WidgetIdUtils.toString(widgetId);
|
|
51
|
+
const existingWidget = store.widgetsById()[widgetKey];
|
|
52
|
+
|
|
53
|
+
if (existingWidget) {
|
|
54
|
+
// Update position and recalculate cellId based on new position
|
|
55
|
+
const newCellId = CellIdUtils.create(row, col);
|
|
56
|
+
patchState(store, {
|
|
57
|
+
widgetsById: {
|
|
58
|
+
...store.widgetsById(),
|
|
59
|
+
[widgetKey]: { ...existingWidget, row, col, cellId: newCellId },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
createWidget(
|
|
66
|
+
row: number,
|
|
67
|
+
col: number,
|
|
68
|
+
widgetFactory: WidgetFactory,
|
|
69
|
+
widgetState?: string
|
|
70
|
+
) {
|
|
71
|
+
const widgetId = WidgetIdUtils.generate(); // Generate unique widget ID
|
|
72
|
+
const cellId = CellIdUtils.create(row, col); // Calculate position-based cell ID
|
|
73
|
+
const cell: CellData = {
|
|
74
|
+
widgetId,
|
|
75
|
+
cellId,
|
|
76
|
+
row,
|
|
77
|
+
col,
|
|
78
|
+
rowSpan: 1,
|
|
79
|
+
colSpan: 1,
|
|
80
|
+
widgetFactory,
|
|
81
|
+
widgetState,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const widgetKey = WidgetIdUtils.toString(widgetId);
|
|
85
|
+
patchState(store, {
|
|
86
|
+
widgetsById: { ...store.widgetsById(), [widgetKey]: cell },
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
updateCellSettings(widgetId: WidgetId, flat: boolean) {
|
|
91
|
+
const widgetKey = WidgetIdUtils.toString(widgetId);
|
|
92
|
+
const existingWidget = store.widgetsById()[widgetKey];
|
|
93
|
+
|
|
94
|
+
if (existingWidget) {
|
|
95
|
+
patchState(store, {
|
|
96
|
+
widgetsById: {
|
|
97
|
+
...store.widgetsById(),
|
|
98
|
+
[widgetKey]: { ...existingWidget, flat },
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
updateWidgetSpan(widgetId: WidgetId, rowSpan: number, colSpan: number) {
|
|
105
|
+
const widgetKey = WidgetIdUtils.toString(widgetId);
|
|
106
|
+
const existingWidget = store.widgetsById()[widgetKey];
|
|
107
|
+
|
|
108
|
+
if (existingWidget) {
|
|
109
|
+
patchState(store, {
|
|
110
|
+
widgetsById: {
|
|
111
|
+
...store.widgetsById(),
|
|
112
|
+
[widgetKey]: { ...existingWidget, rowSpan, colSpan },
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
updateWidgetState(widgetId: WidgetId, widgetState: unknown) {
|
|
119
|
+
const widgetKey = WidgetIdUtils.toString(widgetId);
|
|
120
|
+
const existingWidget = store.widgetsById()[widgetKey];
|
|
121
|
+
|
|
122
|
+
if (existingWidget) {
|
|
123
|
+
patchState(store, {
|
|
124
|
+
widgetsById: {
|
|
125
|
+
...store.widgetsById(),
|
|
126
|
+
[widgetKey]: { ...existingWidget, widgetState },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
updateAllWidgetStates(cellStates: Map<string, unknown>) {
|
|
133
|
+
const updatedWidgetsById = { ...store.widgetsById() };
|
|
134
|
+
|
|
135
|
+
// Convert cell ID keys to widget IDs and update states
|
|
136
|
+
for (const [cellIdString, newState] of cellStates) {
|
|
137
|
+
// Find the widget with the matching cell ID
|
|
138
|
+
const widget = Object.values(updatedWidgetsById).find(w =>
|
|
139
|
+
CellIdUtils.toString(w.cellId) === cellIdString
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (widget) {
|
|
143
|
+
const widgetIdString = WidgetIdUtils.toString(widget.widgetId);
|
|
144
|
+
updatedWidgetsById[widgetIdString] = {
|
|
145
|
+
...updatedWidgetsById[widgetIdString],
|
|
146
|
+
widgetState: newState,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
patchState(store, { widgetsById: updatedWidgetsById });
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
clearDashboard() {
|
|
155
|
+
patchState(store, { widgetsById: {} });
|
|
156
|
+
},
|
|
157
|
+
}))
|
|
158
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@mixin dashboard-grid-vars {
|
|
2
|
+
/* grid geometry derived once, reused everywhere */
|
|
3
|
+
--cell-size: calc(
|
|
4
|
+
100cqi / var(--columns) - var(--gutter-size) * var(--gutters) /
|
|
5
|
+
var(--columns)
|
|
6
|
+
);
|
|
7
|
+
--tile-size: calc(var(--cell-size) + var(--gutter-size));
|
|
8
|
+
--tile-offset: calc(
|
|
9
|
+
var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// widget-list-bridge-integration.spec.ts
|
|
2
|
+
import { TestBed } from '@angular/core/testing';
|
|
3
|
+
import { Component, signal } from '@angular/core';
|
|
4
|
+
import { WidgetListComponent } from '../widget-list.component';
|
|
5
|
+
import { DashboardBridgeService } from '../../services/dashboard-bridge.service';
|
|
6
|
+
import { DashboardService } from '../../services/dashboard.service';
|
|
7
|
+
import { ArrowWidgetComponent } from '@dragonworks/ngx-dashboard-widgets';
|
|
8
|
+
|
|
9
|
+
// Mock dashboard store
|
|
10
|
+
function createMockDashboardStore(dashboardId = 'test-dashboard', dimensions = { width: 150, height: 100 }) {
|
|
11
|
+
return {
|
|
12
|
+
dashboardId: jasmine.createSpy('dashboardId').and.returnValue(dashboardId),
|
|
13
|
+
gridCellDimensions: jasmine.createSpy('gridCellDimensions').and.returnValue(dimensions),
|
|
14
|
+
startDrag: jasmine.createSpy('startDrag'),
|
|
15
|
+
endDrag: jasmine.createSpy('endDrag')
|
|
16
|
+
} as any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Test component that simulates dashboard registration
|
|
20
|
+
@Component({
|
|
21
|
+
template: '',
|
|
22
|
+
providers: []
|
|
23
|
+
})
|
|
24
|
+
class MockDashboardComponent {
|
|
25
|
+
private mockStore = createMockDashboardStore('test-dashboard-1', { width: 200, height: 120 });
|
|
26
|
+
|
|
27
|
+
constructor(private bridge: DashboardBridgeService) {
|
|
28
|
+
this.bridge.registerDashboard(this.mockStore);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
destroy() {
|
|
32
|
+
this.bridge.unregisterDashboard(this.mockStore);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('WidgetListComponent Integration with DashboardBridgeService', () => {
|
|
37
|
+
let component: WidgetListComponent;
|
|
38
|
+
let fixture: any;
|
|
39
|
+
let bridgeService: DashboardBridgeService;
|
|
40
|
+
let dashboardService: DashboardService;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
await TestBed.configureTestingModule({
|
|
44
|
+
imports: [WidgetListComponent],
|
|
45
|
+
providers: [
|
|
46
|
+
DashboardBridgeService,
|
|
47
|
+
DashboardService
|
|
48
|
+
]
|
|
49
|
+
}).compileComponents();
|
|
50
|
+
|
|
51
|
+
// Register a test widget
|
|
52
|
+
dashboardService = TestBed.inject(DashboardService);
|
|
53
|
+
dashboardService.registerWidgetType(ArrowWidgetComponent);
|
|
54
|
+
|
|
55
|
+
bridgeService = TestBed.inject(DashboardBridgeService);
|
|
56
|
+
fixture = TestBed.createComponent(WidgetListComponent);
|
|
57
|
+
component = fixture.componentInstance;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Standalone Functionality', () => {
|
|
61
|
+
it('should create without any registered dashboards', () => {
|
|
62
|
+
expect(component).toBeTruthy();
|
|
63
|
+
expect(bridgeService.dashboardCount()).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should use fallback dimensions when no dashboards registered', () => {
|
|
67
|
+
fixture.detectChanges();
|
|
68
|
+
|
|
69
|
+
expect(component.gridCellDimensions()).toEqual({ width: 100, height: 100 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should display available widgets even without dashboards', () => {
|
|
73
|
+
fixture.detectChanges();
|
|
74
|
+
|
|
75
|
+
expect(component.widgets().length).toBeGreaterThan(0);
|
|
76
|
+
expect(component.widgets()[0].widgetTypeid).toBe('@default/arrow-widget');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle drag start/end gracefully without dashboards', () => {
|
|
80
|
+
const mockEvent = {
|
|
81
|
+
dataTransfer: {
|
|
82
|
+
effectAllowed: '',
|
|
83
|
+
setDragImage: jasmine.createSpy('setDragImage')
|
|
84
|
+
}
|
|
85
|
+
} as any;
|
|
86
|
+
|
|
87
|
+
const widget = component.widgets()[0];
|
|
88
|
+
|
|
89
|
+
expect(() => {
|
|
90
|
+
component.onDragStart(mockEvent, widget);
|
|
91
|
+
component.onDragEnd();
|
|
92
|
+
}).not.toThrow();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
describe('Drag Ghost Creation', () => {
|
|
99
|
+
it('should create drag ghost with fallback dimensions when no dashboards', () => {
|
|
100
|
+
fixture.detectChanges();
|
|
101
|
+
|
|
102
|
+
const mockEvent = {
|
|
103
|
+
dataTransfer: {
|
|
104
|
+
effectAllowed: '',
|
|
105
|
+
setDragImage: jasmine.createSpy('setDragImage')
|
|
106
|
+
}
|
|
107
|
+
} as any;
|
|
108
|
+
|
|
109
|
+
const widget = component.widgets()[0];
|
|
110
|
+
component.onDragStart(mockEvent, widget);
|
|
111
|
+
|
|
112
|
+
// Should still work with fallback dimensions
|
|
113
|
+
expect(mockEvent.dataTransfer.setDragImage).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Widget State Management', () => {
|
|
118
|
+
it('should manage active widget state independently of dashboard registration', () => {
|
|
119
|
+
expect(component.activeWidget()).toBeNull();
|
|
120
|
+
|
|
121
|
+
const mockEvent = {
|
|
122
|
+
dataTransfer: {
|
|
123
|
+
effectAllowed: '',
|
|
124
|
+
setDragImage: jasmine.createSpy('setDragImage')
|
|
125
|
+
}
|
|
126
|
+
} as any;
|
|
127
|
+
|
|
128
|
+
const widget = component.widgets()[0];
|
|
129
|
+
|
|
130
|
+
component.onDragStart(mockEvent, widget);
|
|
131
|
+
expect(component.activeWidget()).toBe(widget.widgetTypeid);
|
|
132
|
+
|
|
133
|
+
component.onDragEnd();
|
|
134
|
+
expect(component.activeWidget()).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- widget-list.component.html -->
|
|
2
|
+
<div class="widget-list" role="list" aria-label="Available widgets">
|
|
3
|
+
@for (widget of widgets(); track widget.widgetTypeid) {
|
|
4
|
+
<div
|
|
5
|
+
class="widget-list-item"
|
|
6
|
+
[class.active]="activeWidget() === widget.widgetTypeid"
|
|
7
|
+
draggable="true"
|
|
8
|
+
(dragstart)="onDragStart($event, widget)"
|
|
9
|
+
(dragend)="onDragEnd()"
|
|
10
|
+
role="listitem"
|
|
11
|
+
[attr.aria-grabbed]="activeWidget() === widget.widgetTypeid"
|
|
12
|
+
[attr.aria-label]="widget.name + ' widget: ' + widget.description"
|
|
13
|
+
tabindex="0"
|
|
14
|
+
>
|
|
15
|
+
<div class="icon" [innerHTML]="widget.safeSvgIcon" aria-hidden="true"></div>
|
|
16
|
+
<div class="content">
|
|
17
|
+
<strong>{{ widget.name }}</strong>
|
|
18
|
+
<small>{{ widget.description }}</small>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
}
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// widget-list.component.scss
|
|
2
|
+
:host {
|
|
3
|
+
background-color: var(--mat-sys-surface-container, #f5f5f5);
|
|
4
|
+
container-type: inline-size;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.widget-list {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: var(--mat-sys-spacing-2, 8px);
|
|
11
|
+
|
|
12
|
+
// Responsive spacing based on container width
|
|
13
|
+
@container (max-width: 200px) {
|
|
14
|
+
gap: var(--mat-sys-spacing-1, 4px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@container (min-width: 400px) {
|
|
18
|
+
gap: var(--mat-sys-spacing-3, 12px);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.widget-list-item {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: start;
|
|
25
|
+
gap: var(--mat-sys-spacing-3, 12px);
|
|
26
|
+
background-color: var(--mat-sys-surface, #ffffff);
|
|
27
|
+
border: 1px solid var(--mat-sys-outline-variant, #c7c7c7);
|
|
28
|
+
padding: var(--mat-sys-spacing-3, 12px);
|
|
29
|
+
border-radius: var(--mat-sys-corner-small, 4px);
|
|
30
|
+
cursor: grab;
|
|
31
|
+
transition: background-color var(--mat-sys-motion-duration-medium2, 300ms) var(--mat-sys-motion-easing-standard, ease-in-out),
|
|
32
|
+
border-color var(--mat-sys-motion-duration-medium2, 300ms) var(--mat-sys-motion-easing-standard, ease-in-out),
|
|
33
|
+
box-shadow var(--mat-sys-motion-duration-medium2, 300ms) var(--mat-sys-motion-easing-standard, ease-in-out);
|
|
34
|
+
box-shadow: var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, 0.05));
|
|
35
|
+
|
|
36
|
+
// Responsive padding based on container width
|
|
37
|
+
@container (max-width: 200px) {
|
|
38
|
+
padding: var(--mat-sys-spacing-2, 8px);
|
|
39
|
+
gap: var(--mat-sys-spacing-2, 8px);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@container (min-width: 400px) {
|
|
43
|
+
padding: var(--mat-sys-spacing-4, 16px);
|
|
44
|
+
gap: var(--mat-sys-spacing-4, 16px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.icon {
|
|
48
|
+
width: clamp(20px, 4vw, 28px);
|
|
49
|
+
height: clamp(20px, 4vw, 28px);
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
|
|
52
|
+
::ng-deep svg {
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 100%;
|
|
55
|
+
display: block;
|
|
56
|
+
fill: var(--mat-sys-on-surface-variant, #5f5f5f);
|
|
57
|
+
transition: fill var(--mat-sys-motion-duration-short2, 150ms) var(--mat-sys-motion-easing-standard, ease-in-out);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.content {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
line-height: 1.2;
|
|
65
|
+
color: var(--mat-sys-on-surface, #1c1c1c);
|
|
66
|
+
flex: 1;
|
|
67
|
+
min-width: 0; // Allow text to truncate
|
|
68
|
+
|
|
69
|
+
strong {
|
|
70
|
+
color: var(--mat-sys-on-surface, #1c1c1c);
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
font-size: clamp(0.875rem, 2.5vw, 1rem);
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
small {
|
|
79
|
+
color: var(--mat-sys-on-surface-variant, #5f5f5f);
|
|
80
|
+
font-size: clamp(0.75rem, 2vw, 0.875rem);
|
|
81
|
+
margin-top: var(--mat-sys-spacing-1, 4px);
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&:hover {
|
|
89
|
+
background-color: var(--mat-sys-surface-container-low, #f0f0f0);
|
|
90
|
+
box-shadow: var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, 0.1));
|
|
91
|
+
|
|
92
|
+
.icon ::ng-deep svg {
|
|
93
|
+
fill: var(--mat-sys-on-surface, #1c1c1c);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&:active {
|
|
98
|
+
cursor: grabbing;
|
|
99
|
+
background-color: var(--mat-sys-surface-container, #f5f5f5);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.widget-list-item.active {
|
|
104
|
+
background-color: var(--mat-sys-primary-container, #e6f2ff);
|
|
105
|
+
border-color: var(--mat-sys-primary, #1976d2);
|
|
106
|
+
color: var(--mat-sys-on-primary-container, #004a99);
|
|
107
|
+
|
|
108
|
+
.content {
|
|
109
|
+
strong {
|
|
110
|
+
color: var(--mat-sys-on-primary-container, #004a99);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
small {
|
|
114
|
+
color: var(--mat-sys-on-primary-container, #004a99);
|
|
115
|
+
opacity: 0.8;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.icon ::ng-deep svg {
|
|
120
|
+
fill: var(--mat-sys-on-primary-container, #004a99);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Drag ghost styles
|
|
125
|
+
.drag-ghost {
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: 0;
|
|
128
|
+
left: 0;
|
|
129
|
+
z-index: 9999;
|
|
130
|
+
margin: 0;
|
|
131
|
+
pointer-events: none;
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
box-sizing: border-box;
|
|
136
|
+
|
|
137
|
+
background-color: var(--mat-sys-surface, #ffffff);
|
|
138
|
+
border: 1px solid var(--mat-sys-outline-variant, #c7c7c7);
|
|
139
|
+
border-radius: var(--mat-sys-corner-small, 4px);
|
|
140
|
+
box-shadow: var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, 0.15));
|
|
141
|
+
opacity: 0.8;
|
|
142
|
+
|
|
143
|
+
.icon {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
|
|
148
|
+
::ng-deep svg {
|
|
149
|
+
display: block;
|
|
150
|
+
fill: var(--mat-sys-on-surface-variant, #5f5f5f);
|
|
151
|
+
opacity: 0.6;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// widget-list.component.ts
|
|
2
|
+
import {
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
inject,
|
|
6
|
+
Renderer2,
|
|
7
|
+
signal,
|
|
8
|
+
ChangeDetectionStrategy,
|
|
9
|
+
afterNextRender,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { DragData, WidgetMetadata } from '../models';
|
|
12
|
+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
13
|
+
import { DashboardService } from '../services/dashboard.service';
|
|
14
|
+
import { DashboardBridgeService } from '../services/dashboard-bridge.service';
|
|
15
|
+
|
|
16
|
+
interface WidgetDisplayItem extends WidgetMetadata {
|
|
17
|
+
safeSvgIcon?: SafeHtml;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'ngx-dashboard-widget-list',
|
|
22
|
+
standalone: true,
|
|
23
|
+
imports: [],
|
|
24
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
|
+
templateUrl: './widget-list.component.html',
|
|
26
|
+
styleUrl: './widget-list.component.scss',
|
|
27
|
+
})
|
|
28
|
+
export class WidgetListComponent {
|
|
29
|
+
readonly #service = inject(DashboardService);
|
|
30
|
+
readonly #sanitizer = inject(DomSanitizer);
|
|
31
|
+
readonly #renderer = inject(Renderer2);
|
|
32
|
+
readonly #bridge = inject(DashboardBridgeService);
|
|
33
|
+
|
|
34
|
+
activeWidget = signal<string | null>(null);
|
|
35
|
+
|
|
36
|
+
// Get grid cell dimensions from bridge service (uses first available dashboard)
|
|
37
|
+
gridCellDimensions = this.#bridge.availableDimensions;
|
|
38
|
+
|
|
39
|
+
widgets = computed(() =>
|
|
40
|
+
this.#service.widgetTypes().map((w) => ({
|
|
41
|
+
...w.metadata,
|
|
42
|
+
safeSvgIcon: this.#sanitizer.bypassSecurityTrustHtml(w.metadata.svgIcon),
|
|
43
|
+
}))
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
onDragStart(event: DragEvent, widget: WidgetDisplayItem) {
|
|
47
|
+
if (!event.dataTransfer) return;
|
|
48
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
49
|
+
|
|
50
|
+
const dragData: DragData = {
|
|
51
|
+
kind: 'widget',
|
|
52
|
+
content: widget,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.activeWidget.set(widget.widgetTypeid);
|
|
56
|
+
this.#bridge.startDrag(dragData);
|
|
57
|
+
|
|
58
|
+
// Create custom drag ghost for better UX
|
|
59
|
+
const ghost = this.#createDragGhost(widget.svgIcon);
|
|
60
|
+
document.body.appendChild(ghost);
|
|
61
|
+
|
|
62
|
+
// Force reflow to ensure element is rendered
|
|
63
|
+
const _reflow = ghost.offsetHeight;
|
|
64
|
+
|
|
65
|
+
event.dataTransfer.setDragImage(ghost, 10, 10);
|
|
66
|
+
|
|
67
|
+
// Delay removal to ensure browser has time to snapshot the drag image
|
|
68
|
+
setTimeout(() => ghost.remove());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onDragEnd(): void {
|
|
72
|
+
this.activeWidget.set(null);
|
|
73
|
+
this.#bridge.endDrag();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#createDragGhost(svgIcon: string | undefined): HTMLElement {
|
|
77
|
+
const dimensions = this.gridCellDimensions();
|
|
78
|
+
|
|
79
|
+
const el = this.#renderer.createElement('div');
|
|
80
|
+
this.#renderer.addClass(el, 'drag-ghost');
|
|
81
|
+
|
|
82
|
+
// Set dimensions using CSS custom properties for dynamic sizing
|
|
83
|
+
this.#renderer.setStyle(el, 'width', `${dimensions.width}px`);
|
|
84
|
+
this.#renderer.setStyle(el, 'height', `${dimensions.height}px`);
|
|
85
|
+
|
|
86
|
+
if (svgIcon) {
|
|
87
|
+
const iconWrapper = this.#renderer.createElement('div');
|
|
88
|
+
this.#renderer.addClass(iconWrapper, 'icon');
|
|
89
|
+
|
|
90
|
+
iconWrapper.innerHTML = svgIcon;
|
|
91
|
+
const svg = iconWrapper.querySelector('svg');
|
|
92
|
+
|
|
93
|
+
if (svg) {
|
|
94
|
+
// Size the SVG to 80% of the cell dimensions
|
|
95
|
+
svg.setAttribute('width', `${dimensions.width * 0.8}`);
|
|
96
|
+
svg.setAttribute('height', `${dimensions.height * 0.8}`);
|
|
97
|
+
// Remove hardcoded fill and opacity - let CSS handle styling
|
|
98
|
+
svg.removeAttribute('fill');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
el.appendChild(iconWrapper);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return el;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API Surface of ngx-dashboard
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Main dashboard components
|
|
6
|
+
export * from './lib/dashboard/dashboard.component';
|
|
7
|
+
export * from './lib/dashboard-editor/dashboard-editor.component';
|
|
8
|
+
export * from './lib/dashboard-viewer/dashboard-viewer.component';
|
|
9
|
+
export * from './lib/widget-list/widget-list.component';
|
|
10
|
+
|
|
11
|
+
// Public Services
|
|
12
|
+
export * from './lib/services/dashboard.service';
|
|
13
|
+
|
|
14
|
+
// Store is now internal - removed from public API
|
|
15
|
+
|
|
16
|
+
// Public Models and interfaces
|
|
17
|
+
// TODO: not everything should be exported
|
|
18
|
+
export * from './lib/models';
|
|
19
|
+
|
|
20
|
+
// Providers
|
|
21
|
+
export * from './lib/providers';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import 'zone.js';
|
|
2
|
+
import 'zone.js/testing';
|
|
3
|
+
import { getTestBed } from '@angular/core/testing';
|
|
4
|
+
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
|
|
5
|
+
|
|
6
|
+
// Initialize the Angular testing environment
|
|
7
|
+
getTestBed().initTestEnvironment(
|
|
8
|
+
BrowserTestingModule,
|
|
9
|
+
platformBrowserTesting(),
|
|
10
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/lib",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"inlineSources": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"types": []
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"],
|
|
14
|
+
"exclude": ["**/*.spec.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.lib.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"declarationMap": false
|
|
7
|
+
},
|
|
8
|
+
"angularCompilerOptions": {
|
|
9
|
+
"compilationMode": "partial"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/spec",
|
|
7
|
+
"types": [
|
|
8
|
+
"jasmine"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|