@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +79416 -7715
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +3583 -504
  6. package/dist/nextcloud-vue.esm.js +79343 -7692
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/l10n/en.json +164 -0
  9. package/l10n/nl.json +164 -0
  10. package/package.json +104 -63
  11. package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
  12. package/src/components/CnActionsBar/index.js +1 -0
  13. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -0
  14. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  15. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
  17. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  18. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  19. package/src/components/CnCard/CnCard.vue +415 -0
  20. package/src/components/CnCard/index.js +1 -0
  21. package/src/components/CnCardGrid/CnCardGrid.vue +156 -152
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  24. package/src/components/CnCellRenderer/index.js +1 -1
  25. package/src/components/CnChartWidget/CnChartWidget.vue +346 -0
  26. package/src/components/CnChartWidget/index.js +1 -0
  27. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  28. package/src/components/CnConfigurationCard/index.js +1 -1
  29. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  30. package/src/components/CnContextMenu/index.js +1 -0
  31. package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -0
  32. package/src/components/CnCopyDialog/index.js +1 -0
  33. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  34. package/src/components/CnDashboardGrid/index.js +1 -0
  35. package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -0
  36. package/src/components/CnDashboardPage/index.js +1 -0
  37. package/src/components/CnDataTable/CnDataTable.vue +362 -354
  38. package/src/components/CnDataTable/index.js +1 -1
  39. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  40. package/src/components/CnDeleteDialog/index.js +1 -0
  41. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  42. package/src/components/CnDetailCard/index.js +1 -0
  43. package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -0
  44. package/src/components/CnDetailGrid/index.js +1 -0
  45. package/src/components/CnDetailPage/CnDetailPage.vue +432 -0
  46. package/src/components/CnDetailPage/index.js +1 -0
  47. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -223
  48. package/src/components/CnFacetSidebar/index.js +1 -1
  49. package/src/components/CnFilterBar/CnFilterBar.vue +153 -152
  50. package/src/components/CnFilterBar/index.js +1 -1
  51. package/src/components/CnFormDialog/CnFormDialog.vue +1047 -0
  52. package/src/components/CnFormDialog/index.js +1 -0
  53. package/src/components/CnIcon/CnIcon.vue +89 -0
  54. package/src/components/CnIcon/index.js +1 -0
  55. package/src/components/CnIndexPage/CnIndexPage.vue +980 -682
  56. package/src/components/CnIndexPage/index.js +1 -1
  57. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -0
  58. package/src/components/CnIndexSidebar/index.js +1 -0
  59. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  60. package/src/components/CnInfoWidget/index.js +1 -0
  61. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  62. package/src/components/CnItemCard/index.js +1 -0
  63. package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -0
  64. package/src/components/CnJsonViewer/index.js +1 -0
  65. package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -89
  66. package/src/components/CnKpiGrid/index.js +1 -1
  67. package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -160
  68. package/src/components/CnMassActionBar/index.js +1 -1
  69. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -320
  70. package/src/components/CnMassCopyDialog/index.js +1 -1
  71. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -238
  72. package/src/components/CnMassDeleteDialog/index.js +1 -1
  73. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -190
  74. package/src/components/CnMassExportDialog/index.js +1 -1
  75. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -491
  76. package/src/components/CnMassImportDialog/index.js +1 -1
  77. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  78. package/src/components/CnNoteCard/index.js +1 -0
  79. package/src/components/CnNotesCard/CnNotesCard.vue +416 -0
  80. package/src/components/CnNotesCard/index.js +1 -0
  81. package/src/components/CnObjectCard/CnObjectCard.vue +294 -292
  82. package/src/components/CnObjectCard/index.js +1 -1
  83. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -0
  84. package/src/components/CnObjectDataWidget/index.js +1 -0
  85. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -0
  86. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  87. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -0
  88. package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -0
  89. package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -0
  90. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -0
  91. package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -0
  92. package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -0
  93. package/src/components/CnObjectSidebar/index.js +6 -0
  94. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  95. package/src/components/CnPageHeader/index.js +1 -0
  96. package/src/components/CnPagination/CnPagination.vue +253 -252
  97. package/src/components/CnPagination/index.js +1 -1
  98. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  99. package/src/components/CnProgressBar/index.js +1 -0
  100. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -0
  101. package/src/components/CnRegisterMapping/index.js +1 -0
  102. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  103. package/src/components/CnRowActions/index.js +1 -1
  104. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -0
  106. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  107. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  108. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  109. package/src/components/CnSchemaFormDialog/index.js +1 -0
  110. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  111. package/src/components/CnSettingsCard/index.js +1 -1
  112. package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -266
  113. package/src/components/CnSettingsSection/index.js +1 -1
  114. package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -366
  115. package/src/components/CnStatsBlock/index.js +1 -1
  116. package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -0
  117. package/src/components/CnStatsPanel/index.js +1 -0
  118. package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -77
  119. package/src/components/CnStatusBadge/index.js +1 -1
  120. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -0
  121. package/src/components/CnTabbedFormDialog/index.js +1 -0
  122. package/src/components/CnTableWidget/CnTableWidget.vue +333 -0
  123. package/src/components/CnTableWidget/index.js +1 -0
  124. package/src/components/CnTasksCard/CnTasksCard.vue +374 -0
  125. package/src/components/CnTasksCard/index.js +1 -0
  126. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  127. package/src/components/CnTileWidget/index.js +1 -0
  128. package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -0
  129. package/src/components/CnTimelineStages/index.js +1 -0
  130. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -0
  131. package/src/components/CnUserActionMenu/index.js +1 -0
  132. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -312
  133. package/src/components/CnVersionInfoCard/index.js +1 -1
  134. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  135. package/src/components/CnWidgetRenderer/index.js +1 -0
  136. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -0
  137. package/src/components/CnWidgetWrapper/index.js +1 -0
  138. package/src/components/index.js +57 -25
  139. package/src/composables/index.js +5 -3
  140. package/src/composables/useContextMenu.js +126 -0
  141. package/src/composables/useDashboardView.js +286 -0
  142. package/src/composables/useDetailView.js +290 -132
  143. package/src/composables/useListView.js +364 -153
  144. package/src/composables/useSubResource.js +142 -142
  145. package/src/constants/metadata.js +30 -0
  146. package/src/css/CnSchemaFormDialog.css +546 -0
  147. package/src/css/__sample_nextcloud_tokens.css +110 -0
  148. package/src/css/actions-bar.css +54 -0
  149. package/src/css/badge.css +83 -51
  150. package/src/css/card.css +129 -128
  151. package/src/css/context-menu.css +20 -0
  152. package/src/css/dashboard.css +70 -0
  153. package/src/css/detail-page.css +235 -0
  154. package/src/css/detail.css +68 -68
  155. package/src/css/index-page.css +44 -0
  156. package/src/css/index-sidebar.css +193 -0
  157. package/src/css/index.css +17 -8
  158. package/src/css/layout.css +90 -90
  159. package/src/css/page-header.css +35 -0
  160. package/src/css/pagination.css +72 -72
  161. package/src/css/table.css +142 -143
  162. package/src/css/timeline-stages.css +220 -0
  163. package/src/css/utilities.css +46 -46
  164. package/src/index.js +95 -50
  165. package/src/l10n/index.js +12 -0
  166. package/src/mixins/gridLayout.js +118 -0
  167. package/src/store/createCrudStore.d.ts +350 -0
  168. package/src/store/createCrudStore.js +413 -0
  169. package/src/store/createSubResourcePlugin.js +125 -135
  170. package/src/store/index.js +4 -3
  171. package/src/store/pluginMerge.js +55 -0
  172. package/src/store/plugins/auditTrails.js +357 -17
  173. package/src/store/plugins/files.js +250 -186
  174. package/src/store/plugins/index.js +8 -4
  175. package/src/store/plugins/lifecycle.js +180 -180
  176. package/src/store/plugins/logs.d.ts +22 -0
  177. package/src/store/plugins/logs.js +172 -0
  178. package/src/store/plugins/registerMapping.js +195 -0
  179. package/src/store/plugins/relations.js +68 -68
  180. package/src/store/plugins/search.js +385 -0
  181. package/src/store/plugins/selection.js +104 -0
  182. package/src/store/useObjectStore.js +793 -625
  183. package/src/types/auditTrail.d.ts +32 -32
  184. package/src/types/file.d.ts +23 -23
  185. package/src/types/index.d.ts +67 -35
  186. package/src/types/notification.d.ts +36 -36
  187. package/src/types/object.d.ts +40 -40
  188. package/src/types/organisation.d.ts +41 -41
  189. package/src/types/register.d.ts +25 -25
  190. package/src/types/schema.d.ts +39 -39
  191. package/src/types/shared.d.ts +79 -79
  192. package/src/types/source.d.ts +14 -14
  193. package/src/types/task.d.ts +31 -31
  194. package/src/utils/errors.js +96 -96
  195. package/src/utils/getTheme.js +9 -0
  196. package/src/utils/headers.js +80 -44
  197. package/src/utils/id.js +13 -0
  198. package/src/utils/index.js +4 -3
  199. package/src/utils/schema.js +423 -287
  200. package/src/utils/widgetVisibility.js +162 -0
  201. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  202. package/src/components/CnDetailViewLayout/index.js +0 -1
  203. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  204. package/src/components/CnEmptyState/index.js +0 -1
  205. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  206. package/src/components/CnListViewLayout/index.js +0 -1
  207. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  208. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -0,0 +1,287 @@
1
+ <template>
2
+ <div class="cn-sidebar-tab">
3
+ <!-- Upload error -->
4
+ <div v-if="uploadError" class="cn-sidebar-tab__upload-error">
5
+ {{ uploadError }}
6
+ </div>
7
+
8
+ <!-- File drop zone -->
9
+ <div
10
+ class="cn-sidebar-tab__dropzone"
11
+ :class="{ 'cn-sidebar-tab__dropzone--active': isDragOver }"
12
+ @click="triggerFileInput"
13
+ @dragover.prevent="onDragOver"
14
+ @dragleave.prevent="onDragLeave"
15
+ @drop.prevent="onDrop">
16
+ <input
17
+ ref="fileInput"
18
+ type="file"
19
+ multiple
20
+ class="cn-sidebar-tab__file-input"
21
+ @change="onFileUpload">
22
+ <Upload :size="24" class="cn-sidebar-tab__dropzone-icon" />
23
+ <span class="cn-sidebar-tab__dropzone-text">{{ dropZoneLabel }}</span>
24
+ </div>
25
+
26
+ <!-- File list -->
27
+ <NcLoadingIcon v-if="loading" />
28
+ <div v-else-if="files.length === 0" class="cn-sidebar-tab__empty">
29
+ {{ noFilesLabel }}
30
+ </div>
31
+ <div v-else class="cn-sidebar-tab__list">
32
+ <NcListItem
33
+ v-for="file in files"
34
+ :key="file.id"
35
+ :name="file.name || file.title"
36
+ :bold="false"
37
+ :force-display-actions="true">
38
+ <template #icon>
39
+ <FileOutline :size="32" />
40
+ </template>
41
+ <template #subname>
42
+ {{ formatFileSize(file.size) }}
43
+ </template>
44
+ <template #actions>
45
+ <NcActionButton @click="openFile(file)">
46
+ <template #icon>
47
+ <OpenInNew :size="20" />
48
+ </template>
49
+ {{ openLabel }}
50
+ </NcActionButton>
51
+ <NcActionButton @click="deleteFile(file)">
52
+ <template #icon>
53
+ <Delete :size="20" />
54
+ </template>
55
+ {{ deleteLabel }}
56
+ </NcActionButton>
57
+ </template>
58
+ </NcListItem>
59
+ </div>
60
+ <NcButton
61
+ v-if="files.length < total"
62
+ type="tertiary"
63
+ :wide="true"
64
+ :disabled="loadingMore"
65
+ class="cn-sidebar-tab__load-more"
66
+ @click="loadMore">
67
+ <template v-if="loadingMore" #icon>
68
+ <NcLoadingIcon :size="20" />
69
+ </template>
70
+ {{ loadingMore ? '' : loadMoreLabel }}
71
+ </NcButton>
72
+ </div>
73
+ </template>
74
+
75
+ <script>
76
+ import { translate as t } from '@nextcloud/l10n'
77
+ import { NcButton, NcListItem, NcActionButton, NcLoadingIcon } from '@nextcloud/vue'
78
+ import Upload from 'vue-material-design-icons/Upload.vue'
79
+ import FileOutline from 'vue-material-design-icons/FileOutline.vue'
80
+ import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
81
+ import Delete from 'vue-material-design-icons/Delete.vue'
82
+ import { buildHeaders } from '../../utils/index.js'
83
+
84
+ export default {
85
+ name: 'CnFilesTab',
86
+
87
+ components: { NcButton, NcListItem, NcActionButton, NcLoadingIcon, Upload, FileOutline, OpenInNew, Delete },
88
+
89
+ props: {
90
+ objectId: { type: String, required: true },
91
+ register: { type: String, default: '' },
92
+ schema: { type: String, default: '' },
93
+ apiBase: { type: String, default: '/apps/openregister/api' },
94
+ dropZoneLabel: { type: String, default: () => t('nextcloud-vue', 'Drop files here or click to browse') },
95
+ noFilesLabel: { type: String, default: () => t('nextcloud-vue', 'No files attached') },
96
+ openLabel: { type: String, default: () => t('nextcloud-vue', 'Open') },
97
+ deleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
98
+ loadMoreLabel: { type: String, default: () => t('nextcloud-vue', 'Load more') },
99
+ },
100
+
101
+ data() {
102
+ return {
103
+ files: [],
104
+ loading: false,
105
+ loadingMore: false,
106
+ isDragOver: false,
107
+ uploadError: '',
108
+ page: 1,
109
+ total: 0,
110
+ limit: 20,
111
+ }
112
+ },
113
+
114
+ watch: {
115
+ objectId: {
116
+ immediate: true,
117
+ handler(id) { if (id) this.fetchFiles() },
118
+ },
119
+ },
120
+
121
+ methods: {
122
+ async fetchFiles(append = false) {
123
+ if (!this.register || !this.schema) return
124
+ if (append) { this.loadingMore = true } else { this.loading = true }
125
+ try {
126
+ const params = new URLSearchParams({ limit: this.limit, _page: this.page })
127
+ const response = await fetch(
128
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/files?${params}`,
129
+ { headers: buildHeaders() },
130
+ )
131
+ if (response.ok) {
132
+ const data = await response.json()
133
+ const results = data.results || data || []
134
+ this.files = append ? [...this.files, ...results] : results
135
+ this.total = data.total || this.files.length
136
+ }
137
+ } catch (err) {
138
+ console.error('CnFilesTab: Failed to fetch files', err)
139
+ } finally {
140
+ this.loading = false
141
+ this.loadingMore = false
142
+ }
143
+ },
144
+
145
+ loadMore() {
146
+ this.page++
147
+ this.fetchFiles(true)
148
+ },
149
+
150
+ triggerFileInput() {
151
+ this.$refs.fileInput?.click()
152
+ },
153
+
154
+ onDragOver() { this.isDragOver = true },
155
+ onDragLeave() { this.isDragOver = false },
156
+
157
+ onDrop(event) {
158
+ this.isDragOver = false
159
+ const droppedFiles = event.dataTransfer?.files
160
+ if (droppedFiles?.length) this.uploadFiles(droppedFiles)
161
+ },
162
+
163
+ async onFileUpload(event) {
164
+ const inputFiles = event.target.files
165
+ if (!inputFiles?.length) return
166
+ await this.uploadFiles(inputFiles)
167
+ if (this.$refs.fileInput) this.$refs.fileInput.value = ''
168
+ },
169
+
170
+ async uploadFiles(fileList) {
171
+ if (!fileList?.length || !this.register || !this.schema) return
172
+ this.uploadError = ''
173
+ const formData = new FormData()
174
+ for (const file of fileList) {
175
+ formData.append('files[]', file)
176
+ }
177
+
178
+ this.loading = true
179
+ try {
180
+ const response = await fetch(
181
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/filesMultipart`,
182
+ {
183
+ method: 'POST',
184
+ headers: { requesttoken: OC?.requestToken || '', 'OCS-APIREQUEST': 'true' },
185
+ body: formData,
186
+ },
187
+ )
188
+ if (!response.ok) {
189
+ const data = await response.json().catch(() => ({}))
190
+ this.uploadError = data.error || `Upload failed (${response.status})`
191
+ return
192
+ }
193
+ await this.fetchFiles()
194
+ } catch (err) {
195
+ console.error('CnFilesTab: Failed to upload file', err)
196
+ this.uploadError = 'Upload failed: could not connect to server'
197
+ } finally {
198
+ this.loading = false
199
+ }
200
+ },
201
+
202
+ openFile(file) {
203
+ if (file.accessUrl) {
204
+ window.open(file.accessUrl, '_blank')
205
+ } else if (file.id) {
206
+ const dirPath = file.path ? file.path.substring(0, file.path.lastIndexOf('/')) : ''
207
+ const cleanPath = dirPath.replace(/^\/admin\/files\//, '/')
208
+ window.open(`/index.php/apps/files/files/${file.id}?dir=${encodeURIComponent(cleanPath)}&openfile=true`, '_blank')
209
+ }
210
+ },
211
+
212
+ async deleteFile(file) {
213
+ if (!this.register || !this.schema) return
214
+ try {
215
+ await fetch(
216
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/files/${file.id}`,
217
+ { method: 'DELETE', headers: buildHeaders() },
218
+ )
219
+ this.files = this.files.filter(f => f.id !== file.id)
220
+ } catch (err) {
221
+ console.error('CnFilesTab: Failed to delete file', err)
222
+ }
223
+ },
224
+
225
+ formatFileSize(bytes) {
226
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
227
+ if (!bytes || bytes === 0) return 'n/a'
228
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
229
+ if (i === 0) return '< 1 KB'
230
+ return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]
231
+ },
232
+ },
233
+ }
234
+ </script>
235
+
236
+ <style scoped>
237
+ .cn-sidebar-tab { padding: 12px; }
238
+
239
+ .cn-sidebar-tab__upload-error {
240
+ padding: 8px 12px;
241
+ margin-bottom: 8px;
242
+ border-radius: var(--border-radius, 4px);
243
+ background-color: var(--color-error-light, rgba(229, 57, 53, 0.1));
244
+ color: var(--color-error, #e53935);
245
+ font-size: 13px;
246
+ }
247
+
248
+ .cn-sidebar-tab__dropzone {
249
+ display: flex;
250
+ flex-direction: column;
251
+ align-items: center;
252
+ justify-content: center;
253
+ gap: 8px;
254
+ padding: 20px 12px;
255
+ margin-bottom: 16px;
256
+ border: 2px dashed var(--color-border);
257
+ border-radius: var(--border-radius-large, 8px);
258
+ cursor: pointer;
259
+ transition: border-color 0.15s ease, background-color 0.15s ease;
260
+ }
261
+
262
+ .cn-sidebar-tab__dropzone:hover {
263
+ border-color: var(--color-primary-element);
264
+ background-color: var(--color-primary-element-light, rgba(0, 130, 201, 0.08));
265
+ }
266
+
267
+ .cn-sidebar-tab__dropzone--active {
268
+ border-color: var(--color-primary-element);
269
+ background-color: var(--color-primary-element-light, rgba(0, 130, 201, 0.12));
270
+ }
271
+
272
+ .cn-sidebar-tab__dropzone-icon { color: var(--color-text-maxcontrast); }
273
+ .cn-sidebar-tab__dropzone--active .cn-sidebar-tab__dropzone-icon,
274
+ .cn-sidebar-tab__dropzone:hover .cn-sidebar-tab__dropzone-icon { color: var(--color-primary-element); }
275
+ .cn-sidebar-tab__dropzone-text { font-size: 13px; color: var(--color-text-maxcontrast); }
276
+ .cn-sidebar-tab__file-input { display: none; }
277
+
278
+ .cn-sidebar-tab__empty {
279
+ text-align: center;
280
+ padding: 24px 12px;
281
+ color: var(--color-text-maxcontrast);
282
+ font-size: 13px;
283
+ }
284
+
285
+ .cn-sidebar-tab__list { display: flex; flex-direction: column; gap: 2px; }
286
+ .cn-sidebar-tab__load-more { margin-top: 8px; }
287
+ </style>
@@ -0,0 +1,250 @@
1
+ <template>
2
+ <div class="cn-sidebar-tab">
3
+ <!-- Add / Edit note -->
4
+ <div class="cn-sidebar-tab__action">
5
+ <textarea
6
+ v-model="newNoteText"
7
+ class="cn-sidebar-tab__textarea"
8
+ :placeholder="addNotePlaceholder"
9
+ rows="3" />
10
+ <div class="cn-sidebar-tab__action--row">
11
+ <NcButton
12
+ v-if="editingNoteId"
13
+ type="tertiary"
14
+ @click="cancelEdit">
15
+ {{ cancelLabel }}
16
+ </NcButton>
17
+ <NcButton
18
+ type="primary"
19
+ :disabled="!newNoteText.trim() || saving"
20
+ @click="editingNoteId ? saveEdit() : addNote()">
21
+ <template #icon>
22
+ <Send :size="20" />
23
+ </template>
24
+ {{ editingNoteId ? saveLabel : addNoteLabel }}
25
+ </NcButton>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Notes list -->
30
+ <NcLoadingIcon v-if="loading" />
31
+ <div v-else-if="notes.length === 0" class="cn-sidebar-tab__empty">
32
+ {{ noNotesLabel }}
33
+ </div>
34
+ <div v-else class="cn-sidebar-tab__list">
35
+ <NcListItem
36
+ v-for="note in notes"
37
+ :key="note.id"
38
+ :name="note.actorDisplayName || note.author || 'Unknown'"
39
+ :bold="false"
40
+ :force-display-actions="true">
41
+ <template #icon>
42
+ <CommentTextOutline :size="32" />
43
+ </template>
44
+ <template #subname>
45
+ {{ note.message || note.content }}
46
+ </template>
47
+ <template #details>
48
+ {{ formatDate(note.creationDateTime || note.created) }}
49
+ </template>
50
+ <template v-if="canDelete(note)" #actions>
51
+ <NcActionButton @click="startEdit(note)">
52
+ <template #icon>
53
+ <Pencil :size="20" />
54
+ </template>
55
+ {{ editLabel }}
56
+ </NcActionButton>
57
+ <NcActionButton @click="deleteNote(note)">
58
+ <template #icon>
59
+ <Delete :size="20" />
60
+ </template>
61
+ {{ deleteLabel }}
62
+ </NcActionButton>
63
+ </template>
64
+ </NcListItem>
65
+ </div>
66
+ </div>
67
+ </template>
68
+
69
+ <script>
70
+ import { translate as t } from '@nextcloud/l10n'
71
+ import { NcButton, NcListItem, NcActionButton, NcLoadingIcon } from '@nextcloud/vue'
72
+ import CommentTextOutline from 'vue-material-design-icons/CommentTextOutline.vue'
73
+ import Send from 'vue-material-design-icons/Send.vue'
74
+ import Pencil from 'vue-material-design-icons/Pencil.vue'
75
+ import Delete from 'vue-material-design-icons/Delete.vue'
76
+ import { buildHeaders } from '../../utils/index.js'
77
+
78
+ export default {
79
+ name: 'CnNotesTab',
80
+
81
+ components: { NcButton, NcListItem, NcActionButton, NcLoadingIcon, CommentTextOutline, Send, Pencil, Delete },
82
+
83
+ props: {
84
+ objectId: { type: String, required: true },
85
+ register: { type: String, default: '' },
86
+ schema: { type: String, default: '' },
87
+ apiBase: { type: String, default: '/apps/openregister/api' },
88
+ addNoteLabel: { type: String, default: () => t('nextcloud-vue', 'Add note') },
89
+ addNotePlaceholder: { type: String, default: () => t('nextcloud-vue', 'Write a note...') },
90
+ editLabel: { type: String, default: () => t('nextcloud-vue', 'Edit') },
91
+ saveLabel: { type: String, default: () => t('nextcloud-vue', 'Save') },
92
+ cancelLabel: { type: String, default: () => t('nextcloud-vue', 'Cancel') },
93
+ deleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
94
+ noNotesLabel: { type: String, default: () => t('nextcloud-vue', 'No notes yet') },
95
+ },
96
+
97
+ data() {
98
+ return {
99
+ notes: [],
100
+ loading: false,
101
+ newNoteText: '',
102
+ saving: false,
103
+ editingNoteId: null,
104
+ }
105
+ },
106
+
107
+ watch: {
108
+ objectId: {
109
+ immediate: true,
110
+ handler(id) { if (id) this.fetchNotes() },
111
+ },
112
+ },
113
+
114
+ methods: {
115
+ async fetchNotes() {
116
+ if (!this.register || !this.schema) return
117
+ this.loading = true
118
+ try {
119
+ const response = await fetch(
120
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes`,
121
+ { headers: buildHeaders() },
122
+ )
123
+ if (response.ok) {
124
+ const data = await response.json()
125
+ this.notes = data.results || data || []
126
+ }
127
+ } catch (err) {
128
+ console.error('CnNotesTab: Failed to fetch notes', err)
129
+ } finally {
130
+ this.loading = false
131
+ }
132
+ },
133
+
134
+ async addNote() {
135
+ if (!this.newNoteText.trim()) return
136
+ this.saving = true
137
+ try {
138
+ await fetch(
139
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes`,
140
+ {
141
+ method: 'POST',
142
+ headers: buildHeaders(),
143
+ body: JSON.stringify({ message: this.newNoteText.trim() }),
144
+ },
145
+ )
146
+ this.newNoteText = ''
147
+ await this.fetchNotes()
148
+ } catch (err) {
149
+ console.error('CnNotesTab: Failed to add note', err)
150
+ } finally {
151
+ this.saving = false
152
+ }
153
+ },
154
+
155
+ startEdit(note) {
156
+ this.editingNoteId = note.id
157
+ this.newNoteText = note.message || note.content || ''
158
+ },
159
+
160
+ cancelEdit() {
161
+ this.editingNoteId = null
162
+ this.newNoteText = ''
163
+ },
164
+
165
+ async saveEdit() {
166
+ if (!this.newNoteText.trim() || !this.editingNoteId) return
167
+ this.saving = true
168
+ try {
169
+ await fetch(
170
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes/${this.editingNoteId}`,
171
+ {
172
+ method: 'PUT',
173
+ headers: buildHeaders(),
174
+ body: JSON.stringify({ message: this.newNoteText.trim() }),
175
+ },
176
+ )
177
+ this.editingNoteId = null
178
+ this.newNoteText = ''
179
+ await this.fetchNotes()
180
+ } catch (err) {
181
+ console.error('CnNotesTab: Failed to update note', err)
182
+ } finally {
183
+ this.saving = false
184
+ }
185
+ },
186
+
187
+ canDelete(note) {
188
+ return note.actorId === OC?.currentUser || note.author === OC?.currentUser
189
+ },
190
+
191
+ async deleteNote(note) {
192
+ try {
193
+ await fetch(
194
+ `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/notes/${note.id}`,
195
+ { method: 'DELETE', headers: buildHeaders() },
196
+ )
197
+ this.notes = this.notes.filter(n => n.id !== note.id)
198
+ } catch (err) {
199
+ console.error('CnNotesTab: Failed to delete note', err)
200
+ }
201
+ },
202
+
203
+ formatDate(dateStr) {
204
+ if (!dateStr) return ''
205
+ try {
206
+ return new Date(dateStr).toLocaleString(undefined, {
207
+ year: 'numeric',
208
+ month: 'short',
209
+ day: 'numeric',
210
+ hour: '2-digit',
211
+ minute: '2-digit',
212
+ })
213
+ } catch { return dateStr }
214
+ },
215
+ },
216
+ }
217
+ </script>
218
+
219
+ <style scoped>
220
+ .cn-sidebar-tab { padding: 12px; }
221
+ .cn-sidebar-tab__action { margin-bottom: 16px; }
222
+ .cn-sidebar-tab__action--row { display: flex; gap: 8px; align-items: flex-end; }
223
+
224
+ .cn-sidebar-tab__textarea {
225
+ width: 100%;
226
+ padding: 8px;
227
+ border: 1px solid var(--color-border);
228
+ border-radius: var(--border-radius);
229
+ resize: vertical;
230
+ font-family: inherit;
231
+ font-size: 13px;
232
+ margin-bottom: 8px;
233
+ background: var(--color-main-background);
234
+ color: var(--color-main-text);
235
+ }
236
+
237
+ .cn-sidebar-tab__textarea:focus {
238
+ border-color: var(--color-primary-element);
239
+ outline: none;
240
+ }
241
+
242
+ .cn-sidebar-tab__empty {
243
+ text-align: center;
244
+ padding: 24px 12px;
245
+ color: var(--color-text-maxcontrast);
246
+ font-size: 13px;
247
+ }
248
+
249
+ .cn-sidebar-tab__list { display: flex; flex-direction: column; gap: 2px; }
250
+ </style>