@conduction/nextcloud-vue 0.1.0-beta.2 → 0.1.0-beta.4

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 (153) hide show
  1. package/README.md +226 -226
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +60455 -8755
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +2062 -528
  6. package/dist/nextcloud-vue.esm.js +60411 -8731
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +75 -61
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  10. package/src/components/CnActionsBar/index.js +1 -1
  11. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  12. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  13. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  16. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  17. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  18. package/src/components/CnCardGrid/index.js +1 -1
  19. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  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/CnConfigurationCard.vue +77 -77
  24. package/src/components/CnConfigurationCard/index.js +1 -1
  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 +349 -349
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  32. package/src/components/CnDetailCard/index.js +1 -0
  33. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  34. package/src/components/CnDetailPage/index.js +1 -0
  35. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  36. package/src/components/CnFacetSidebar/index.js +1 -1
  37. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnIcon/CnIcon.vue +89 -89
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  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/CnKpiGrid.vue +89 -89
  48. package/src/components/CnKpiGrid/index.js +1 -1
  49. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  50. package/src/components/CnMassActionBar/index.js +1 -1
  51. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  52. package/src/components/CnMassCopyDialog/index.js +1 -1
  53. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  54. package/src/components/CnMassDeleteDialog/index.js +1 -1
  55. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  56. package/src/components/CnMassExportDialog/index.js +1 -1
  57. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  58. package/src/components/CnMassImportDialog/index.js +1 -1
  59. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  60. package/src/components/CnNoteCard/index.js +1 -0
  61. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  62. package/src/components/CnNotesCard/index.js +1 -0
  63. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  64. package/src/components/CnObjectCard/index.js +1 -1
  65. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  66. package/src/components/CnObjectSidebar/index.js +1 -0
  67. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  68. package/src/components/CnPageHeader/index.js +1 -1
  69. package/src/components/CnPagination/CnPagination.vue +252 -252
  70. package/src/components/CnPagination/index.js +1 -1
  71. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  72. package/src/components/CnRowActions/index.js +1 -1
  73. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  77. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  78. package/src/components/CnSchemaFormDialog/index.js +1 -0
  79. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  80. package/src/components/CnSettingsCard/index.js +1 -1
  81. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  82. package/src/components/CnSettingsSection/index.js +1 -1
  83. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  84. package/src/components/CnStatsBlock/index.js +1 -1
  85. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  86. package/src/components/CnStatusBadge/index.js +1 -1
  87. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  88. package/src/components/CnTabbedFormDialog/index.js +1 -0
  89. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  90. package/src/components/CnTasksCard/index.js +1 -0
  91. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  92. package/src/components/CnTileWidget/index.js +1 -0
  93. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  94. package/src/components/CnTimelineStages/index.js +1 -0
  95. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  96. package/src/components/CnUserActionMenu/index.js +1 -0
  97. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  98. package/src/components/CnVersionInfoCard/index.js +1 -1
  99. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  100. package/src/components/CnWidgetRenderer/index.js +1 -0
  101. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  102. package/src/components/CnWidgetWrapper/index.js +1 -0
  103. package/src/components/index.js +43 -29
  104. package/src/composables/index.js +4 -3
  105. package/src/composables/useDashboardView.js +240 -0
  106. package/src/composables/useDetailView.js +289 -132
  107. package/src/composables/useListView.js +363 -153
  108. package/src/composables/useSubResource.js +142 -142
  109. package/src/constants/metadata.js +30 -30
  110. package/src/css/CnSchemaFormDialog.css +546 -0
  111. package/src/css/__sample_nextcloud_tokens.css +110 -0
  112. package/src/css/actions-bar.css +48 -48
  113. package/src/css/badge.css +51 -51
  114. package/src/css/card.css +128 -128
  115. package/src/css/dashboard.css +70 -0
  116. package/src/css/detail-page.css +168 -0
  117. package/src/css/detail.css +68 -68
  118. package/src/css/index-page.css +44 -32
  119. package/src/css/index-sidebar.css +193 -187
  120. package/src/css/index.css +16 -12
  121. package/src/css/layout.css +90 -90
  122. package/src/css/page-header.css +33 -33
  123. package/src/css/pagination.css +72 -72
  124. package/src/css/table.css +142 -142
  125. package/src/css/timeline-stages.css +218 -0
  126. package/src/css/utilities.css +46 -46
  127. package/src/index.js +72 -53
  128. package/src/store/createSubResourcePlugin.js +135 -135
  129. package/src/store/index.js +3 -3
  130. package/src/store/plugins/auditTrails.js +17 -17
  131. package/src/store/plugins/files.js +250 -186
  132. package/src/store/plugins/index.js +7 -5
  133. package/src/store/plugins/lifecycle.js +180 -180
  134. package/src/store/plugins/relations.js +68 -68
  135. package/src/store/plugins/search.js +372 -0
  136. package/src/store/plugins/selection.js +104 -0
  137. package/src/store/useObjectStore.js +829 -686
  138. package/src/types/auditTrail.d.ts +32 -32
  139. package/src/types/file.d.ts +23 -23
  140. package/src/types/index.d.ts +35 -35
  141. package/src/types/notification.d.ts +36 -36
  142. package/src/types/object.d.ts +40 -40
  143. package/src/types/organisation.d.ts +41 -41
  144. package/src/types/register.d.ts +25 -25
  145. package/src/types/schema.d.ts +39 -39
  146. package/src/types/shared.d.ts +79 -79
  147. package/src/types/source.d.ts +14 -14
  148. package/src/types/task.d.ts +31 -31
  149. package/src/utils/errors.js +96 -96
  150. package/src/utils/headers.js +68 -50
  151. package/src/utils/id.js +13 -0
  152. package/src/utils/index.js +3 -3
  153. package/src/utils/schema.js +422 -419
@@ -0,0 +1,876 @@
1
+ <!--
2
+ CnObjectSidebar — Right sidebar with standardized tabs for generic object functionality.
3
+
4
+ Provides Files, Notes, Tags, Tasks, and Audit Trail tabs that integrate with
5
+ OpenRegister API endpoints (which bridge to Nextcloud-native APIs).
6
+ All tabs are optional and overridable via props and slots.
7
+ -->
8
+ <template>
9
+ <NcAppSidebar
10
+ :title="sidebarTitle"
11
+ :subtitle="sidebarSubtitle"
12
+ :active.sync="activeTab"
13
+ @update:open="$emit('update:open', $event)"
14
+ @close="$emit('update:open', false)">
15
+ <!-- Files Tab -->
16
+ <NcAppSidebarTab
17
+ v-if="!isTabHidden('files')"
18
+ id="files"
19
+ :name="filesLabel"
20
+ :order="1">
21
+ <template #icon>
22
+ <Paperclip :size="20" />
23
+ </template>
24
+ <slot name="tab-files" :object-id="objectId" :object-type="objectType">
25
+ <div class="cn-sidebar-tab">
26
+ <!-- File upload -->
27
+ <div class="cn-sidebar-tab__action">
28
+ <label class="cn-sidebar-tab__upload-btn">
29
+ <NcButton type="secondary" :wide="true" :disabled="filesLoading">
30
+ <template #icon>
31
+ <Upload :size="20" />
32
+ </template>
33
+ {{ uploadLabel }}
34
+ </NcButton>
35
+ <input
36
+ ref="fileInput"
37
+ type="file"
38
+ multiple
39
+ class="cn-sidebar-tab__file-input"
40
+ @change="onFileUpload">
41
+ </label>
42
+ </div>
43
+
44
+ <!-- File list -->
45
+ <NcLoadingIcon v-if="filesLoading" />
46
+ <div v-else-if="files.length === 0" class="cn-sidebar-tab__empty">
47
+ {{ noFilesLabel }}
48
+ </div>
49
+ <div v-else class="cn-sidebar-tab__list">
50
+ <NcListItem
51
+ v-for="file in files"
52
+ :key="file.id"
53
+ :name="file.name || file.title"
54
+ :bold="false"
55
+ :force-display-actions="true">
56
+ <template #icon>
57
+ <FileOutline :size="32" />
58
+ </template>
59
+ <template #subname>
60
+ {{ formatFileSize(file.size) }}
61
+ </template>
62
+ <template #actions>
63
+ <NcActionButton @click="openFile(file)">
64
+ <template #icon>
65
+ <OpenInNew :size="20" />
66
+ </template>
67
+ {{ openLabel }}
68
+ </NcActionButton>
69
+ <NcActionButton @click="deleteFile(file)">
70
+ <template #icon>
71
+ <Delete :size="20" />
72
+ </template>
73
+ {{ deleteLabel }}
74
+ </NcActionButton>
75
+ </template>
76
+ </NcListItem>
77
+ </div>
78
+ </div>
79
+ </slot>
80
+ </NcAppSidebarTab>
81
+
82
+ <!-- Notes Tab -->
83
+ <NcAppSidebarTab
84
+ v-if="!isTabHidden('notes')"
85
+ id="notes"
86
+ :name="notesLabel"
87
+ :order="2">
88
+ <template #icon>
89
+ <CommentTextOutline :size="20" />
90
+ </template>
91
+ <slot name="tab-notes" :object-id="objectId" :object-type="objectType">
92
+ <div class="cn-sidebar-tab">
93
+ <!-- Add note -->
94
+ <div class="cn-sidebar-tab__action">
95
+ <textarea
96
+ v-model="newNoteText"
97
+ class="cn-sidebar-tab__textarea"
98
+ :placeholder="addNotePlaceholder"
99
+ rows="3" />
100
+ <NcButton
101
+ type="primary"
102
+ :disabled="!newNoteText.trim() || noteSaving"
103
+ @click="addNote">
104
+ <template #icon>
105
+ <Send :size="20" />
106
+ </template>
107
+ {{ addNoteLabel }}
108
+ </NcButton>
109
+ </div>
110
+
111
+ <!-- Notes list -->
112
+ <NcLoadingIcon v-if="notesLoading" />
113
+ <div v-else-if="notes.length === 0" class="cn-sidebar-tab__empty">
114
+ {{ noNotesLabel }}
115
+ </div>
116
+ <div v-else class="cn-sidebar-tab__list">
117
+ <div
118
+ v-for="note in notes"
119
+ :key="note.id"
120
+ class="cn-sidebar-tab__note">
121
+ <div class="cn-sidebar-tab__note-header">
122
+ <strong>{{ note.actorDisplayName || note.author || 'Unknown' }}</strong>
123
+ <span class="cn-sidebar-tab__note-time">{{ formatDate(note.creationDateTime || note.created) }}</span>
124
+ </div>
125
+ <p class="cn-sidebar-tab__note-body">{{ note.message || note.content }}</p>
126
+ <NcButton
127
+ v-if="canDeleteNote(note)"
128
+ type="tertiary"
129
+ class="cn-sidebar-tab__note-delete"
130
+ @click="deleteNote(note)">
131
+ <template #icon>
132
+ <Delete :size="16" />
133
+ </template>
134
+ </NcButton>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </slot>
139
+ </NcAppSidebarTab>
140
+
141
+ <!-- Tags Tab -->
142
+ <NcAppSidebarTab
143
+ v-if="!isTabHidden('tags')"
144
+ id="tags"
145
+ :name="tagsLabel"
146
+ :order="3">
147
+ <template #icon>
148
+ <TagOutline :size="20" />
149
+ </template>
150
+ <slot name="tab-tags" :object-id="objectId" :object-type="objectType">
151
+ <div class="cn-sidebar-tab">
152
+ <!-- Add tag -->
153
+ <div class="cn-sidebar-tab__action cn-sidebar-tab__action--row">
154
+ <NcTextField
155
+ v-model="newTagName"
156
+ :label="addTagPlaceholder"
157
+ @keyup.enter="addTag" />
158
+ <NcButton
159
+ type="primary"
160
+ :disabled="!newTagName.trim() || tagSaving"
161
+ @click="addTag">
162
+ <template #icon>
163
+ <Plus :size="20" />
164
+ </template>
165
+ </NcButton>
166
+ </div>
167
+
168
+ <!-- Tags list -->
169
+ <NcLoadingIcon v-if="tagsLoading" />
170
+ <div v-else-if="tags.length === 0" class="cn-sidebar-tab__empty">
171
+ {{ noTagsLabel }}
172
+ </div>
173
+ <div v-else class="cn-sidebar-tab__tags">
174
+ <span
175
+ v-for="tag in tags"
176
+ :key="tag.id || tag"
177
+ class="cn-sidebar-tab__tag">
178
+ {{ tag.name || tag }}
179
+ <button
180
+ class="cn-sidebar-tab__tag-remove"
181
+ :aria-label="'Remove ' + (tag.name || tag)"
182
+ @click="removeTag(tag)">
183
+ <Close :size="14" />
184
+ </button>
185
+ </span>
186
+ </div>
187
+ </div>
188
+ </slot>
189
+ </NcAppSidebarTab>
190
+
191
+ <!-- Tasks Tab -->
192
+ <NcAppSidebarTab
193
+ v-if="!isTabHidden('tasks')"
194
+ id="tasks"
195
+ :name="tasksLabel"
196
+ :order="4">
197
+ <template #icon>
198
+ <CheckboxMarkedOutline :size="20" />
199
+ </template>
200
+ <slot name="tab-tasks" :object-id="objectId" :object-type="objectType">
201
+ <div class="cn-sidebar-tab">
202
+ <NcLoadingIcon v-if="tasksLoading" />
203
+ <div v-else-if="tasks.length === 0" class="cn-sidebar-tab__empty">
204
+ {{ noTasksLabel }}
205
+ </div>
206
+ <div v-else class="cn-sidebar-tab__list">
207
+ <NcListItem
208
+ v-for="task in tasks"
209
+ :key="task.id"
210
+ :name="task.title || task.name"
211
+ :bold="false">
212
+ <template #icon>
213
+ <CheckboxMarkedOutline v-if="task.status === 'completed'" :size="32" class="cn-sidebar-tab__task-done" />
214
+ <CheckboxBlankOutline v-else :size="32" />
215
+ </template>
216
+ <template #subname>
217
+ {{ task.assignee || '' }}{{ task.dueDate ? ' · ' + formatDate(task.dueDate) : '' }}
218
+ </template>
219
+ </NcListItem>
220
+ </div>
221
+ </div>
222
+ </slot>
223
+ </NcAppSidebarTab>
224
+
225
+ <!-- Audit Trail Tab -->
226
+ <NcAppSidebarTab
227
+ v-if="!isTabHidden('auditTrail')"
228
+ id="auditTrail"
229
+ :name="auditTrailLabel"
230
+ :order="5">
231
+ <template #icon>
232
+ <History :size="20" />
233
+ </template>
234
+ <slot name="tab-audit-trail" :object-id="objectId" :object-type="objectType">
235
+ <div class="cn-sidebar-tab">
236
+ <NcLoadingIcon v-if="auditTrailLoading" />
237
+ <div v-else-if="auditTrails.length === 0" class="cn-sidebar-tab__empty">
238
+ {{ noAuditTrailLabel }}
239
+ </div>
240
+ <div v-else class="cn-sidebar-tab__list">
241
+ <NcListItem
242
+ v-for="entry in auditTrails"
243
+ :key="entry.id"
244
+ :name="formatDate(entry.created)"
245
+ :bold="false"
246
+ :details="entry.action"
247
+ :counter-number="entry.changed ? Object.keys(entry.changed).length : 0">
248
+ <template #icon>
249
+ <History :size="32" />
250
+ </template>
251
+ <template #subname>
252
+ {{ entry.userName || entry.user || 'System' }}
253
+ </template>
254
+ </NcListItem>
255
+ </div>
256
+ </div>
257
+ </slot>
258
+ </NcAppSidebarTab>
259
+
260
+ <!-- Custom tabs slot -->
261
+ <slot name="extra-tabs" />
262
+ </NcAppSidebar>
263
+ </template>
264
+
265
+ <script>
266
+ import {
267
+ NcAppSidebar,
268
+ NcAppSidebarTab,
269
+ NcButton,
270
+ NcListItem,
271
+ NcActionButton,
272
+ NcLoadingIcon,
273
+ NcTextField,
274
+ } from '@nextcloud/vue'
275
+
276
+ import Paperclip from 'vue-material-design-icons/Paperclip.vue'
277
+ import Upload from 'vue-material-design-icons/Upload.vue'
278
+ import FileOutline from 'vue-material-design-icons/FileOutline.vue'
279
+ import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
280
+ import Delete from 'vue-material-design-icons/Delete.vue'
281
+ import CommentTextOutline from 'vue-material-design-icons/CommentTextOutline.vue'
282
+ import Send from 'vue-material-design-icons/Send.vue'
283
+ import TagOutline from 'vue-material-design-icons/TagOutline.vue'
284
+ import Plus from 'vue-material-design-icons/Plus.vue'
285
+ import Close from 'vue-material-design-icons/Close.vue'
286
+ import CheckboxMarkedOutline from 'vue-material-design-icons/CheckboxMarkedOutline.vue'
287
+ import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline.vue'
288
+ import History from 'vue-material-design-icons/History.vue'
289
+
290
+ import { buildHeaders } from '../../utils/index.js'
291
+
292
+ /**
293
+ * CnObjectSidebar — Right sidebar for entity detail pages.
294
+ *
295
+ * Provides standardized tabs for generic object functionality (Files, Notes, Tags,
296
+ * Tasks, Audit Trail) that integrate with OpenRegister API endpoints bridging to
297
+ * Nextcloud-native APIs.
298
+ *
299
+ * @example Basic usage
300
+ * <CnObjectSidebar
301
+ * object-type="pipelinq_lead"
302
+ * :object-id="leadId"
303
+ * :register="registerConfig.register"
304
+ * :schema="registerConfig.schema" />
305
+ *
306
+ * @example Hide specific tabs
307
+ * <CnObjectSidebar
308
+ * object-type="pipelinq_lead"
309
+ * :object-id="leadId"
310
+ * :hidden-tabs="['tasks', 'tags']" />
311
+ *
312
+ * @example Override a tab
313
+ * <CnObjectSidebar object-type="pipelinq_lead" :object-id="leadId">
314
+ * <template #tab-notes="{ objectId }">
315
+ * <MyCustomNotesComponent :id="objectId" />
316
+ * </template>
317
+ * </CnObjectSidebar>
318
+ */
319
+ export default {
320
+ name: 'CnObjectSidebar',
321
+
322
+ components: {
323
+ NcAppSidebar,
324
+ NcAppSidebarTab,
325
+ NcButton,
326
+ NcListItem,
327
+ NcActionButton,
328
+ NcLoadingIcon,
329
+ NcTextField,
330
+ Paperclip,
331
+ Upload,
332
+ FileOutline,
333
+ OpenInNew,
334
+ Delete,
335
+ CommentTextOutline,
336
+ Send,
337
+ TagOutline,
338
+ Plus,
339
+ Close,
340
+ CheckboxMarkedOutline,
341
+ CheckboxBlankOutline,
342
+ History,
343
+ },
344
+
345
+ props: {
346
+ /** The entity type (e.g., "pipelinq_lead", "procest_case") */
347
+ objectType: {
348
+ type: String,
349
+ required: true,
350
+ },
351
+ /** The object UUID */
352
+ objectId: {
353
+ type: String,
354
+ required: true,
355
+ },
356
+ /** OpenRegister register ID (for file/audit trail operations) */
357
+ register: {
358
+ type: String,
359
+ default: '',
360
+ },
361
+ /** OpenRegister schema ID (for file/audit trail operations) */
362
+ schema: {
363
+ type: String,
364
+ default: '',
365
+ },
366
+ /** Array of tab IDs to hide: 'files', 'notes', 'tags', 'tasks', 'auditTrail' */
367
+ hiddenTabs: {
368
+ type: Array,
369
+ default: () => [],
370
+ },
371
+ /** Whether the sidebar is open */
372
+ open: {
373
+ type: Boolean,
374
+ default: true,
375
+ },
376
+ /** Sidebar title (defaults to objectType) */
377
+ title: {
378
+ type: String,
379
+ default: '',
380
+ },
381
+ /** Sidebar subtitle */
382
+ subtitleProp: {
383
+ type: String,
384
+ default: '',
385
+ },
386
+ /** Base API URL for OpenRegister */
387
+ apiBase: {
388
+ type: String,
389
+ default: '/apps/openregister/api',
390
+ },
391
+
392
+ // --- Pre-translated labels ---
393
+ filesLabel: { type: String, default: 'Files' },
394
+ notesLabel: { type: String, default: 'Notes' },
395
+ tagsLabel: { type: String, default: 'Tags' },
396
+ tasksLabel: { type: String, default: 'Tasks' },
397
+ auditTrailLabel: { type: String, default: 'Audit Trail' },
398
+ uploadLabel: { type: String, default: 'Upload file' },
399
+ addNoteLabel: { type: String, default: 'Add note' },
400
+ addNotePlaceholder: { type: String, default: 'Write a note...' },
401
+ addTagPlaceholder: { type: String, default: 'Add tag...' },
402
+ openLabel: { type: String, default: 'Open' },
403
+ deleteLabel: { type: String, default: 'Delete' },
404
+ noFilesLabel: { type: String, default: 'No files attached' },
405
+ noNotesLabel: { type: String, default: 'No notes yet' },
406
+ noTagsLabel: { type: String, default: 'No tags' },
407
+ noTasksLabel: { type: String, default: 'No linked tasks' },
408
+ noAuditTrailLabel: { type: String, default: 'No audit trail entries' },
409
+ },
410
+
411
+ emits: ['update:open'],
412
+
413
+ data() {
414
+ return {
415
+ activeTab: 'files',
416
+ // Files
417
+ files: [],
418
+ filesLoading: false,
419
+ // Notes
420
+ notes: [],
421
+ notesLoading: false,
422
+ newNoteText: '',
423
+ noteSaving: false,
424
+ // Tags
425
+ tags: [],
426
+ tagsLoading: false,
427
+ newTagName: '',
428
+ tagSaving: false,
429
+ // Tasks
430
+ tasks: [],
431
+ tasksLoading: false,
432
+ // Audit Trail
433
+ auditTrails: [],
434
+ auditTrailLoading: false,
435
+ }
436
+ },
437
+
438
+ computed: {
439
+ sidebarTitle() {
440
+ return this.title || this.objectType || 'Details'
441
+ },
442
+ sidebarSubtitle() {
443
+ return this.subtitleProp || this.objectId || ''
444
+ },
445
+ },
446
+
447
+ watch: {
448
+ objectId: {
449
+ immediate: true,
450
+ handler(newId) {
451
+ if (newId) {
452
+ this.loadAllData()
453
+ }
454
+ },
455
+ },
456
+ activeTab(tab) {
457
+ // Lazy-load tab data when switching
458
+ this.loadTabData(tab)
459
+ },
460
+ },
461
+
462
+ methods: {
463
+ isTabHidden(tabId) {
464
+ return this.hiddenTabs.includes(tabId)
465
+ },
466
+
467
+ async loadAllData() {
468
+ // Load files (default tab) immediately, others lazily
469
+ this.loadTabData('files')
470
+ },
471
+
472
+ async loadTabData(tab) {
473
+ switch (tab) {
474
+ case 'files':
475
+ if (this.files.length === 0 && !this.filesLoading) this.fetchFiles()
476
+ break
477
+ case 'notes':
478
+ if (this.notes.length === 0 && !this.notesLoading) this.fetchNotes()
479
+ break
480
+ case 'tags':
481
+ if (this.tags.length === 0 && !this.tagsLoading) this.fetchTags()
482
+ break
483
+ case 'tasks':
484
+ if (this.tasks.length === 0 && !this.tasksLoading) this.fetchTasks()
485
+ break
486
+ case 'auditTrail':
487
+ if (this.auditTrails.length === 0 && !this.auditTrailLoading) this.fetchAuditTrails()
488
+ break
489
+ }
490
+ },
491
+
492
+ // --- Files ---
493
+ async fetchFiles() {
494
+ if (!this.register || !this.schema) return
495
+ this.filesLoading = true
496
+ try {
497
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/files`
498
+ const response = await fetch(url, { headers: buildHeaders() })
499
+ if (response.ok) {
500
+ const data = await response.json()
501
+ this.files = data.results || data || []
502
+ }
503
+ } catch (err) {
504
+ console.error('CnObjectSidebar: Failed to fetch files', err)
505
+ } finally {
506
+ this.filesLoading = false
507
+ }
508
+ },
509
+
510
+ async onFileUpload(event) {
511
+ const inputFiles = event.target.files
512
+ if (!inputFiles?.length || !this.register || !this.schema) return
513
+
514
+ const formData = new FormData()
515
+ for (const file of inputFiles) {
516
+ formData.append('files[]', file)
517
+ }
518
+
519
+ try {
520
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/files`
521
+ await fetch(url, {
522
+ method: 'POST',
523
+ headers: {
524
+ requesttoken: OC?.requestToken || '',
525
+ 'OCS-APIREQUEST': 'true',
526
+ },
527
+ body: formData,
528
+ })
529
+ await this.fetchFiles()
530
+ } catch (err) {
531
+ console.error('CnObjectSidebar: Failed to upload file', err)
532
+ }
533
+
534
+ // Reset input
535
+ if (this.$refs.fileInput) {
536
+ this.$refs.fileInput.value = ''
537
+ }
538
+ },
539
+
540
+ openFile(file) {
541
+ if (file.accessUrl) {
542
+ window.open(file.accessUrl, '_blank')
543
+ } else if (file.id) {
544
+ const dirPath = file.path ? file.path.substring(0, file.path.lastIndexOf('/')) : ''
545
+ const cleanPath = dirPath.replace(/^\/admin\/files\//, '/')
546
+ window.open(`/index.php/apps/files/files/${file.id}?dir=${encodeURIComponent(cleanPath)}&openfile=true`, '_blank')
547
+ }
548
+ },
549
+
550
+ async deleteFile(file) {
551
+ if (!this.register || !this.schema) return
552
+ try {
553
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/files/${file.id}`
554
+ await fetch(url, { method: 'DELETE', headers: buildHeaders() })
555
+ this.files = this.files.filter(f => f.id !== file.id)
556
+ } catch (err) {
557
+ console.error('CnObjectSidebar: Failed to delete file', err)
558
+ }
559
+ },
560
+
561
+ // --- Notes (via OpenRegister → NC Comments API) ---
562
+ async fetchNotes() {
563
+ if (!this.register || !this.schema) return
564
+ this.notesLoading = true
565
+ try {
566
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes`
567
+ const response = await fetch(url, { headers: buildHeaders() })
568
+ if (response.ok) {
569
+ const data = await response.json()
570
+ this.notes = data.results || data || []
571
+ }
572
+ } catch (err) {
573
+ console.error('CnObjectSidebar: Failed to fetch notes', err)
574
+ } finally {
575
+ this.notesLoading = false
576
+ }
577
+ },
578
+
579
+ async addNote() {
580
+ if (!this.newNoteText.trim()) return
581
+ this.noteSaving = true
582
+ try {
583
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes`
584
+ await fetch(url, {
585
+ method: 'POST',
586
+ headers: buildHeaders(),
587
+ body: JSON.stringify({ message: this.newNoteText.trim() }),
588
+ })
589
+ this.newNoteText = ''
590
+ await this.fetchNotes()
591
+ } catch (err) {
592
+ console.error('CnObjectSidebar: Failed to add note', err)
593
+ } finally {
594
+ this.noteSaving = false
595
+ }
596
+ },
597
+
598
+ canDeleteNote(note) {
599
+ return note.actorId === OC?.currentUser || note.author === OC?.currentUser
600
+ },
601
+
602
+ async deleteNote(note) {
603
+ try {
604
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes/${note.id}`
605
+ await fetch(url, { method: 'DELETE', headers: buildHeaders() })
606
+ this.notes = this.notes.filter(n => n.id !== note.id)
607
+ } catch (err) {
608
+ console.error('CnObjectSidebar: Failed to delete note', err)
609
+ }
610
+ },
611
+
612
+ // --- Tags ---
613
+ // Tags are managed via the object's own data (object.tags array) and patched
614
+ // back to the object. Available tags list comes from GET /api/tags.
615
+ async fetchTags() {
616
+ if (!this.register || !this.schema) return
617
+ this.tagsLoading = true
618
+ try {
619
+ // Fetch the object to get its current tags
620
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}`
621
+ const response = await fetch(url, { headers: buildHeaders() })
622
+ if (response.ok) {
623
+ const data = await response.json()
624
+ this.tags = data.tags || data.object?.tags || []
625
+ }
626
+ } catch (err) {
627
+ console.error('CnObjectSidebar: Failed to fetch tags', err)
628
+ } finally {
629
+ this.tagsLoading = false
630
+ }
631
+ },
632
+
633
+ async addTag() {
634
+ if (!this.newTagName.trim() || !this.register || !this.schema) return
635
+ this.tagSaving = true
636
+ try {
637
+ const newTags = [...this.tags, this.newTagName.trim()]
638
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}`
639
+ await fetch(url, {
640
+ method: 'PATCH',
641
+ headers: buildHeaders(),
642
+ body: JSON.stringify({ tags: newTags }),
643
+ })
644
+ this.newTagName = ''
645
+ await this.fetchTags()
646
+ } catch (err) {
647
+ console.error('CnObjectSidebar: Failed to add tag', err)
648
+ } finally {
649
+ this.tagSaving = false
650
+ }
651
+ },
652
+
653
+ async removeTag(tag) {
654
+ if (!this.register || !this.schema) return
655
+ const tagName = tag.name || tag
656
+ try {
657
+ const newTags = this.tags.filter(t => (t.name || t) !== tagName)
658
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}`
659
+ await fetch(url, {
660
+ method: 'PATCH',
661
+ headers: buildHeaders(),
662
+ body: JSON.stringify({ tags: newTags }),
663
+ })
664
+ this.tags = newTags
665
+ } catch (err) {
666
+ console.error('CnObjectSidebar: Failed to remove tag', err)
667
+ }
668
+ },
669
+
670
+ // --- Tasks (via OpenRegister → CalDAV VTODO API) ---
671
+ async fetchTasks() {
672
+ if (!this.register || !this.schema) return
673
+ this.tasksLoading = true
674
+ try {
675
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/tasks`
676
+ const response = await fetch(url, { headers: buildHeaders() })
677
+ if (response.ok) {
678
+ const data = await response.json()
679
+ this.tasks = data.results || data || []
680
+ }
681
+ } catch (err) {
682
+ console.error('CnObjectSidebar: Failed to fetch tasks', err)
683
+ } finally {
684
+ this.tasksLoading = false
685
+ }
686
+ },
687
+
688
+ // --- Audit Trail ---
689
+ async fetchAuditTrails() {
690
+ if (!this.register || !this.schema) return
691
+ this.auditTrailLoading = true
692
+ try {
693
+ const url = `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails`
694
+ const response = await fetch(url, { headers: buildHeaders() })
695
+ if (response.ok) {
696
+ const data = await response.json()
697
+ this.auditTrails = data.results || data || []
698
+ }
699
+ } catch (err) {
700
+ console.error('CnObjectSidebar: Failed to fetch audit trails', err)
701
+ } finally {
702
+ this.auditTrailLoading = false
703
+ }
704
+ },
705
+
706
+ // --- Helpers ---
707
+ formatFileSize(bytes) {
708
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
709
+ if (!bytes || bytes === 0) return 'n/a'
710
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
711
+ if (i === 0) return '< 1 KB'
712
+ return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]
713
+ },
714
+
715
+ formatDate(dateStr) {
716
+ if (!dateStr) return ''
717
+ try {
718
+ return new Date(dateStr).toLocaleString(undefined, {
719
+ year: 'numeric',
720
+ month: 'short',
721
+ day: 'numeric',
722
+ hour: '2-digit',
723
+ minute: '2-digit',
724
+ })
725
+ } catch {
726
+ return dateStr
727
+ }
728
+ },
729
+ },
730
+ }
731
+ </script>
732
+
733
+ <style scoped>
734
+ .cn-sidebar-tab {
735
+ padding: 12px;
736
+ }
737
+
738
+ .cn-sidebar-tab__action {
739
+ margin-bottom: 16px;
740
+ }
741
+
742
+ .cn-sidebar-tab__action--row {
743
+ display: flex;
744
+ gap: 8px;
745
+ align-items: flex-end;
746
+ }
747
+
748
+ .cn-sidebar-tab__upload-btn {
749
+ display: block;
750
+ cursor: pointer;
751
+ }
752
+
753
+ .cn-sidebar-tab__file-input {
754
+ display: none;
755
+ }
756
+
757
+ .cn-sidebar-tab__textarea {
758
+ width: 100%;
759
+ padding: 8px;
760
+ border: 1px solid var(--color-border);
761
+ border-radius: var(--border-radius);
762
+ resize: vertical;
763
+ font-family: inherit;
764
+ font-size: 13px;
765
+ margin-bottom: 8px;
766
+ background: var(--color-main-background);
767
+ color: var(--color-main-text);
768
+ }
769
+
770
+ .cn-sidebar-tab__textarea:focus {
771
+ border-color: var(--color-primary-element);
772
+ outline: none;
773
+ }
774
+
775
+ .cn-sidebar-tab__empty {
776
+ text-align: center;
777
+ padding: 24px 12px;
778
+ color: var(--color-text-maxcontrast);
779
+ font-size: 13px;
780
+ }
781
+
782
+ .cn-sidebar-tab__list {
783
+ display: flex;
784
+ flex-direction: column;
785
+ gap: 2px;
786
+ }
787
+
788
+ /* Notes */
789
+ .cn-sidebar-tab__note {
790
+ padding: 10px 12px;
791
+ border-bottom: 1px solid var(--color-border);
792
+ position: relative;
793
+ }
794
+
795
+ .cn-sidebar-tab__note:last-child {
796
+ border-bottom: none;
797
+ }
798
+
799
+ .cn-sidebar-tab__note-header {
800
+ display: flex;
801
+ justify-content: space-between;
802
+ align-items: baseline;
803
+ margin-bottom: 4px;
804
+ }
805
+
806
+ .cn-sidebar-tab__note-header strong {
807
+ font-size: 13px;
808
+ }
809
+
810
+ .cn-sidebar-tab__note-time {
811
+ font-size: 11px;
812
+ color: var(--color-text-maxcontrast);
813
+ }
814
+
815
+ .cn-sidebar-tab__note-body {
816
+ font-size: 13px;
817
+ margin: 0;
818
+ white-space: pre-wrap;
819
+ word-break: break-word;
820
+ }
821
+
822
+ .cn-sidebar-tab__note-delete {
823
+ position: absolute;
824
+ top: 8px;
825
+ right: 4px;
826
+ opacity: 0;
827
+ transition: opacity 0.15s ease;
828
+ }
829
+
830
+ .cn-sidebar-tab__note:hover .cn-sidebar-tab__note-delete {
831
+ opacity: 1;
832
+ }
833
+
834
+ /* Tags */
835
+ .cn-sidebar-tab__tags {
836
+ display: flex;
837
+ flex-wrap: wrap;
838
+ gap: 6px;
839
+ padding: 4px 0;
840
+ }
841
+
842
+ .cn-sidebar-tab__tag {
843
+ display: inline-flex;
844
+ align-items: center;
845
+ gap: 4px;
846
+ padding: 3px 8px;
847
+ border-radius: var(--border-radius-pill, 16px);
848
+ background: var(--color-primary-element-light, rgba(0, 130, 201, 0.1));
849
+ color: var(--color-primary-element);
850
+ font-size: 12px;
851
+ font-weight: 500;
852
+ }
853
+
854
+ .cn-sidebar-tab__tag-remove {
855
+ display: flex;
856
+ align-items: center;
857
+ justify-content: center;
858
+ background: none;
859
+ border: none;
860
+ padding: 0;
861
+ cursor: pointer;
862
+ color: var(--color-primary-element);
863
+ opacity: 0.6;
864
+ border-radius: 50%;
865
+ }
866
+
867
+ .cn-sidebar-tab__tag-remove:hover {
868
+ opacity: 1;
869
+ background: rgba(0, 0, 0, 0.08);
870
+ }
871
+
872
+ /* Tasks */
873
+ .cn-sidebar-tab__task-done {
874
+ color: var(--color-success);
875
+ }
876
+ </style>