@conduction/nextcloud-vue 0.1.0-beta.3 → 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 (152) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs.js +60455 -8755
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +2062 -528
  5. package/dist/nextcloud-vue.esm.js +60411 -8731
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +75 -62
  8. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  9. package/src/components/CnActionsBar/index.js +1 -1
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  15. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  16. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  17. package/src/components/CnCardGrid/index.js +1 -1
  18. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  19. package/src/components/CnCellRenderer/index.js +1 -1
  20. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  21. package/src/components/CnChartWidget/index.js +1 -0
  22. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  25. package/src/components/CnDashboardGrid/index.js +1 -0
  26. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  27. package/src/components/CnDashboardPage/index.js +1 -0
  28. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  29. package/src/components/CnDataTable/index.js +1 -1
  30. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  31. package/src/components/CnDetailCard/index.js +1 -0
  32. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  33. package/src/components/CnDetailPage/index.js +1 -0
  34. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  35. package/src/components/CnFacetSidebar/index.js +1 -1
  36. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  37. package/src/components/CnFilterBar/index.js +1 -1
  38. package/src/components/CnIcon/CnIcon.vue +89 -89
  39. package/src/components/CnIcon/index.js +1 -1
  40. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  41. package/src/components/CnIndexPage/index.js +1 -1
  42. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  43. package/src/components/CnIndexSidebar/index.js +1 -1
  44. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  45. package/src/components/CnItemCard/index.js +1 -0
  46. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  49. package/src/components/CnMassActionBar/index.js +1 -1
  50. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  51. package/src/components/CnMassCopyDialog/index.js +1 -1
  52. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  53. package/src/components/CnMassDeleteDialog/index.js +1 -1
  54. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  55. package/src/components/CnMassExportDialog/index.js +1 -1
  56. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  57. package/src/components/CnMassImportDialog/index.js +1 -1
  58. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  59. package/src/components/CnNoteCard/index.js +1 -0
  60. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  61. package/src/components/CnNotesCard/index.js +1 -0
  62. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  63. package/src/components/CnObjectCard/index.js +1 -1
  64. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  65. package/src/components/CnObjectSidebar/index.js +1 -0
  66. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  67. package/src/components/CnPageHeader/index.js +1 -1
  68. package/src/components/CnPagination/CnPagination.vue +252 -252
  69. package/src/components/CnPagination/index.js +1 -1
  70. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  71. package/src/components/CnRowActions/index.js +1 -1
  72. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  73. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  77. package/src/components/CnSchemaFormDialog/index.js +1 -0
  78. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  79. package/src/components/CnSettingsCard/index.js +1 -1
  80. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  81. package/src/components/CnSettingsSection/index.js +1 -1
  82. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  83. package/src/components/CnStatsBlock/index.js +1 -1
  84. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  85. package/src/components/CnStatusBadge/index.js +1 -1
  86. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  87. package/src/components/CnTabbedFormDialog/index.js +1 -0
  88. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  89. package/src/components/CnTasksCard/index.js +1 -0
  90. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  91. package/src/components/CnTileWidget/index.js +1 -0
  92. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  93. package/src/components/CnTimelineStages/index.js +1 -0
  94. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  95. package/src/components/CnUserActionMenu/index.js +1 -0
  96. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  97. package/src/components/CnVersionInfoCard/index.js +1 -1
  98. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  99. package/src/components/CnWidgetRenderer/index.js +1 -0
  100. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  101. package/src/components/CnWidgetWrapper/index.js +1 -0
  102. package/src/components/index.js +43 -29
  103. package/src/composables/index.js +4 -3
  104. package/src/composables/useDashboardView.js +240 -0
  105. package/src/composables/useDetailView.js +289 -132
  106. package/src/composables/useListView.js +363 -362
  107. package/src/composables/useSubResource.js +142 -142
  108. package/src/constants/metadata.js +30 -30
  109. package/src/css/CnSchemaFormDialog.css +546 -0
  110. package/src/css/__sample_nextcloud_tokens.css +110 -0
  111. package/src/css/actions-bar.css +48 -48
  112. package/src/css/badge.css +51 -51
  113. package/src/css/card.css +128 -128
  114. package/src/css/dashboard.css +70 -0
  115. package/src/css/detail-page.css +168 -0
  116. package/src/css/detail.css +68 -68
  117. package/src/css/index-page.css +44 -32
  118. package/src/css/index-sidebar.css +193 -187
  119. package/src/css/index.css +16 -12
  120. package/src/css/layout.css +90 -90
  121. package/src/css/page-header.css +33 -33
  122. package/src/css/pagination.css +72 -72
  123. package/src/css/table.css +142 -142
  124. package/src/css/timeline-stages.css +218 -0
  125. package/src/css/utilities.css +46 -46
  126. package/src/index.js +72 -53
  127. package/src/store/createSubResourcePlugin.js +135 -135
  128. package/src/store/index.js +3 -3
  129. package/src/store/plugins/auditTrails.js +17 -17
  130. package/src/store/plugins/files.js +250 -186
  131. package/src/store/plugins/index.js +7 -5
  132. package/src/store/plugins/lifecycle.js +180 -180
  133. package/src/store/plugins/relations.js +68 -68
  134. package/src/store/plugins/search.js +372 -0
  135. package/src/store/plugins/selection.js +104 -0
  136. package/src/store/useObjectStore.js +829 -686
  137. package/src/types/auditTrail.d.ts +32 -32
  138. package/src/types/file.d.ts +23 -23
  139. package/src/types/index.d.ts +35 -35
  140. package/src/types/notification.d.ts +36 -36
  141. package/src/types/object.d.ts +40 -40
  142. package/src/types/organisation.d.ts +41 -41
  143. package/src/types/register.d.ts +25 -25
  144. package/src/types/schema.d.ts +39 -39
  145. package/src/types/shared.d.ts +79 -79
  146. package/src/types/source.d.ts +14 -14
  147. package/src/types/task.d.ts +31 -31
  148. package/src/utils/errors.js +96 -96
  149. package/src/utils/headers.js +68 -50
  150. package/src/utils/id.js +13 -0
  151. package/src/utils/index.js +3 -3
  152. 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>