@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,236 @@
1
+ <template>
2
+ <div class="cn-schema-form__security-section">
3
+ <CnNoteCard type="info">
4
+ <p><strong>Role-Based Access Control (RBAC)</strong></p>
5
+ <p>Configure which Nextcloud user groups can perform CRUD operations on objects of this schema.</p>
6
+ <ul>
7
+ <li>If no groups are specified for an operation, all users can perform it</li>
8
+ <li>The 'admin' group always has full access (cannot be changed)</li>
9
+ <li>The object owner always has full access</li>
10
+ <li>'public' represents unauthenticated access</li>
11
+ </ul>
12
+ </CnNoteCard>
13
+
14
+ <div v-if="loadingGroups" class="cn-schema-form__loading-groups">
15
+ <NcLoadingIcon :size="20" />
16
+ <span>Loading user groups...</span>
17
+ </div>
18
+
19
+ <div v-else class="cn-schema-form__rbac-table-container">
20
+ <h3>Group Permissions</h3>
21
+ <table class="cn-schema-form__rbac-table">
22
+ <thead>
23
+ <tr>
24
+ <th>Group</th>
25
+ <th>Create</th>
26
+ <th>Read</th>
27
+ <th>Update</th>
28
+ <th>Delete</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <!-- Public group at top -->
33
+ <tr class="cn-schema-form__public-row">
34
+ <td class="cn-schema-form__group-name">
35
+ <span class="cn-schema-form__group-badge cn-schema-form__public">public</span>
36
+ <small>Unauthenticated users</small>
37
+ </td>
38
+ <td>
39
+ <NcCheckboxRadioSwitch
40
+ :checked="hasGroupPermission('public', 'create')"
41
+ @update:checked="updateGroupPermission('public', 'create', $event)" />
42
+ </td>
43
+ <td>
44
+ <NcCheckboxRadioSwitch
45
+ :checked="hasGroupPermission('public', 'read')"
46
+ @update:checked="updateGroupPermission('public', 'read', $event)" />
47
+ </td>
48
+ <td>
49
+ <NcCheckboxRadioSwitch
50
+ :checked="hasGroupPermission('public', 'update')"
51
+ @update:checked="updateGroupPermission('public', 'update', $event)" />
52
+ </td>
53
+ <td>
54
+ <NcCheckboxRadioSwitch
55
+ :checked="hasGroupPermission('public', 'delete')"
56
+ @update:checked="updateGroupPermission('public', 'delete', $event)" />
57
+ </td>
58
+ </tr>
59
+
60
+ <!-- User group (authenticated users) -->
61
+ <tr class="cn-schema-form__user-row">
62
+ <td class="cn-schema-form__group-name">
63
+ <span class="cn-schema-form__group-badge cn-schema-form__user">user</span>
64
+ <small>Authenticated users</small>
65
+ </td>
66
+ <td>
67
+ <NcCheckboxRadioSwitch
68
+ :checked="hasGroupPermission('user', 'create')"
69
+ @update:checked="updateGroupPermission('user', 'create', $event)" />
70
+ </td>
71
+ <td>
72
+ <NcCheckboxRadioSwitch
73
+ :checked="hasGroupPermission('user', 'read')"
74
+ @update:checked="updateGroupPermission('user', 'read', $event)" />
75
+ </td>
76
+ <td>
77
+ <NcCheckboxRadioSwitch
78
+ :checked="hasGroupPermission('user', 'update')"
79
+ @update:checked="updateGroupPermission('user', 'update', $event)" />
80
+ </td>
81
+ <td>
82
+ <NcCheckboxRadioSwitch
83
+ :checked="hasGroupPermission('user', 'delete')"
84
+ @update:checked="updateGroupPermission('user', 'delete', $event)" />
85
+ </td>
86
+ </tr>
87
+
88
+ <!-- Regular user groups -->
89
+ <tr v-for="group in sortedUserGroups" :key="group.id">
90
+ <td class="cn-schema-form__group-name">
91
+ <span class="cn-schema-form__group-badge">{{ group.displayname || group.id }}</span>
92
+ <small v-if="group.displayname && group.displayname !== group.id">{{ group.id }}</small>
93
+ </td>
94
+ <td>
95
+ <NcCheckboxRadioSwitch
96
+ :checked="hasGroupPermission(group.id, 'create')"
97
+ @update:checked="updateGroupPermission(group.id, 'create', $event)" />
98
+ </td>
99
+ <td>
100
+ <NcCheckboxRadioSwitch
101
+ :checked="hasGroupPermission(group.id, 'read')"
102
+ @update:checked="updateGroupPermission(group.id, 'read', $event)" />
103
+ </td>
104
+ <td>
105
+ <NcCheckboxRadioSwitch
106
+ :checked="hasGroupPermission(group.id, 'update')"
107
+ @update:checked="updateGroupPermission(group.id, 'update', $event)" />
108
+ </td>
109
+ <td>
110
+ <NcCheckboxRadioSwitch
111
+ :checked="hasGroupPermission(group.id, 'delete')"
112
+ @update:checked="updateGroupPermission(group.id, 'delete', $event)" />
113
+ </td>
114
+ </tr>
115
+
116
+ <!-- Admin group at bottom (disabled) -->
117
+ <tr class="cn-schema-form__admin-row">
118
+ <td class="cn-schema-form__group-name">
119
+ <span class="cn-schema-form__group-badge cn-schema-form__admin">admin</span>
120
+ <small>Always has full access</small>
121
+ </td>
122
+ <td>
123
+ <NcCheckboxRadioSwitch
124
+ :checked="true"
125
+ :disabled="true" />
126
+ </td>
127
+ <td>
128
+ <NcCheckboxRadioSwitch
129
+ :checked="true"
130
+ :disabled="true" />
131
+ </td>
132
+ <td>
133
+ <NcCheckboxRadioSwitch
134
+ :checked="true"
135
+ :disabled="true" />
136
+ </td>
137
+ <td>
138
+ <NcCheckboxRadioSwitch
139
+ :checked="true"
140
+ :disabled="true" />
141
+ </td>
142
+ </tr>
143
+ </tbody>
144
+ </table>
145
+
146
+ <div class="cn-schema-form__rbac-summary">
147
+ <CnNoteCard v-if="!hasAnyPermissions" type="success">
148
+ <p><strong>Open Access:</strong> No specific permissions set - all users can perform all operations.</p>
149
+ </CnNoteCard>
150
+ <CnNoteCard v-else-if="isRestrictiveSchema" type="warning">
151
+ <p><strong>Restrictive Schema:</strong> Access is limited to specified groups only.</p>
152
+ </CnNoteCard>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </template>
157
+
158
+ <script>
159
+ import {
160
+ // NcNoteCard,
161
+ NcCheckboxRadioSwitch,
162
+ NcLoadingIcon,
163
+ } from '@nextcloud/vue'
164
+ import CnNoteCard from '../CnNoteCard/CnNoteCard.vue'
165
+
166
+ /**
167
+ * CnSchemaSecurityTab — RBAC permissions table tab for CnSchemaFormDialog.
168
+ *
169
+ * Renders the schema-level authorization configuration. Mutates
170
+ * schemaItem.authorization directly.
171
+ */
172
+ export default {
173
+ name: 'CnSchemaSecurityTab',
174
+ components: {
175
+ CnNoteCard,
176
+ NcCheckboxRadioSwitch,
177
+ NcLoadingIcon,
178
+ },
179
+ props: {
180
+ /** The full schema item — mutates authorization directly */
181
+ schemaItem: { type: Object, required: true },
182
+ /** Full user groups array */
183
+ userGroups: { type: Array, default: () => [] },
184
+ /** Filtered/sorted user groups (excludes admin/public) */
185
+ sortedUserGroups: { type: Array, default: () => [] },
186
+ /** Whether groups are loading */
187
+ loadingGroups: { type: Boolean, default: false },
188
+ /** Whether schema has any permissions set */
189
+ hasAnyPermissions: { type: Boolean, default: false },
190
+ /** Whether schema has restrictive permissions */
191
+ isRestrictiveSchema: { type: Boolean, default: false },
192
+ },
193
+ computed: {
194
+ /** Local alias to avoid vue/no-mutating-props on template bindings */
195
+ schema() {
196
+ return this.schemaItem
197
+ },
198
+ },
199
+ methods: {
200
+ hasGroupPermission(groupId, action) {
201
+ const auth = this.schema.authorization || {}
202
+ if (!auth[action] || !Array.isArray(auth[action])) {
203
+ return false
204
+ }
205
+ return auth[action].includes(groupId)
206
+ },
207
+
208
+ updateGroupPermission(groupId, action, hasPermission) {
209
+ if (!this.schema.authorization) {
210
+ this.$set(this.schema, 'authorization', {})
211
+ }
212
+
213
+ if (!this.schema.authorization[action]) {
214
+ this.$set(this.schema.authorization, action, [])
215
+ }
216
+
217
+ const currentPermissions = this.schema.authorization[action]
218
+ const groupIndex = currentPermissions.indexOf(groupId)
219
+
220
+ if (hasPermission && groupIndex === -1) {
221
+ currentPermissions.push(groupId)
222
+ } else if (!hasPermission && groupIndex !== -1) {
223
+ currentPermissions.splice(groupIndex, 1)
224
+ }
225
+
226
+ if (currentPermissions.length === 0) {
227
+ this.$delete(this.schema.authorization, action)
228
+ }
229
+
230
+ if (Object.keys(this.schema.authorization).length === 0) {
231
+ this.$set(this.schema, 'authorization', {})
232
+ }
233
+ },
234
+ },
235
+ }
236
+ </script>
@@ -0,0 +1 @@
1
+ export { default as CnSchemaFormDialog } from './CnSchemaFormDialog.vue'
@@ -1 +1 @@
1
- export { default as CnSettingsCard } from './CnSettingsCard.vue'
1
+ export { default as CnSettingsCard } from './CnSettingsCard.vue'
@@ -1 +1 @@
1
- export { default as CnSettingsSection } from './CnSettingsSection.vue'
1
+ export { default as CnSettingsSection } from './CnSettingsSection.vue'
@@ -1,9 +1,9 @@
1
1
  <template>
2
2
  <component
3
- :is="clickable ? 'a' : 'div'"
3
+ :is="componentTag"
4
4
  class="cn-stats-block"
5
5
  :class="rootClasses"
6
- v-bind="clickable ? { href: '#', role: 'button', tabindex: '0' } : {}"
6
+ v-bind="componentAttrs"
7
7
  @click="onClick">
8
8
  <!-- Icon -->
9
9
  <div v-if="hasIcon" class="cn-stats-block__icon" :class="iconClasses">
@@ -18,7 +18,7 @@
18
18
  <h4>{{ title || 'Objects' }}</h4>
19
19
  </div>
20
20
 
21
- <div v-if="count > 0" class="cn-stats-block__count">
21
+ <div v-if="count > 0 || (showZeroCount && count === 0)" class="cn-stats-block__count">
22
22
  <span class="cn-stats-block__count-value">{{ formattedCount }}</span>
23
23
  <span class="cn-stats-block__count-label">{{ countLabel }}</span>
24
24
  </div>
@@ -31,7 +31,7 @@
31
31
  </div>
32
32
 
33
33
  <!-- Breakdown details -->
34
- <div v-if="breakdown && count > 0" class="cn-stats-block__breakdown">
34
+ <div v-if="breakdown && (count > 0 || showZeroCount)" class="cn-stats-block__breakdown">
35
35
  <div
36
36
  v-for="(value, key) in breakdown"
37
37
  :key="key"
@@ -70,6 +70,14 @@ import { NcLoadingIcon } from '@nextcloud/vue'
70
70
  * clickable
71
71
  * @click="goToCases" />
72
72
  *
73
+ * @example With route-based navigation (renders as <router-link>)
74
+ * <CnStatsBlock
75
+ * title="Open Cases"
76
+ * :count="42"
77
+ * :icon="BriefcaseOutline"
78
+ * variant="primary"
79
+ * :route="{ name: 'Cases', query: { status: 'open' } }" />
80
+ *
73
81
  * @example With breakdown
74
82
  * <CnStatsBlock
75
83
  * title="Cases"
@@ -152,6 +160,21 @@ export default {
152
160
  type: Boolean,
153
161
  default: false,
154
162
  },
163
+ /** Whether to display 0 as a count value instead of the empty label */
164
+ showZeroCount: {
165
+ type: Boolean,
166
+ default: false,
167
+ },
168
+ /**
169
+ * Vue Router location object for declarative navigation.
170
+ * When set, the card renders as a <router-link> and clickable styles are implied.
171
+ * @example { name: 'Cases', query: { status: 'open' } }
172
+ * @example { path: '/catalogi' }
173
+ */
174
+ route: {
175
+ type: Object,
176
+ default: null,
177
+ },
155
178
  },
156
179
 
157
180
  computed: {
@@ -163,10 +186,39 @@ export default {
163
186
  return this.count.toLocaleString()
164
187
  },
165
188
 
189
+ /**
190
+ * Whether the card is interactive (clickable or has a route).
191
+ * Used for applying hover/focus styles.
192
+ */
193
+ isInteractive() {
194
+ return !!this.route || this.clickable
195
+ },
196
+
197
+ /**
198
+ * Determines which HTML element or component to render.
199
+ * - route set → 'router-link' (SPA navigation)
200
+ * - clickable → 'a' (app handles click via event)
201
+ * - neither → 'div' (static display)
202
+ */
203
+ componentTag() {
204
+ if (this.route) return 'router-link'
205
+ if (this.clickable) return 'a'
206
+ return 'div'
207
+ },
208
+
209
+ /**
210
+ * Dynamic attributes for the root element based on rendering mode.
211
+ */
212
+ componentAttrs() {
213
+ if (this.route) return { to: this.route, tabindex: '0' }
214
+ if (this.clickable) return { href: '#', role: 'button', tabindex: '0' }
215
+ return {}
216
+ },
217
+
166
218
  rootClasses() {
167
219
  return {
168
220
  'cn-stats-block--horizontal': this.horizontal,
169
- 'cn-stats-block--clickable': this.clickable,
221
+ 'cn-stats-block--clickable': this.isInteractive,
170
222
  [`cn-stats-block--${this.variant}`]: this.variant !== 'default',
171
223
  }
172
224
  },
@@ -184,6 +236,8 @@ export default {
184
236
  },
185
237
 
186
238
  onClick(event) {
239
+ // When route is set, router-link handles navigation — do not emit click
240
+ if (this.route) return
187
241
  if (this.clickable) {
188
242
  event.preventDefault()
189
243
  this.$emit('click', event)
@@ -205,16 +259,22 @@ export default {
205
259
  color: inherit;
206
260
  border: 2px solid transparent;
207
261
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
262
+ height: 100%;
263
+ width: 100%;
264
+ box-sizing: border-box;
265
+ overflow: hidden;
266
+ min-width: 0;
208
267
  }
209
268
 
210
269
  .cn-stats-block--horizontal {
211
270
  flex-direction: row;
212
- align-items: flex-start;
213
- gap: 16px;
271
+ align-items: center;
272
+ gap: 12px;
214
273
  }
215
274
 
216
275
  .cn-stats-block--horizontal .cn-stats-block__content {
217
276
  text-align: left;
277
+ min-width: 0;
218
278
  }
219
279
 
220
280
  .cn-stats-block--horizontal .cn-stats-block__count {
@@ -251,6 +311,8 @@ export default {
251
311
 
252
312
  .cn-stats-block--horizontal .cn-stats-block__icon {
253
313
  margin-bottom: 0;
314
+ width: 36px;
315
+ height: 36px;
254
316
  }
255
317
 
256
318
  .cn-stats-block__icon--primary {
@@ -260,17 +322,17 @@ export default {
260
322
 
261
323
  .cn-stats-block__icon--success {
262
324
  background: rgba(70, 186, 97, 0.1);
263
- color: var(--color-success);
325
+ color: var(--color-element-success, var(--color-success));
264
326
  }
265
327
 
266
328
  .cn-stats-block__icon--warning {
267
329
  background: rgba(232, 163, 24, 0.1);
268
- color: var(--color-warning);
330
+ color: var(--color-element-warning, var(--color-warning));
269
331
  }
270
332
 
271
333
  .cn-stats-block__icon--error {
272
334
  background: rgba(224, 36, 36, 0.1);
273
- color: var(--color-error);
335
+ color: var(--color-element-error, var(--color-error));
274
336
  }
275
337
 
276
338
  /* Content */
@@ -282,34 +344,42 @@ export default {
282
344
 
283
345
  .cn-stats-block__header h4 {
284
346
  margin-top: 0;
285
- margin-bottom: 0.5rem;
347
+ margin-bottom: 0.25rem;
286
348
  color: var(--color-main-text);
287
349
  font-size: 14px;
288
350
  font-weight: 600;
351
+ white-space: nowrap;
352
+ overflow: hidden;
353
+ text-overflow: ellipsis;
289
354
  }
290
355
 
291
356
  .cn-stats-block__count {
292
357
  display: flex;
293
358
  align-items: baseline;
294
359
  justify-content: center;
295
- gap: 0.5rem;
360
+ gap: 0.25rem;
296
361
  font-size: 1.2rem;
297
- margin-bottom: 0.5rem;
362
+ margin-bottom: 0.25rem;
363
+ white-space: nowrap;
364
+ overflow: hidden;
298
365
  }
299
366
 
300
367
  .cn-stats-block__count-value {
301
368
  font-size: 2rem;
302
369
  font-weight: bold;
303
370
  color: var(--color-primary-element);
371
+ flex-shrink: 0;
304
372
  }
305
373
 
306
374
  .cn-stats-block--primary .cn-stats-block__count-value { color: var(--color-primary-element); }
307
- .cn-stats-block--success .cn-stats-block__count-value { color: var(--color-success); }
308
- .cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-warning); }
309
- .cn-stats-block--error .cn-stats-block__count-value { color: var(--color-error); }
375
+ .cn-stats-block--success .cn-stats-block__count-value { color: var(--color-element-success, var(--color-success)); }
376
+ .cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-element-warning, var(--color-warning)); }
377
+ .cn-stats-block--error .cn-stats-block__count-value { color: var(--color-element-error, var(--color-error)); }
310
378
 
311
379
  .cn-stats-block__count-label {
312
380
  color: var(--color-text-maxcontrast);
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
313
383
  }
314
384
 
315
385
  .cn-stats-block__loading {
@@ -360,7 +430,7 @@ export default {
360
430
  background: var(--color-background-hover);
361
431
  }
362
432
 
363
- .cn-stats-block__breakdown-value--invalid { color: var(--color-warning); }
364
- .cn-stats-block__breakdown-value--deleted { color: var(--color-error); }
365
- .cn-stats-block__breakdown-value--published { color: var(--color-success); }
433
+ .cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }
434
+ .cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }
435
+ .cn-stats-block__breakdown-value--published { color: var(--color-element-success); }
366
436
  </style>
@@ -1 +1 @@
1
- export { default as CnStatsBlock } from './CnStatsBlock.vue'
1
+ export { default as CnStatsBlock } from './CnStatsBlock.vue'