@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.7

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 (82) hide show
  1. package/dist/nextcloud-vue.cjs.js +13606 -1918
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +1238 -270
  4. package/dist/nextcloud-vue.esm.js +13548 -1880
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +9 -4
  7. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  8. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  11. package/src/components/CnCard/CnCard.vue +415 -0
  12. package/src/components/CnCard/index.js +1 -0
  13. package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
  14. package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
  16. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
  17. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
  18. package/src/components/CnDataTable/CnDataTable.vue +6 -2
  19. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
  20. package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
  21. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  22. package/src/components/CnDetailGrid/index.js +1 -0
  23. package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
  24. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
  25. package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
  26. package/src/components/CnIcon/CnIcon.vue +1 -1
  27. package/src/components/CnIndexPage/CnIndexPage.vue +51 -9
  28. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
  29. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  30. package/src/components/CnInfoWidget/index.js +1 -0
  31. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  32. package/src/components/CnJsonViewer/index.js +1 -0
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  34. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
  35. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
  36. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
  38. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  39. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  40. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  41. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
  43. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  44. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  45. package/src/components/CnObjectSidebar/index.js +5 -0
  46. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  47. package/src/components/CnProgressBar/index.js +1 -0
  48. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
  50. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  51. package/src/components/CnStatsPanel/index.js +1 -0
  52. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  53. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
  54. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  55. package/src/components/CnTableWidget/index.js +1 -0
  56. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
  57. package/src/components/index.js +11 -0
  58. package/src/composables/useDashboardView.js +58 -12
  59. package/src/composables/useDetailView.js +3 -2
  60. package/src/composables/useListView.js +7 -6
  61. package/src/composables/useSubResource.js +3 -3
  62. package/src/css/badge.css +32 -0
  63. package/src/css/card.css +1 -0
  64. package/src/css/detail-page.css +74 -7
  65. package/src/index.js +16 -0
  66. package/src/mixins/gridLayout.js +118 -0
  67. package/src/store/createCrudStore.js +360 -0
  68. package/src/store/createSubResourcePlugin.js +5 -15
  69. package/src/store/index.js +1 -0
  70. package/src/store/plugins/auditTrails.js +346 -6
  71. package/src/store/plugins/lifecycle.js +4 -4
  72. package/src/store/plugins/registerMapping.js +18 -8
  73. package/src/store/plugins/relations.js +1 -1
  74. package/src/store/plugins/search.js +21 -8
  75. package/src/store/useObjectStore.js +30 -36
  76. package/src/utils/getTheme.js +9 -0
  77. package/src/utils/headers.js +13 -3
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
  81. package/src/components/CnObjectCard/eslint-setup.md +0 -235
  82. package/src/components/CnObjectCard/package.json-or.json +0 -132
@@ -22,60 +22,11 @@
22
22
  <Paperclip :size="20" />
23
23
  </template>
24
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>
25
+ <CnFilesTab
26
+ :object-id="objectId"
27
+ :register="register"
28
+ :schema="schema"
29
+ :api-base="apiBase" />
79
30
  </slot>
80
31
  </NcAppSidebarTab>
81
32
 
@@ -89,52 +40,11 @@
89
40
  <CommentTextOutline :size="20" />
90
41
  </template>
91
42
  <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>
43
+ <CnNotesTab
44
+ :object-id="objectId"
45
+ :register="register"
46
+ :schema="schema"
47
+ :api-base="apiBase" />
138
48
  </slot>
139
49
  </NcAppSidebarTab>
140
50
 
@@ -148,43 +58,11 @@
148
58
  <TagOutline :size="20" />
149
59
  </template>
150
60
  <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>
61
+ <CnTagsTab
62
+ :object-id="objectId"
63
+ :register="register"
64
+ :schema="schema"
65
+ :api-base="apiBase" />
188
66
  </slot>
189
67
  </NcAppSidebarTab>
190
68
 
@@ -198,27 +76,11 @@
198
76
  <CheckboxMarkedOutline :size="20" />
199
77
  </template>
200
78
  <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>
79
+ <CnTasksTab
80
+ :object-id="objectId"
81
+ :register="register"
82
+ :schema="schema"
83
+ :api-base="apiBase" />
222
84
  </slot>
223
85
  </NcAppSidebarTab>
224
86
 
@@ -232,28 +94,11 @@
232
94
  <History :size="20" />
233
95
  </template>
234
96
  <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>
97
+ <CnAuditTrailTab
98
+ :object-id="objectId"
99
+ :register="register"
100
+ :schema="schema"
101
+ :api-base="apiBase" />
257
102
  </slot>
258
103
  </NcAppSidebarTab>
259
104
 
@@ -263,38 +108,26 @@
263
108
  </template>
264
109
 
265
110
  <script>
266
- import {
267
- NcAppSidebar,
268
- NcAppSidebarTab,
269
- NcButton,
270
- NcListItem,
271
- NcActionButton,
272
- NcLoadingIcon,
273
- NcTextField,
274
- } from '@nextcloud/vue'
111
+ import { NcAppSidebar, NcAppSidebarTab } from '@nextcloud/vue'
275
112
 
276
113
  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
114
  import CommentTextOutline from 'vue-material-design-icons/CommentTextOutline.vue'
282
- import Send from 'vue-material-design-icons/Send.vue'
283
115
  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
116
  import CheckboxMarkedOutline from 'vue-material-design-icons/CheckboxMarkedOutline.vue'
287
- import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline.vue'
288
117
  import History from 'vue-material-design-icons/History.vue'
289
118
 
290
- import { buildHeaders } from '../../utils/index.js'
119
+ import CnFilesTab from './CnFilesTab.vue'
120
+ import CnNotesTab from './CnNotesTab.vue'
121
+ import CnTagsTab from './CnTagsTab.vue'
122
+ import CnTasksTab from './CnTasksTab.vue'
123
+ import CnAuditTrailTab from './CnAuditTrailTab.vue'
291
124
 
292
125
  /**
293
126
  * CnObjectSidebar — Right sidebar for entity detail pages.
294
127
  *
295
128
  * Provides standardized tabs for generic object functionality (Files, Notes, Tags,
296
129
  * Tasks, Audit Trail) that integrate with OpenRegister API endpoints bridging to
297
- * Nextcloud-native APIs.
130
+ * Nextcloud-native APIs. Each tab is a self-contained component.
298
131
  *
299
132
  * @example Basic usage
300
133
  * <CnObjectSidebar
@@ -322,24 +155,16 @@ export default {
322
155
  components: {
323
156
  NcAppSidebar,
324
157
  NcAppSidebarTab,
325
- NcButton,
326
- NcListItem,
327
- NcActionButton,
328
- NcLoadingIcon,
329
- NcTextField,
330
158
  Paperclip,
331
- Upload,
332
- FileOutline,
333
- OpenInNew,
334
- Delete,
335
159
  CommentTextOutline,
336
- Send,
337
160
  TagOutline,
338
- Plus,
339
- Close,
340
161
  CheckboxMarkedOutline,
341
- CheckboxBlankOutline,
342
162
  History,
163
+ CnFilesTab,
164
+ CnNotesTab,
165
+ CnTagsTab,
166
+ CnTasksTab,
167
+ CnAuditTrailTab,
343
168
  },
344
169
 
345
170
  props: {
@@ -353,12 +178,12 @@ export default {
353
178
  type: String,
354
179
  required: true,
355
180
  },
356
- /** OpenRegister register ID (for file/audit trail operations) */
181
+ /** OpenRegister register ID */
357
182
  register: {
358
183
  type: String,
359
184
  default: '',
360
185
  },
361
- /** OpenRegister schema ID (for file/audit trail operations) */
186
+ /** OpenRegister schema ID */
362
187
  schema: {
363
188
  type: String,
364
189
  default: '',
@@ -379,6 +204,11 @@ export default {
379
204
  default: '',
380
205
  },
381
206
  /** Sidebar subtitle */
207
+ subtitle: {
208
+ type: String,
209
+ default: '',
210
+ },
211
+ /** @deprecated Use subtitle instead */
382
212
  subtitleProp: {
383
213
  type: String,
384
214
  default: '',
@@ -395,17 +225,6 @@ export default {
395
225
  tagsLabel: { type: String, default: 'Tags' },
396
226
  tasksLabel: { type: String, default: 'Tasks' },
397
227
  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
228
  },
410
229
 
411
230
  emits: ['update:open'],
@@ -413,25 +232,6 @@ export default {
413
232
  data() {
414
233
  return {
415
234
  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
235
  }
436
236
  },
437
237
 
@@ -440,22 +240,7 @@ export default {
440
240
  return this.title || this.objectType || 'Details'
441
241
  },
442
242
  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)
243
+ return this.subtitle || this.subtitleProp || ''
459
244
  },
460
245
  },
461
246
 
@@ -463,414 +248,6 @@ export default {
463
248
  isTabHidden(tabId) {
464
249
  return this.hiddenTabs.includes(tabId)
465
250
  },
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
251
  },
730
252
  }
731
253
  </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>