@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.10

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 (197) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs +67614 -0
  4. package/dist/nextcloud-vue.cjs.js +76311 -5905
  5. package/dist/nextcloud-vue.cjs.js.map +1 -1
  6. package/dist/nextcloud-vue.cjs.map +1 -0
  7. package/dist/nextcloud-vue.css +3279 -203
  8. package/dist/nextcloud-vue.esm.js +76240 -5882
  9. package/dist/nextcloud-vue.esm.js.map +1 -1
  10. package/package.json +89 -63
  11. package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
  12. package/src/components/CnActionsBar/index.js +1 -0
  13. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +569 -0
  14. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  15. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
  17. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  18. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  19. package/src/components/CnCard/CnCard.vue +415 -0
  20. package/src/components/CnCard/index.js +1 -0
  21. package/src/components/CnCardGrid/CnCardGrid.vue +23 -20
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/index.js +1 -1
  24. package/src/components/CnChartWidget/CnChartWidget.vue +318 -0
  25. package/src/components/CnChartWidget/index.js +1 -0
  26. package/src/components/CnConfigurationCard/index.js +1 -1
  27. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  28. package/src/components/CnContextMenu/index.js +1 -0
  29. package/src/components/CnCopyDialog/CnCopyDialog.vue +257 -0
  30. package/src/components/CnCopyDialog/index.js +1 -0
  31. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  32. package/src/components/CnDashboardGrid/index.js +1 -0
  33. package/src/components/CnDashboardPage/CnDashboardPage.vue +396 -0
  34. package/src/components/CnDashboardPage/index.js +1 -0
  35. package/src/components/CnDataTable/CnDataTable.vue +24 -16
  36. package/src/components/CnDataTable/index.js +1 -1
  37. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  38. package/src/components/CnDeleteDialog/index.js +1 -0
  39. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  40. package/src/components/CnDetailCard/index.js +1 -0
  41. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  42. package/src/components/CnDetailGrid/index.js +1 -0
  43. package/src/components/CnDetailPage/CnDetailPage.vue +431 -0
  44. package/src/components/CnDetailPage/index.js +1 -0
  45. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +12 -2
  46. package/src/components/CnFacetSidebar/index.js +1 -1
  47. package/src/components/CnFilterBar/index.js +1 -1
  48. package/src/components/CnFormDialog/CnFormDialog.vue +934 -0
  49. package/src/components/CnFormDialog/index.js +1 -0
  50. package/src/components/CnIcon/CnIcon.vue +89 -0
  51. package/src/components/CnIcon/index.js +1 -0
  52. package/src/components/CnIndexPage/CnIndexPage.vue +589 -291
  53. package/src/components/CnIndexPage/index.js +1 -1
  54. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +535 -0
  55. package/src/components/CnIndexSidebar/index.js +1 -0
  56. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  57. package/src/components/CnInfoWidget/index.js +1 -0
  58. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  59. package/src/components/CnItemCard/index.js +1 -0
  60. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  61. package/src/components/CnJsonViewer/index.js +1 -0
  62. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  63. package/src/components/CnKpiGrid/index.js +1 -1
  64. package/src/components/CnMassActionBar/CnMassActionBar.vue +6 -5
  65. package/src/components/CnMassActionBar/index.js +1 -1
  66. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +16 -9
  67. package/src/components/CnMassCopyDialog/index.js +1 -1
  68. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +16 -9
  69. package/src/components/CnMassDeleteDialog/index.js +1 -1
  70. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +8 -7
  71. package/src/components/CnMassExportDialog/index.js +1 -1
  72. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +20 -17
  73. package/src/components/CnMassImportDialog/index.js +1 -1
  74. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  75. package/src/components/CnNoteCard/index.js +1 -0
  76. package/src/components/CnNotesCard/CnNotesCard.vue +415 -0
  77. package/src/components/CnNotesCard/index.js +1 -0
  78. package/src/components/CnObjectCard/CnObjectCard.vue +3 -1
  79. package/src/components/CnObjectCard/index.js +1 -1
  80. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +853 -0
  81. package/src/components/CnObjectDataWidget/index.js +1 -0
  82. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +288 -0
  83. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  84. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  85. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  86. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  87. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +254 -0
  88. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  89. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  90. package/src/components/CnObjectSidebar/index.js +6 -0
  91. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  92. package/src/components/CnPageHeader/index.js +1 -0
  93. package/src/components/CnPagination/CnPagination.vue +7 -6
  94. package/src/components/CnPagination/index.js +1 -1
  95. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  96. package/src/components/CnProgressBar/index.js +1 -0
  97. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
  98. package/src/components/CnRegisterMapping/index.js +1 -0
  99. package/src/components/CnRowActions/CnRowActions.vue +25 -3
  100. package/src/components/CnRowActions/index.js +1 -1
  101. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  102. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  103. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  104. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  106. package/src/components/CnSchemaFormDialog/index.js +1 -0
  107. package/src/components/CnSettingsCard/index.js +1 -1
  108. package/src/components/CnSettingsSection/index.js +1 -1
  109. package/src/components/CnStatsBlock/CnStatsBlock.vue +89 -19
  110. package/src/components/CnStatsBlock/index.js +1 -1
  111. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  112. package/src/components/CnStatsPanel/index.js +1 -0
  113. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  114. package/src/components/CnStatusBadge/index.js +1 -1
  115. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +544 -0
  116. package/src/components/CnTabbedFormDialog/index.js +1 -0
  117. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  118. package/src/components/CnTableWidget/index.js +1 -0
  119. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  120. package/src/components/CnTasksCard/index.js +1 -0
  121. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  122. package/src/components/CnTileWidget/index.js +1 -0
  123. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  124. package/src/components/CnTimelineStages/index.js +1 -0
  125. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  126. package/src/components/CnUserActionMenu/index.js +1 -0
  127. package/src/components/CnVersionInfoCard/index.js +1 -1
  128. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  129. package/src/components/CnWidgetRenderer/index.js +1 -0
  130. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +246 -0
  131. package/src/components/CnWidgetWrapper/index.js +1 -0
  132. package/src/components/index.js +57 -25
  133. package/src/composables/index.js +5 -3
  134. package/src/composables/useContextMenu.js +126 -0
  135. package/src/composables/useDashboardView.js +286 -0
  136. package/src/composables/useDetailView.js +290 -132
  137. package/src/composables/useListView.js +364 -153
  138. package/src/composables/useSubResource.js +142 -142
  139. package/src/constants/metadata.js +30 -0
  140. package/src/css/CnSchemaFormDialog.css +546 -0
  141. package/src/css/__sample_nextcloud_tokens.css +110 -0
  142. package/src/css/actions-bar.css +54 -0
  143. package/src/css/badge.css +83 -51
  144. package/src/css/card.css +129 -128
  145. package/src/css/context-menu.css +20 -0
  146. package/src/css/dashboard.css +70 -0
  147. package/src/css/detail-page.css +235 -0
  148. package/src/css/detail.css +68 -68
  149. package/src/css/index-page.css +44 -0
  150. package/src/css/index-sidebar.css +193 -0
  151. package/src/css/index.css +17 -8
  152. package/src/css/layout.css +90 -90
  153. package/src/css/page-header.css +35 -0
  154. package/src/css/pagination.css +72 -72
  155. package/src/css/table.css +142 -143
  156. package/src/css/timeline-stages.css +220 -0
  157. package/src/css/utilities.css +46 -46
  158. package/src/index.js +91 -50
  159. package/src/mixins/gridLayout.js +118 -0
  160. package/src/store/createCrudStore.js +360 -0
  161. package/src/store/createSubResourcePlugin.js +125 -135
  162. package/src/store/index.js +4 -3
  163. package/src/store/plugins/auditTrails.js +357 -17
  164. package/src/store/plugins/files.js +250 -186
  165. package/src/store/plugins/index.js +7 -4
  166. package/src/store/plugins/lifecycle.js +180 -180
  167. package/src/store/plugins/registerMapping.js +195 -0
  168. package/src/store/plugins/relations.js +68 -68
  169. package/src/store/plugins/search.js +385 -0
  170. package/src/store/plugins/selection.js +104 -0
  171. package/src/store/useObjectStore.js +823 -625
  172. package/src/types/auditTrail.d.ts +32 -32
  173. package/src/types/file.d.ts +23 -23
  174. package/src/types/index.d.ts +35 -35
  175. package/src/types/notification.d.ts +36 -36
  176. package/src/types/object.d.ts +40 -40
  177. package/src/types/organisation.d.ts +41 -41
  178. package/src/types/register.d.ts +25 -25
  179. package/src/types/schema.d.ts +39 -39
  180. package/src/types/shared.d.ts +79 -79
  181. package/src/types/source.d.ts +14 -14
  182. package/src/types/task.d.ts +31 -31
  183. package/src/utils/errors.js +96 -96
  184. package/src/utils/getTheme.js +9 -0
  185. package/src/utils/headers.js +80 -44
  186. package/src/utils/id.js +13 -0
  187. package/src/utils/index.js +4 -3
  188. package/src/utils/schema.js +422 -287
  189. package/src/utils/widgetVisibility.js +162 -0
  190. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  191. package/src/components/CnDetailViewLayout/index.js +0 -1
  192. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  193. package/src/components/CnEmptyState/index.js +0 -1
  194. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  195. package/src/components/CnListViewLayout/index.js +0 -1
  196. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  197. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -0,0 +1,396 @@
1
+ <!--
2
+ CnDashboardPage — Top-level dashboard page with GridStack widget grid.
3
+
4
+ The dashboard equivalent of CnIndexPage. Assembles a complete dashboard
5
+ from a widget definition array and a layout array. Supports:
6
+ - Custom widgets via scoped slots (#widget-{widgetId})
7
+ - Nextcloud Dashboard API widgets (auto-rendered)
8
+ - Tile widgets (quick-access links)
9
+ - Drag-and-drop editing mode
10
+ - Header with title, actions, and edit toggle
11
+ -->
12
+ <template>
13
+ <div class="cn-dashboard-page">
14
+ <!-- Header -->
15
+ <div class="cn-dashboard-page__header">
16
+ <div class="cn-dashboard-page__header-left">
17
+ <h2 v-if="title" class="cn-dashboard-page__title">
18
+ {{ title }}
19
+ </h2>
20
+ <p v-if="description" class="cn-dashboard-page__description">
21
+ {{ description }}
22
+ </p>
23
+ </div>
24
+ <div class="cn-dashboard-page__header-actions">
25
+ <slot name="actions" />
26
+ <NcButton
27
+ v-if="allowEdit"
28
+ :type="isEditing ? 'primary' : 'secondary'"
29
+ @click="toggleEdit">
30
+ <template #icon>
31
+ <Pencil v-if="!isEditing" :size="20" />
32
+ <Check v-else :size="20" />
33
+ </template>
34
+ {{ isEditing ? doneLabel : editLabel }}
35
+ </NcButton>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Loading state -->
40
+ <NcLoadingIcon v-if="loading" />
41
+
42
+ <!-- Empty state -->
43
+ <div v-else-if="!hasWidgets" class="cn-dashboard-page__empty">
44
+ <slot name="empty">
45
+ <NcEmptyContent :description="emptyLabel">
46
+ <template #icon>
47
+ <ViewDashboardOutline :size="48" />
48
+ </template>
49
+ </NcEmptyContent>
50
+ </slot>
51
+ </div>
52
+
53
+ <!-- Dashboard grid -->
54
+ <CnDashboardGrid
55
+ v-else
56
+ :layout="layout"
57
+ :editable="isEditing"
58
+ :columns="columns"
59
+ :cell-height="cellHeight"
60
+ :margin="gridMargin"
61
+ @layout-change="onLayoutChange">
62
+ <template #widget="{ item }">
63
+ <!-- Tile widget -->
64
+ <CnTileWidget
65
+ v-if="isTile(item)"
66
+ :tile="getTileConfig(item)" />
67
+
68
+ <!-- Custom slot widget — apps provide their own rendering -->
69
+ <template v-else-if="hasWidgetSlot(item.widgetId)">
70
+ <CnWidgetWrapper
71
+ :title="getWidgetTitle(item)"
72
+ :icon-url="getWidgetIconUrl(item)"
73
+ :icon-class="getWidgetIconClass(item)"
74
+ :show-title="item.showTitle !== false"
75
+ :borderless="item.showTitle === false"
76
+ :flush="item.flush === true"
77
+ :buttons="getWidgetButtons(item)"
78
+ :style-config="item.styleConfig || {}">
79
+ <!-- Per-widget header actions (e.g. #widget-my-work-actions) -->
80
+ <template v-if="$slots['widget-' + item.widgetId + '-actions']" #actions>
81
+ <slot :name="'widget-' + item.widgetId + '-actions'" :item="item" :widget="getWidgetDef(item.widgetId)" />
82
+ </template>
83
+ <slot :name="'widget-' + item.widgetId" :item="item" :widget="getWidgetDef(item.widgetId)" />
84
+ </CnWidgetWrapper>
85
+ </template>
86
+
87
+ <!-- NC Dashboard API widget -->
88
+ <template v-else-if="isNcWidget(item)">
89
+ <CnWidgetWrapper
90
+ :title="getWidgetTitle(item)"
91
+ :icon-url="getWidgetIconUrl(item)"
92
+ :icon-class="getWidgetIconClass(item)"
93
+ :show-title="item.showTitle !== false"
94
+ :buttons="getWidgetButtons(item)"
95
+ :style-config="item.styleConfig || {}">
96
+ <CnWidgetRenderer
97
+ :widget="getWidgetDef(item.widgetId)"
98
+ :unavailable-text="unavailableLabel" />
99
+ </CnWidgetWrapper>
100
+ </template>
101
+
102
+ <!-- Unknown widget fallback -->
103
+ <CnWidgetWrapper
104
+ v-else
105
+ :title="getWidgetTitle(item)"
106
+ :show-title="item.showTitle !== false">
107
+ <div class="cn-dashboard-page__unknown">
108
+ {{ unavailableLabel }}
109
+ </div>
110
+ </CnWidgetWrapper>
111
+ </template>
112
+ </CnDashboardGrid>
113
+ </div>
114
+ </template>
115
+
116
+ <script>
117
+ import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
118
+ import Pencil from 'vue-material-design-icons/Pencil.vue'
119
+ import Check from 'vue-material-design-icons/Check.vue'
120
+ import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
121
+ import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
122
+ import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
123
+ import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
124
+ import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
125
+
126
+ /**
127
+ * CnDashboardPage — Top-level dashboard page component.
128
+ *
129
+ * The dashboard equivalent of CnIndexPage. Renders a configurable grid
130
+ * of widgets from a `widgets` definition array and a `layout` array.
131
+ *
132
+ * Widget types:
133
+ * 1. **Custom** — App provides rendering via `#widget-{widgetId}` slot
134
+ * 2. **NC Dashboard API** — Widgets with `itemApiVersions` are auto-rendered
135
+ * 3. **Tile** — Items with `type: 'tile'` render as quick-access tiles
136
+ *
137
+ * @example Basic usage with custom widgets
138
+ * <CnDashboardPage
139
+ * title="Dashboard"
140
+ * :widgets="widgetDefs"
141
+ * :layout="savedLayout"
142
+ * @layout-change="saveLayout">
143
+ * <template #widget-cases-by-status="{ item }">
144
+ * <StatusChart :data="statusData" />
145
+ * </template>
146
+ * <template #widget-my-work="{ item }">
147
+ * <MyWorkList :items="workItems" />
148
+ * </template>
149
+ * </CnDashboardPage>
150
+ *
151
+ * @example With NC Dashboard API widgets
152
+ * <CnDashboardPage
153
+ * title="Dashboard"
154
+ * :widgets="[...appWidgets, ...ncWidgets]"
155
+ * :layout="layout"
156
+ * @layout-change="saveLayout" />
157
+ */
158
+ export default {
159
+ name: 'CnDashboardPage',
160
+
161
+ components: {
162
+ NcButton,
163
+ NcEmptyContent,
164
+ NcLoadingIcon,
165
+ Pencil,
166
+ Check,
167
+ ViewDashboardOutline,
168
+ CnDashboardGrid,
169
+ CnWidgetWrapper,
170
+ CnWidgetRenderer,
171
+ CnTileWidget,
172
+ },
173
+
174
+ props: {
175
+ /** Page title */
176
+ title: {
177
+ type: String,
178
+ default: '',
179
+ },
180
+ /** Page description (shown below title) */
181
+ description: {
182
+ type: String,
183
+ default: '',
184
+ },
185
+ /**
186
+ * Widget definitions array. Each widget defines metadata for rendering.
187
+ *
188
+ * Custom widgets: `{ id: 'my-widget', title: 'My Widget', type: 'custom' }`
189
+ * NC API widgets: `{ id: 'calendar', title: 'Calendar', itemApiVersions: [1,2], ... }`
190
+ * Tile widgets: `{ id: 'tile-files', type: 'tile', title: 'Files', icon: 'M12...', iconType: 'svg', backgroundColor: '#0082c9', textColor: '#fff', linkType: 'app', linkValue: 'files' }`
191
+ *
192
+ * @type {Array<{ id: string, title: string, type?: string, iconUrl?: string, iconClass?: string, buttons?: Array, itemApiVersions?: number[], reloadInterval?: number, [key: string]: any }>}
193
+ */
194
+ widgets: {
195
+ type: Array,
196
+ default: () => [],
197
+ },
198
+ /**
199
+ * Layout array defining widget positions in the grid.
200
+ *
201
+ * Each item: `{ id: 'unique-id', widgetId: 'my-widget', gridX: 0, gridY: 0, gridWidth: 4, gridHeight: 3 }`
202
+ *
203
+ * Additional properties (showTitle, styleConfig, tile config) are passed through.
204
+ *
205
+ * @type {Array<{ id: string|number, widgetId: string, gridX: number, gridY: number, gridWidth: number, gridHeight: number, showTitle?: boolean, styleConfig?: object, [key: string]: any }>}
206
+ */
207
+ layout: {
208
+ type: Array,
209
+ default: () => [],
210
+ },
211
+ /** Whether the dashboard is loading */
212
+ loading: {
213
+ type: Boolean,
214
+ default: false,
215
+ },
216
+ /** Whether to show the edit toggle button */
217
+ allowEdit: {
218
+ type: Boolean,
219
+ default: false,
220
+ },
221
+ /** Number of grid columns */
222
+ columns: {
223
+ type: Number,
224
+ default: 12,
225
+ },
226
+ /** Grid cell height in pixels */
227
+ cellHeight: {
228
+ type: Number,
229
+ default: 80,
230
+ },
231
+ /** Grid margin in pixels */
232
+ gridMargin: {
233
+ type: Number,
234
+ default: 12,
235
+ },
236
+ /** Label for the edit button */
237
+ editLabel: {
238
+ type: String,
239
+ default: 'Edit',
240
+ },
241
+ /** Label for the done button (when editing) */
242
+ doneLabel: {
243
+ type: String,
244
+ default: 'Done',
245
+ },
246
+ /** Label for the empty state */
247
+ emptyLabel: {
248
+ type: String,
249
+ default: 'No widgets configured',
250
+ },
251
+ /** Label for unavailable widgets */
252
+ unavailableLabel: {
253
+ type: String,
254
+ default: 'Widget not available',
255
+ },
256
+ },
257
+
258
+ emits: ['layout-change', 'edit-toggle'],
259
+
260
+ data() {
261
+ return {
262
+ isEditing: false,
263
+ }
264
+ },
265
+
266
+ computed: {
267
+ hasWidgets() {
268
+ return this.layout.length > 0
269
+ },
270
+
271
+ widgetMap() {
272
+ const map = {}
273
+ for (const w of this.widgets) {
274
+ map[w.id] = w
275
+ }
276
+ return map
277
+ },
278
+ },
279
+
280
+ methods: {
281
+ toggleEdit() {
282
+ this.isEditing = !this.isEditing
283
+ this.$emit('edit-toggle', this.isEditing)
284
+ },
285
+
286
+ onLayoutChange(updated) {
287
+ this.$emit('layout-change', updated)
288
+ },
289
+
290
+ getWidgetDef(widgetId) {
291
+ return this.widgetMap[widgetId] || null
292
+ },
293
+
294
+ getWidgetTitle(item) {
295
+ const def = this.getWidgetDef(item.widgetId)
296
+ return item.customTitle || def?.title || item.widgetId
297
+ },
298
+
299
+ getWidgetIconUrl(item) {
300
+ const def = this.getWidgetDef(item.widgetId)
301
+ return def?.iconUrl || null
302
+ },
303
+
304
+ getWidgetIconClass(item) {
305
+ const def = this.getWidgetDef(item.widgetId)
306
+ return def?.iconClass || null
307
+ },
308
+
309
+ getWidgetButtons(item) {
310
+ const def = this.getWidgetDef(item.widgetId)
311
+ return def?.buttons || []
312
+ },
313
+
314
+ isTile(item) {
315
+ const def = this.getWidgetDef(item.widgetId)
316
+ return def?.type === 'tile'
317
+ },
318
+
319
+ getTileConfig(item) {
320
+ const def = this.getWidgetDef(item.widgetId)
321
+ if (!def) return null
322
+ return {
323
+ title: def.title,
324
+ icon: def.icon,
325
+ iconType: def.iconType,
326
+ backgroundColor: def.backgroundColor,
327
+ textColor: def.textColor,
328
+ linkType: def.linkType,
329
+ linkValue: def.linkValue,
330
+ }
331
+ },
332
+
333
+ isNcWidget(item) {
334
+ const def = this.getWidgetDef(item.widgetId)
335
+ return def?.itemApiVersions && def.itemApiVersions.length > 0
336
+ },
337
+
338
+ hasWidgetSlot(widgetId) {
339
+ return !!this.$scopedSlots['widget-' + widgetId]
340
+ },
341
+ },
342
+ }
343
+ </script>
344
+
345
+ <style scoped>
346
+ .cn-dashboard-page {
347
+ padding: 20px;
348
+ max-width: 1400px;
349
+ }
350
+
351
+ .cn-dashboard-page__header {
352
+ display: flex;
353
+ justify-content: space-between;
354
+ align-items: flex-start;
355
+ margin-bottom: 20px;
356
+ flex-wrap: wrap;
357
+ gap: 12px;
358
+ }
359
+
360
+ .cn-dashboard-page__header-left {
361
+ min-width: 0;
362
+ }
363
+
364
+ .cn-dashboard-page__title {
365
+ margin: 0;
366
+ font-size: 20px;
367
+ font-weight: 700;
368
+ }
369
+
370
+ .cn-dashboard-page__description {
371
+ margin: 4px 0 0;
372
+ font-size: 14px;
373
+ color: var(--color-text-maxcontrast);
374
+ }
375
+
376
+ .cn-dashboard-page__header-actions {
377
+ display: flex;
378
+ gap: 8px;
379
+ flex-wrap: wrap;
380
+ flex-shrink: 0;
381
+ }
382
+
383
+ .cn-dashboard-page__empty {
384
+ padding: 60px 20px;
385
+ }
386
+
387
+ .cn-dashboard-page__unknown {
388
+ display: flex;
389
+ align-items: center;
390
+ justify-content: center;
391
+ height: 100%;
392
+ color: var(--color-text-maxcontrast);
393
+ font-size: 14px;
394
+ padding: 16px;
395
+ }
396
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnDashboardPage } from './CnDashboardPage.vue'
@@ -38,7 +38,7 @@
38
38
 
39
39
  <!-- Actions column -->
40
40
  <th v-if="$scopedSlots['row-actions']" class="cn-table-col--actions">
41
- <!-- Actions header intentionally empty -->
41
+ <slot name="actions-header" />
42
42
  </th>
43
43
  </tr>
44
44
  </thead>
@@ -63,7 +63,8 @@
63
63
  isSelected(row) ? 'cn-table-row--selected' : '',
64
64
  rowClass ? rowClass(row) : '',
65
65
  ]"
66
- @click="$emit('row-click', row)">
66
+ @click="$emit('row-click', row)"
67
+ @contextmenu.prevent="$emit('row-context-menu', { row, event: $event })">
67
68
  <!-- Checkbox -->
68
69
  <td v-if="selectable" class="cn-table-col--checkbox" @click.stop>
69
70
  <NcCheckboxRadioSwitch
@@ -101,6 +102,7 @@
101
102
  </template>
102
103
 
103
104
  <script>
105
+ import { translate as t } from '@nextcloud/l10n'
104
106
  import { NcLoadingIcon, NcCheckboxRadioSwitch } from '@nextcloud/vue'
105
107
  import { CnCellRenderer } from '../CnCellRenderer/index.js'
106
108
  import { columnsFromSchema } from '../../utils/schema.js'
@@ -118,11 +120,6 @@ import { columnsFromSchema } from '../../utils/schema.js'
118
120
  * (dates, booleans, UUIDs, enums, etc.). Scoped slots still override individual
119
121
  * columns when needed.
120
122
  *
121
- * NL Design tokens used:
122
- * - --nldesign-component-table-header-background-color
123
- * - --nldesign-component-table-row-hover-background-color
124
- * - --nldesign-component-table-border-color
125
- *
126
123
  * @example Manual columns (backwards compatible)
127
124
  * <CnDataTable
128
125
  * :columns="[
@@ -203,11 +200,11 @@ export default {
203
200
  type: String,
204
201
  default: null,
205
202
  },
206
- /** Current sort order: 'asc' or 'desc' */
203
+ /** Current sort order: 'asc', 'desc', or null (no sort) */
207
204
  sortOrder: {
208
205
  type: String,
209
206
  default: 'asc',
210
- validator: (v) => ['asc', 'desc'].includes(v),
207
+ validator: (v) => v === null || ['asc', 'desc'].includes(v),
211
208
  },
212
209
  /** Whether rows can be selected with checkboxes */
213
210
  selectable: {
@@ -227,7 +224,7 @@ export default {
227
224
  /** Text shown when there are no rows */
228
225
  emptyText: {
229
226
  type: String,
230
- default: 'No items found',
227
+ default: () => t('nextcloud-vue', 'No items found'),
231
228
  },
232
229
  /** Function returning CSS class(es) for a row: (row) => string|object */
233
230
  rowClass: {
@@ -242,7 +239,7 @@ export default {
242
239
  /** Text shown while loading */
243
240
  loadingText: {
244
241
  type: String,
245
- default: 'Loading...',
242
+ default: () => t('nextcloud-vue', 'Loading...'),
246
243
  },
247
244
  },
248
245
 
@@ -320,15 +317,22 @@ export default {
320
317
  * @param {string} key Column key
321
318
  */
322
319
  onSort(key) {
320
+ let newKey = key
323
321
  let order = 'asc'
324
322
  if (this.sortKey === key) {
325
- order = this.sortOrder === 'asc' ? 'desc' : 'asc'
323
+ if (this.sortOrder === 'asc') {
324
+ order = 'desc'
325
+ } else {
326
+ // desc → disabled: clear sort entirely
327
+ newKey = null
328
+ order = null
329
+ }
326
330
  }
327
331
  /**
328
332
  * @event sort Emitted when a sortable column header is clicked.
329
- * @type {{ key: string, order: 'asc'|'desc' }}
333
+ * @type {{ key: string|null, order: 'asc'|'desc'|null }}
330
334
  */
331
- this.$emit('sort', { key, order })
335
+ this.$emit('sort', { key: newKey, order })
332
336
  },
333
337
 
334
338
  toggleSelect(row) {
@@ -342,9 +346,13 @@ export default {
342
346
 
343
347
  toggleSelectAll() {
344
348
  if (this.allSelected) {
345
- this.$emit('select', [])
349
+ // Remove only current page IDs, preserving cross-page selections
350
+ const currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))
351
+ this.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))
346
352
  } else {
347
- this.$emit('select', this.rows.map((row) => row[this.rowKey]))
353
+ // Add current page IDs to existing selections
354
+ const merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])
355
+ this.$emit('select', [...merged])
348
356
  }
349
357
  /** @event select-all Emitted when select-all checkbox is toggled. */
350
358
  this.$emit('select-all', !this.allSelected)
@@ -1 +1 @@
1
- export { default as CnDataTable } from './CnDataTable.vue'
1
+ export { default as CnDataTable } from './CnDataTable.vue'
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <NcDialog
3
+ :name="dialogTitle"
4
+ size="small"
5
+ :can-close="!loading"
6
+ @closing="$emit('close')">
7
+ <!-- Result phase -->
8
+ <div v-if="result !== null" class="cn-delete__result">
9
+ <NcNoteCard v-if="result.success" type="success">
10
+ {{ successText }}
11
+ </NcNoteCard>
12
+ <NcNoteCard v-if="result.error" type="error">
13
+ {{ result.error }}
14
+ </NcNoteCard>
15
+ </div>
16
+
17
+ <!-- Confirm phase -->
18
+ <div v-else class="cn-delete__confirm">
19
+ <NcNoteCard type="warning">
20
+ {{ resolvedWarningText }}
21
+ </NcNoteCard>
22
+ </div>
23
+
24
+ <template #actions>
25
+ <NcButton @click="$emit('close')">
26
+ {{ result !== null ? closeLabel : cancelLabel }}
27
+ </NcButton>
28
+ <NcButton
29
+ v-if="result === null"
30
+ type="error"
31
+ :disabled="loading"
32
+ @click="executeDelete">
33
+ <template #icon>
34
+ <NcLoadingIcon v-if="loading" :size="20" />
35
+ <TrashCanOutline v-else :size="20" />
36
+ </template>
37
+ {{ confirmLabel }}
38
+ </NcButton>
39
+ </template>
40
+ </NcDialog>
41
+ </template>
42
+
43
+ <script>
44
+ import { translate as t } from '@nextcloud/l10n'
45
+ import { NcDialog, NcButton, NcNoteCard, NcLoadingIcon } from '@nextcloud/vue'
46
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
47
+
48
+ /**
49
+ * CnDeleteDialog — Single-item delete confirmation dialog.
50
+ *
51
+ * Two-phase UI: confirm then result. The dialog does NOT perform the delete
52
+ * itself — it emits a `confirm` event with the item ID. The parent performs
53
+ * the actual API call and calls `setResult()` via a ref.
54
+ *
55
+ * @example
56
+ * <CnDeleteDialog
57
+ * v-if="showDeleteDialog"
58
+ * ref="deleteDialog"
59
+ * :item="itemToDelete"
60
+ * @confirm="onDeleteConfirm"
61
+ * @close="showDeleteDialog = false" />
62
+ *
63
+ * // In methods:
64
+ * async onDeleteConfirm(id) {
65
+ * try {
66
+ * await store.deleteItem(id)
67
+ * this.$refs.deleteDialog.setResult({ success: true })
68
+ * } catch (e) {
69
+ * this.$refs.deleteDialog.setResult({ error: e.message })
70
+ * }
71
+ * }
72
+ */
73
+ export default {
74
+ name: 'CnDeleteDialog',
75
+
76
+ components: {
77
+ NcDialog,
78
+ NcButton,
79
+ NcNoteCard,
80
+ NcLoadingIcon,
81
+ TrashCanOutline,
82
+ },
83
+
84
+ props: {
85
+ /** The item to delete. Must have an `id` property. */
86
+ item: {
87
+ type: Object,
88
+ required: true,
89
+ },
90
+ /** Property name used for display (e.g., 'title', 'name') */
91
+ nameField: {
92
+ type: String,
93
+ default: 'title',
94
+ },
95
+ /** Optional function to format the item name. Receives the item, returns a string. Overrides nameField when provided. */
96
+ nameFormatter: {
97
+ type: Function,
98
+ default: null,
99
+ },
100
+ /** Dialog title */
101
+ dialogTitle: {
102
+ type: String,
103
+ default: () => t('nextcloud-vue', 'Delete item'),
104
+ },
105
+ /** Warning text. Use `{name}` as placeholder for the item name. */
106
+ warningText: {
107
+ type: String,
108
+ default: () => t('nextcloud-vue', 'Are you sure you want to permanently delete "{name}"? This action cannot be undone.'),
109
+ },
110
+ /** Success message */
111
+ successText: {
112
+ type: String,
113
+ default: () => t('nextcloud-vue', 'Item successfully deleted.'),
114
+ },
115
+ cancelLabel: { type: String, default: () => t('nextcloud-vue', 'Cancel') },
116
+ closeLabel: { type: String, default: () => t('nextcloud-vue', 'Close') },
117
+ confirmLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
118
+ },
119
+
120
+ data() {
121
+ return {
122
+ loading: false,
123
+ result: null,
124
+ closeTimeout: null,
125
+ }
126
+ },
127
+
128
+ computed: {
129
+ itemName() {
130
+ if (this.nameFormatter) return this.nameFormatter(this.item)
131
+ return this.item[this.nameField] || this.item.name || this.item.title || this.item.id
132
+ },
133
+ resolvedWarningText() {
134
+ return this.warningText.replace('{name}', this.itemName)
135
+ },
136
+ },
137
+
138
+ beforeDestroy() {
139
+ if (this.closeTimeout) clearTimeout(this.closeTimeout)
140
+ },
141
+
142
+ /**
143
+ * @event confirm Emitted when the user confirms deletion. Payload: the item ID.
144
+ * @event close Emitted when the dialog should be closed (cancel, close button, or auto-close after success).
145
+ */
146
+
147
+ methods: {
148
+ executeDelete() {
149
+ this.loading = true
150
+ this.$emit('confirm', this.item.id)
151
+ },
152
+
153
+ /**
154
+ * Set the result of the delete operation. Call this from the parent
155
+ * after the API call completes.
156
+ *
157
+ * @param {{ success?: boolean, error?: string }} resultData - Result data to pass to the dialog
158
+ * @public
159
+ */
160
+ setResult(resultData) {
161
+ this.loading = false
162
+ this.result = resultData
163
+ if (resultData.success) {
164
+ this.closeTimeout = setTimeout(() => {
165
+ this.$emit('close')
166
+ }, 2000)
167
+ }
168
+ },
169
+ },
170
+ }
171
+ </script>
172
+
173
+ <style scoped>
174
+ .cn-delete__confirm {
175
+ padding: 4px 0;
176
+ }
177
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnDeleteDialog } from './CnDeleteDialog.vue'