@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.8
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.
- package/dist/nextcloud-vue.cjs.js +13575 -2374
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +1238 -270
- package/dist/nextcloud-vue.esm.js +13517 -2336
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +11 -7
- package/src/components/CnActionsBar/CnActionsBar.vue +20 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
- package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
- package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +6 -2
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
- package/src/components/CnIcon/CnIcon.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +63 -9
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
- package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +5 -0
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
- package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
- package/src/components/index.js +11 -0
- package/src/composables/useDashboardView.js +58 -12
- package/src/composables/useDetailView.js +3 -2
- package/src/composables/useListView.js +7 -6
- package/src/composables/useSubResource.js +3 -3
- package/src/css/badge.css +32 -0
- package/src/css/card.css +1 -0
- package/src/css/detail-page.css +74 -7
- package/src/index.js +16 -0
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +5 -15
- package/src/store/index.js +1 -0
- package/src/store/plugins/auditTrails.js +346 -6
- package/src/store/plugins/lifecycle.js +4 -4
- package/src/store/plugins/registerMapping.js +18 -8
- package/src/store/plugins/relations.js +1 -1
- package/src/store/plugins/search.js +21 -8
- package/src/store/useObjectStore.js +30 -36
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +13 -3
- package/src/utils/index.js +1 -0
- package/src/utils/schema.js +3 -3
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnObjectCard/eslint-setup.md +0 -235
- 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
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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
|
|
181
|
+
/** OpenRegister register ID */
|
|
357
182
|
register: {
|
|
358
183
|
type: String,
|
|
359
184
|
default: '',
|
|
360
185
|
},
|
|
361
|
-
/** OpenRegister schema ID
|
|
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.
|
|
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>
|