@conduction/nextcloud-vue 0.1.0-beta.3 → 0.1.0-beta.5

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.
Files changed (142) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs +67614 -0
  3. package/dist/nextcloud-vue.cjs.js +58386 -6112
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.cjs.map +1 -0
  6. package/dist/nextcloud-vue.css +1819 -285
  7. package/dist/nextcloud-vue.esm.js +58342 -6088
  8. package/dist/nextcloud-vue.esm.js.map +1 -1
  9. package/package.json +82 -62
  10. package/src/components/CnActionsBar/CnActionsBar.vue +17 -7
  11. package/src/components/CnActionsBar/index.js +1 -1
  12. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  13. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  14. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  17. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  18. package/src/components/CnCardGrid/CnCardGrid.vue +1 -1
  19. package/src/components/CnCardGrid/index.js +1 -1
  20. package/src/components/CnCellRenderer/index.js +1 -1
  21. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  22. package/src/components/CnChartWidget/index.js +1 -0
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
  25. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  26. package/src/components/CnDashboardGrid/index.js +1 -0
  27. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  28. package/src/components/CnDashboardPage/index.js +1 -0
  29. package/src/components/CnDataTable/CnDataTable.vue +1 -1
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
  32. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  33. package/src/components/CnDetailCard/index.js +1 -0
  34. package/src/components/CnDetailPage/CnDetailPage.vue +285 -0
  35. package/src/components/CnDetailPage/index.js +1 -0
  36. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +9 -1
  37. package/src/components/CnFacetSidebar/index.js +1 -1
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +71 -3
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +121 -102
  44. package/src/components/CnIndexSidebar/index.js +1 -1
  45. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  46. package/src/components/CnItemCard/index.js +1 -0
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/index.js +1 -1
  49. package/src/components/CnMassCopyDialog/index.js +1 -1
  50. package/src/components/CnMassDeleteDialog/index.js +1 -1
  51. package/src/components/CnMassExportDialog/index.js +1 -1
  52. package/src/components/CnMassImportDialog/index.js +1 -1
  53. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  54. package/src/components/CnNoteCard/index.js +1 -0
  55. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  56. package/src/components/CnNotesCard/index.js +1 -0
  57. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  58. package/src/components/CnObjectCard/index.js +1 -1
  59. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  60. package/src/components/CnObjectSidebar/index.js +1 -0
  61. package/src/components/CnPageHeader/index.js +1 -1
  62. package/src/components/CnPagination/index.js +1 -1
  63. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
  64. package/src/components/CnRowActions/CnRowActions.vue +25 -3
  65. package/src/components/CnRowActions/index.js +1 -1
  66. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  67. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  68. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  69. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  70. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  71. package/src/components/CnSchemaFormDialog/index.js +1 -0
  72. package/src/components/CnSettingsCard/index.js +1 -1
  73. package/src/components/CnSettingsSection/index.js +1 -1
  74. package/src/components/CnStatsBlock/CnStatsBlock.vue +62 -8
  75. package/src/components/CnStatsBlock/index.js +1 -1
  76. package/src/components/CnStatusBadge/index.js +1 -1
  77. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  78. package/src/components/CnTabbedFormDialog/index.js +1 -0
  79. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  80. package/src/components/CnTasksCard/index.js +1 -0
  81. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  82. package/src/components/CnTileWidget/index.js +1 -0
  83. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  84. package/src/components/CnTimelineStages/index.js +1 -0
  85. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  86. package/src/components/CnUserActionMenu/index.js +1 -0
  87. package/src/components/CnVersionInfoCard/index.js +1 -1
  88. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  89. package/src/components/CnWidgetRenderer/index.js +1 -0
  90. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  91. package/src/components/CnWidgetWrapper/index.js +1 -0
  92. package/src/components/index.js +43 -29
  93. package/src/composables/index.js +4 -3
  94. package/src/composables/useDashboardView.js +240 -0
  95. package/src/composables/useDetailView.js +289 -132
  96. package/src/composables/useListView.js +363 -362
  97. package/src/composables/useSubResource.js +142 -142
  98. package/src/constants/metadata.js +30 -30
  99. package/src/css/CnSchemaFormDialog.css +546 -0
  100. package/src/css/__sample_nextcloud_tokens.css +110 -0
  101. package/src/css/actions-bar.css +48 -48
  102. package/src/css/badge.css +51 -51
  103. package/src/css/card.css +128 -128
  104. package/src/css/dashboard.css +70 -0
  105. package/src/css/detail-page.css +168 -0
  106. package/src/css/detail.css +68 -68
  107. package/src/css/index-page.css +44 -32
  108. package/src/css/index-sidebar.css +193 -187
  109. package/src/css/index.css +16 -12
  110. package/src/css/layout.css +90 -90
  111. package/src/css/page-header.css +33 -33
  112. package/src/css/pagination.css +72 -72
  113. package/src/css/table.css +142 -142
  114. package/src/css/timeline-stages.css +218 -0
  115. package/src/css/utilities.css +46 -46
  116. package/src/index.js +72 -53
  117. package/src/store/createSubResourcePlugin.js +135 -135
  118. package/src/store/index.js +3 -3
  119. package/src/store/plugins/auditTrails.js +17 -17
  120. package/src/store/plugins/files.js +250 -186
  121. package/src/store/plugins/index.js +7 -5
  122. package/src/store/plugins/lifecycle.js +180 -180
  123. package/src/store/plugins/relations.js +68 -68
  124. package/src/store/plugins/search.js +372 -0
  125. package/src/store/plugins/selection.js +104 -0
  126. package/src/store/useObjectStore.js +829 -686
  127. package/src/types/auditTrail.d.ts +32 -32
  128. package/src/types/file.d.ts +23 -23
  129. package/src/types/index.d.ts +35 -35
  130. package/src/types/notification.d.ts +36 -36
  131. package/src/types/object.d.ts +40 -40
  132. package/src/types/organisation.d.ts +41 -41
  133. package/src/types/register.d.ts +25 -25
  134. package/src/types/schema.d.ts +39 -39
  135. package/src/types/shared.d.ts +79 -79
  136. package/src/types/source.d.ts +14 -14
  137. package/src/types/task.d.ts +31 -31
  138. package/src/utils/errors.js +96 -96
  139. package/src/utils/headers.js +68 -50
  140. package/src/utils/id.js +13 -0
  141. package/src/utils/index.js +3 -3
  142. package/src/utils/schema.js +422 -419
@@ -0,0 +1,211 @@
1
+ <!--
2
+ CnWidgetWrapper — Container shell around a dashboard widget.
3
+
4
+ Provides header (icon + title), scrollable content area, and optional
5
+ footer with action buttons. Applies style configuration for borders,
6
+ backgrounds, and padding.
7
+ -->
8
+ <template>
9
+ <div class="cn-widget-wrapper" :style="wrapperStyles">
10
+ <!-- Header -->
11
+ <div v-if="showTitle" class="cn-widget-wrapper__header">
12
+ <div class="cn-widget-wrapper__header-left">
13
+ <img
14
+ v-if="iconUrl"
15
+ :src="iconUrl"
16
+ :alt="displayTitle"
17
+ class="cn-widget-wrapper__icon">
18
+ <span
19
+ v-else-if="iconClass"
20
+ :class="iconClass"
21
+ class="cn-widget-wrapper__icon" />
22
+ <h3 class="cn-widget-wrapper__title">
23
+ {{ displayTitle }}
24
+ </h3>
25
+ </div>
26
+ <div class="cn-widget-wrapper__actions">
27
+ <slot name="header-actions" />
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Content -->
32
+ <div class="cn-widget-wrapper__content">
33
+ <slot />
34
+ </div>
35
+
36
+ <!-- Footer -->
37
+ <div v-if="$slots.footer || (buttons && buttons.length > 0)" class="cn-widget-wrapper__footer">
38
+ <slot name="footer">
39
+ <a
40
+ v-for="button in buttons"
41
+ :key="button.link"
42
+ :href="button.link"
43
+ class="cn-widget-wrapper__footer-link">
44
+ {{ button.text }}
45
+ </a>
46
+ </slot>
47
+ </div>
48
+ </div>
49
+ </template>
50
+
51
+ <script>
52
+ /**
53
+ * CnWidgetWrapper — Widget container with header, content, and footer.
54
+ *
55
+ * @example
56
+ * <CnWidgetWrapper title="My Cases" :icon-url="casesIconUrl">
57
+ * <MyCasesChart :data="chartData" />
58
+ * </CnWidgetWrapper>
59
+ *
60
+ * @example With NC widget object
61
+ * <CnWidgetWrapper
62
+ * :title="widget.title"
63
+ * :icon-url="widget.iconUrl"
64
+ * :icon-class="widget.iconClass"
65
+ * :buttons="widget.buttons">
66
+ * <CnWidgetRenderer :widget="widget" />
67
+ * </CnWidgetWrapper>
68
+ */
69
+ export default {
70
+ name: 'CnWidgetWrapper',
71
+
72
+ props: {
73
+ /** Widget title */
74
+ title: {
75
+ type: String,
76
+ default: 'Widget',
77
+ },
78
+ /** Whether to show the header with title */
79
+ showTitle: {
80
+ type: Boolean,
81
+ default: true,
82
+ },
83
+ /** Icon URL (image) */
84
+ iconUrl: {
85
+ type: String,
86
+ default: null,
87
+ },
88
+ /** Icon CSS class (e.g., Nextcloud icon class) */
89
+ iconClass: {
90
+ type: String,
91
+ default: null,
92
+ },
93
+ /** Footer action buttons: [{ text, link }] */
94
+ buttons: {
95
+ type: Array,
96
+ default: () => [],
97
+ },
98
+ /**
99
+ * Style configuration for the wrapper.
100
+ * @type {{ backgroundColor?: string, borderStyle?: string, borderWidth?: number, borderColor?: string, borderRadius?: number, padding?: { top: number, right: number, bottom: number, left: number } }}
101
+ */
102
+ styleConfig: {
103
+ type: Object,
104
+ default: () => ({}),
105
+ },
106
+ },
107
+
108
+ computed: {
109
+ displayTitle() {
110
+ return this.title || 'Widget'
111
+ },
112
+
113
+ wrapperStyles() {
114
+ const styles = {}
115
+
116
+ if (this.styleConfig.backgroundColor) {
117
+ styles.backgroundColor = this.styleConfig.backgroundColor
118
+ }
119
+
120
+ if (this.styleConfig.borderStyle && this.styleConfig.borderStyle !== 'none') {
121
+ styles.border = `${this.styleConfig.borderWidth || 1}px ${this.styleConfig.borderStyle} ${this.styleConfig.borderColor || 'var(--color-border)'}`
122
+ }
123
+
124
+ if (this.styleConfig.borderRadius !== undefined) {
125
+ styles.borderRadius = `${this.styleConfig.borderRadius}px`
126
+ }
127
+
128
+ if (this.styleConfig.padding) {
129
+ const p = this.styleConfig.padding
130
+ styles.padding = `${p.top || 0}px ${p.right || 0}px ${p.bottom || 0}px ${p.left || 0}px`
131
+ }
132
+
133
+ return styles
134
+ },
135
+ },
136
+ }
137
+ </script>
138
+
139
+ <style scoped>
140
+ .cn-widget-wrapper {
141
+ height: 100%;
142
+ display: flex;
143
+ flex-direction: column;
144
+ background: var(--color-main-background);
145
+ border: 1px solid var(--color-border);
146
+ overflow: hidden;
147
+ }
148
+
149
+ .cn-widget-wrapper__header {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ padding: 12px 16px;
154
+ border-bottom: 1px solid var(--color-border);
155
+ flex-shrink: 0;
156
+ }
157
+
158
+ .cn-widget-wrapper__header-left {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 8px;
162
+ min-width: 0;
163
+ }
164
+
165
+ .cn-widget-wrapper__icon {
166
+ width: 24px;
167
+ height: 24px;
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ .cn-widget-wrapper__title {
172
+ font-weight: 600;
173
+ font-size: 14px;
174
+ margin: 0;
175
+ white-space: nowrap;
176
+ overflow: hidden;
177
+ text-overflow: ellipsis;
178
+ }
179
+
180
+ .cn-widget-wrapper__content {
181
+ flex: 1;
182
+ overflow: auto;
183
+ min-height: 0;
184
+ padding: 16px;
185
+ }
186
+
187
+ .cn-widget-wrapper__actions {
188
+ display: flex;
189
+ gap: 4px;
190
+ flex-shrink: 0;
191
+ }
192
+
193
+ .cn-widget-wrapper__footer {
194
+ display: flex;
195
+ justify-content: flex-end;
196
+ gap: 8px;
197
+ padding: 8px 16px;
198
+ border-top: 1px solid var(--color-border);
199
+ flex-shrink: 0;
200
+ }
201
+
202
+ .cn-widget-wrapper__footer-link {
203
+ font-size: 13px;
204
+ color: var(--color-primary-element);
205
+ text-decoration: none;
206
+ }
207
+
208
+ .cn-widget-wrapper__footer-link:hover {
209
+ text-decoration: underline;
210
+ }
211
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnWidgetWrapper } from './CnWidgetWrapper.vue'
@@ -1,29 +1,43 @@
1
- export { CnDataTable } from './CnDataTable/index.js'
2
- export { CnFilterBar } from './CnFilterBar/index.js'
3
- export { CnStatusBadge } from './CnStatusBadge/index.js'
4
- export { CnPagination } from './CnPagination/index.js'
5
- export { CnSettingsCard } from './CnSettingsCard/index.js'
6
- export { CnSettingsSection } from './CnSettingsSection/index.js'
7
- export { CnStatsBlock } from './CnStatsBlock/index.js'
8
- export { CnConfigurationCard } from './CnConfigurationCard/index.js'
9
- export { CnVersionInfoCard } from './CnVersionInfoCard/index.js'
10
- export { CnCellRenderer } from './CnCellRenderer/index.js'
11
- export { CnObjectCard } from './CnObjectCard/index.js'
12
- export { CnCardGrid } from './CnCardGrid/index.js'
13
- export { CnFacetSidebar } from './CnFacetSidebar/index.js'
14
- export { CnRowActions } from './CnRowActions/index.js'
15
- export { CnIndexPage } from './CnIndexPage/index.js'
16
- export { CnMassActionBar } from './CnMassActionBar/index.js'
17
- export { CnDeleteDialog } from './CnDeleteDialog/index.js'
18
- export { CnCopyDialog } from './CnCopyDialog/index.js'
19
- export { CnFormDialog } from './CnFormDialog/index.js'
20
- export { CnMassDeleteDialog } from './CnMassDeleteDialog/index.js'
21
- export { CnMassCopyDialog } from './CnMassCopyDialog/index.js'
22
- export { CnKpiGrid } from './CnKpiGrid/index.js'
23
- export { CnMassExportDialog } from './CnMassExportDialog/index.js'
24
- export { CnMassImportDialog } from './CnMassImportDialog/index.js'
25
- export { CnIndexSidebar } from './CnIndexSidebar/index.js'
26
- export { CnRegisterMapping } from './CnRegisterMapping/index.js'
27
- export { CnIcon, ICON_MAP, registerIcons } from './CnIcon/index.js'
28
- export { CnPageHeader } from './CnPageHeader/index.js'
29
- export { CnActionsBar } from './CnActionsBar/index.js'
1
+ export { CnDataTable } from './CnDataTable/index.js'
2
+ export { CnFilterBar } from './CnFilterBar/index.js'
3
+ export { CnStatusBadge } from './CnStatusBadge/index.js'
4
+ export { CnPagination } from './CnPagination/index.js'
5
+ export { CnSettingsCard } from './CnSettingsCard/index.js'
6
+ export { CnSettingsSection } from './CnSettingsSection/index.js'
7
+ export { CnStatsBlock } from './CnStatsBlock/index.js'
8
+ export { CnConfigurationCard } from './CnConfigurationCard/index.js'
9
+ export { CnVersionInfoCard } from './CnVersionInfoCard/index.js'
10
+ export { CnCellRenderer } from './CnCellRenderer/index.js'
11
+ export { CnObjectCard } from './CnObjectCard/index.js'
12
+ export { CnCardGrid } from './CnCardGrid/index.js'
13
+ export { CnFacetSidebar } from './CnFacetSidebar/index.js'
14
+ export { CnRowActions } from './CnRowActions/index.js'
15
+ export { CnIndexPage } from './CnIndexPage/index.js'
16
+ export { CnMassActionBar } from './CnMassActionBar/index.js'
17
+ export { CnDeleteDialog } from './CnDeleteDialog/index.js'
18
+ export { CnCopyDialog } from './CnCopyDialog/index.js'
19
+ export { CnFormDialog } from './CnFormDialog/index.js'
20
+ export { CnAdvancedFormDialog } from './CnAdvancedFormDialog/index.js'
21
+ export { CnMassDeleteDialog } from './CnMassDeleteDialog/index.js'
22
+ export { CnMassCopyDialog } from './CnMassCopyDialog/index.js'
23
+ export { CnKpiGrid } from './CnKpiGrid/index.js'
24
+ export { CnMassExportDialog } from './CnMassExportDialog/index.js'
25
+ export { CnMassImportDialog } from './CnMassImportDialog/index.js'
26
+ export { CnIndexSidebar } from './CnIndexSidebar/index.js'
27
+ export { CnRegisterMapping } from './CnRegisterMapping/index.js'
28
+ export { CnIcon, ICON_MAP, registerIcons } from './CnIcon/index.js'
29
+ export { CnPageHeader } from './CnPageHeader/index.js'
30
+ export { CnActionsBar } from './CnActionsBar/index.js'
31
+ export { CnDetailPage } from './CnDetailPage/index.js'
32
+ export { CnDashboardPage } from './CnDashboardPage/index.js'
33
+ export { CnDashboardGrid } from './CnDashboardGrid/index.js'
34
+ export { CnWidgetWrapper } from './CnWidgetWrapper/index.js'
35
+ export { CnWidgetRenderer } from './CnWidgetRenderer/index.js'
36
+ export { CnTileWidget } from './CnTileWidget/index.js'
37
+ export { CnItemCard } from './CnItemCard/index.js'
38
+ export { CnSchemaFormDialog } from './CnSchemaFormDialog/index.js'
39
+ export { CnTabbedFormDialog } from './CnTabbedFormDialog/index.js'
40
+ export { CnTimelineStages } from './CnTimelineStages/index.js'
41
+ export { CnUserActionMenu } from './CnUserActionMenu/index.js'
42
+ export { CnNotesCard } from './CnNotesCard/index.js'
43
+ export { CnTasksCard } from './CnTasksCard/index.js'
@@ -1,3 +1,4 @@
1
- export { useListView } from './useListView.js'
2
- export { useDetailView } from './useDetailView.js'
3
- export { useSubResource } from './useSubResource.js'
1
+ export { useListView } from './useListView.js'
2
+ export { useDetailView } from './useDetailView.js'
3
+ export { useSubResource } from './useSubResource.js'
4
+ export { useDashboardView } from './useDashboardView.js'
@@ -0,0 +1,240 @@
1
+ import { ref, computed, onMounted } from 'vue'
2
+ import axios from '@nextcloud/axios'
3
+ import { generateOcsUrl } from '@nextcloud/router'
4
+
5
+ /**
6
+ * Composable for managing dashboard view state.
7
+ *
8
+ * Handles widget definition loading (including NC Dashboard API widgets),
9
+ * layout management, and edit mode. Apps provide their own widget
10
+ * definitions and persist layouts however they choose (app config,
11
+ * OpenRegister objects, etc.).
12
+ *
13
+ * @param {object} [options] Configuration options
14
+ * @param {Array} [options.widgets=[]] Static widget definitions from the app
15
+ * @param {Array} [options.defaultLayout=[]] Default layout if no saved layout exists
16
+ * @param {Function} [options.loadLayout] Async function that returns saved layout array, or null
17
+ * @param {Function} [options.saveLayout] Async function that persists layout: (layout) => Promise
18
+ * @param {boolean} [options.includeNcWidgets=false] Whether to also load NC Dashboard API widgets
19
+ * @param {number} [options.columns=12] Grid columns
20
+ * @return {object} Reactive state and methods for CnDashboardPage
21
+ *
22
+ * @example Basic usage with static widgets
23
+ * const { widgets, layout, loading, onLayoutChange } = useDashboardView({
24
+ * widgets: [
25
+ * { id: 'kpis', title: 'KPIs', type: 'custom' },
26
+ * { id: 'chart', title: 'Status Chart', type: 'custom' },
27
+ * ],
28
+ * defaultLayout: [
29
+ * { id: 1, widgetId: 'kpis', gridX: 0, gridY: 0, gridWidth: 12, gridHeight: 2 },
30
+ * { id: 2, widgetId: 'chart', gridX: 0, gridY: 2, gridWidth: 6, gridHeight: 4 },
31
+ * ],
32
+ * })
33
+ *
34
+ * @example With persistence and NC widgets
35
+ * const dashboard = useDashboardView({
36
+ * widgets: myWidgets,
37
+ * defaultLayout: defaultLayout,
38
+ * loadLayout: () => fetch('/api/dashboard-layout').then(r => r.json()),
39
+ * saveLayout: (layout) => fetch('/api/dashboard-layout', { method: 'PUT', body: JSON.stringify(layout) }),
40
+ * includeNcWidgets: true,
41
+ * })
42
+ */
43
+ export function useDashboardView(options = {}) {
44
+ const opts = {
45
+ widgets: [],
46
+ defaultLayout: [],
47
+ loadLayout: null,
48
+ saveLayout: null,
49
+ includeNcWidgets: false,
50
+ columns: 12,
51
+ ...options,
52
+ }
53
+
54
+ // ── State ────────────────────────────────────────────────────────────
55
+ const appWidgets = ref(opts.widgets)
56
+ const ncWidgets = ref([])
57
+ const layout = ref([])
58
+ const loading = ref(false)
59
+ const saving = ref(false)
60
+ const isEditing = ref(false)
61
+
62
+ // ── Computed ─────────────────────────────────────────────────────────
63
+
64
+ /** All available widgets (app + NC Dashboard API) */
65
+ const widgets = computed(() => {
66
+ return [...appWidgets.value, ...ncWidgets.value]
67
+ })
68
+
69
+ /** Widget IDs currently on the dashboard */
70
+ const activeWidgetIds = computed(() => {
71
+ return layout.value.map(item => item.widgetId)
72
+ })
73
+
74
+ /** Widgets not yet placed on the dashboard */
75
+ const availableWidgets = computed(() => {
76
+ return widgets.value.filter(w => !activeWidgetIds.value.includes(w.id))
77
+ })
78
+
79
+ // ── Methods ──────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Load NC Dashboard API widgets from the OCS endpoint.
83
+ */
84
+ async function loadNcWidgets() {
85
+ try {
86
+ const url = generateOcsUrl('/apps/dashboard/api/v1/widgets')
87
+ const response = await axios.get(url)
88
+ const data = response.data?.ocs?.data || {}
89
+
90
+ ncWidgets.value = Object.values(data).map(w => ({
91
+ id: w.id,
92
+ title: w.title,
93
+ iconClass: w.icon_class,
94
+ iconUrl: w.icon_url,
95
+ widgetUrl: w.widget_url,
96
+ itemApiVersions: w.item_api_versions || [],
97
+ itemIconsRound: w.item_icons_round || false,
98
+ reloadInterval: w.reload_interval || 0,
99
+ buttons: w.buttons || [],
100
+ type: 'nc-widget',
101
+ }))
102
+ } catch (error) {
103
+ console.error('[useDashboardView] Failed to load NC widgets:', error)
104
+ ncWidgets.value = []
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Initialize the dashboard: load layout and optionally NC widgets.
110
+ */
111
+ async function init() {
112
+ loading.value = true
113
+ try {
114
+ const tasks = []
115
+
116
+ if (opts.includeNcWidgets) {
117
+ tasks.push(loadNcWidgets())
118
+ }
119
+
120
+ if (opts.loadLayout) {
121
+ tasks.push(
122
+ opts.loadLayout().then(saved => {
123
+ if (saved && saved.length > 0) {
124
+ layout.value = saved
125
+ } else {
126
+ layout.value = [...opts.defaultLayout]
127
+ }
128
+ }),
129
+ )
130
+ } else {
131
+ layout.value = [...opts.defaultLayout]
132
+ }
133
+
134
+ await Promise.all(tasks)
135
+ } catch (error) {
136
+ console.error('[useDashboardView] Init failed:', error)
137
+ layout.value = [...opts.defaultLayout]
138
+ } finally {
139
+ loading.value = false
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Handle layout change from the grid. Persists if saveLayout is provided.
145
+ *
146
+ * @param {Array} newLayout Updated layout array
147
+ */
148
+ async function onLayoutChange(newLayout) {
149
+ layout.value = newLayout
150
+
151
+ if (opts.saveLayout) {
152
+ saving.value = true
153
+ try {
154
+ await opts.saveLayout(newLayout)
155
+ } catch (error) {
156
+ console.error('[useDashboardView] Failed to save layout:', error)
157
+ } finally {
158
+ saving.value = false
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Add a widget to the dashboard at the next available position.
165
+ *
166
+ * @param {string} widgetId Widget ID to add
167
+ * @param {object} [position] Override position { gridX, gridY, gridWidth, gridHeight }
168
+ */
169
+ function addWidget(widgetId, position = {}) {
170
+ const maxId = layout.value.reduce((max, item) => {
171
+ const num = typeof item.id === 'number' ? item.id : 0
172
+ return num > max ? num : max
173
+ }, 0)
174
+
175
+ const maxY = layout.value.reduce((max, item) => {
176
+ const bottom = (item.gridY || 0) + (item.gridHeight || 3)
177
+ return bottom > max ? bottom : max
178
+ }, 0)
179
+
180
+ const newItem = {
181
+ id: maxId + 1,
182
+ widgetId,
183
+ gridX: position.gridX ?? 0,
184
+ gridY: position.gridY ?? maxY,
185
+ gridWidth: position.gridWidth ?? 6,
186
+ gridHeight: position.gridHeight ?? 3,
187
+ }
188
+
189
+ const newLayout = [...layout.value, newItem]
190
+ onLayoutChange(newLayout)
191
+ }
192
+
193
+ /**
194
+ * Remove a widget from the dashboard by layout item ID.
195
+ *
196
+ * @param {string|number} itemId Layout item ID to remove
197
+ */
198
+ function removeWidget(itemId) {
199
+ const newLayout = layout.value.filter(item => item.id !== itemId)
200
+ onLayoutChange(newLayout)
201
+ }
202
+
203
+ /**
204
+ * Update app widget definitions (e.g., when data changes).
205
+ *
206
+ * @param {Array} newWidgets Updated widget definitions
207
+ */
208
+ function setWidgets(newWidgets) {
209
+ appWidgets.value = newWidgets
210
+ }
211
+
212
+ // ── Lifecycle ────────────────────────────────────────────────────────
213
+
214
+ onMounted(() => {
215
+ init()
216
+ })
217
+
218
+ // ── Return ───────────────────────────────────────────────────────────
219
+
220
+ return {
221
+ // State
222
+ widgets,
223
+ layout,
224
+ loading,
225
+ saving,
226
+ isEditing,
227
+
228
+ // Derived
229
+ activeWidgetIds,
230
+ availableWidgets,
231
+ ncWidgets,
232
+
233
+ // Methods
234
+ onLayoutChange,
235
+ addWidget,
236
+ removeWidget,
237
+ setWidgets,
238
+ init,
239
+ }
240
+ }