@conduction/nextcloud-vue 0.1.0-beta.4 → 0.1.0-beta.6
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 +67614 -0
- package/dist/nextcloud-vue.cjs.js +9559 -8983
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +1231 -1231
- package/dist/nextcloud-vue.esm.js +9559 -8983
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +14 -5
- package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -418
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
- package/src/components/CnDataTable/CnDataTable.vue +349 -349
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
- package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
- package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
- package/src/components/CnItemCard/CnItemCard.vue +132 -132
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
- package/src/components/CnObjectCard/eslint-setup.md +235 -0
- package/src/components/CnObjectCard/package.json-or.json +132 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
- package/src/components/CnPagination/CnPagination.vue +252 -252
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
- package/src/components/CnRowActions/CnRowActions.vue +95 -73
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
- package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
- package/src/index.js +1 -1
- package/src/types/notification.d.ts +13 -13
- package/src/types/organisation.d.ts +15 -15
- package/src/types/schema.d.ts +13 -13
- package/src/types/task.d.ts +6 -6
- package/src/utils/headers.js +5 -3
|
@@ -1,876 +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>
|
|
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>
|