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

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 (78) hide show
  1. package/dist/nextcloud-vue.cjs +67614 -0
  2. package/dist/nextcloud-vue.cjs.js +13518 -13617
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.cjs.map +1 -0
  5. package/dist/nextcloud-vue.css +1796 -1800
  6. package/dist/nextcloud-vue.esm.js +13518 -13617
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +3 -2
  9. package/src/components/CnActionsBar/CnActionsBar.vue +254 -254
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -570
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -422
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
  15. package/src/components/CnCard/CnCard.vue +415 -415
  16. package/src/components/CnCardGrid/CnCardGrid.vue +156 -156
  17. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  18. package/src/components/CnChartWidget/CnChartWidget.vue +346 -346
  19. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  20. package/src/components/CnContextMenu/CnContextMenu.vue +142 -142
  21. package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -266
  22. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -229
  23. package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -397
  24. package/src/components/CnDataTable/CnDataTable.vue +362 -362
  25. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -177
  26. package/src/components/CnDetailCard/CnDetailCard.vue +225 -225
  27. package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -256
  28. package/src/components/CnDetailPage/CnDetailPage.vue +432 -432
  29. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -234
  30. package/src/components/CnFilterBar/CnFilterBar.vue +153 -153
  31. package/src/components/CnFormDialog/CnFormDialog.vue +1047 -1047
  32. package/src/components/CnIcon/CnIcon.vue +89 -89
  33. package/src/components/CnIndexPage/CnIndexPage.vue +981 -980
  34. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -536
  35. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -219
  36. package/src/components/CnItemCard/CnItemCard.vue +134 -134
  37. package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -312
  38. package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -93
  39. package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -161
  40. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -327
  41. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -245
  42. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -191
  43. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -494
  44. package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
  45. package/src/components/CnNotesCard/CnNotesCard.vue +416 -416
  46. package/src/components/CnObjectCard/CnObjectCard.vue +294 -294
  47. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -854
  48. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -289
  49. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -369
  50. package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -287
  51. package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -250
  52. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -255
  53. package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -259
  54. package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -483
  55. package/src/components/CnPageHeader/CnPageHeader.vue +61 -61
  56. package/src/components/CnPagination/CnPagination.vue +253 -253
  57. package/src/components/CnProgressBar/CnProgressBar.vue +262 -262
  58. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -793
  59. package/src/components/CnRowActions/CnRowActions.vue +95 -95
  60. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
  61. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -788
  62. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
  63. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
  64. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
  65. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  66. package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -267
  67. package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -437
  68. package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -321
  69. package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -90
  70. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -545
  71. package/src/components/CnTableWidget/CnTableWidget.vue +333 -333
  72. package/src/components/CnTasksCard/CnTasksCard.vue +374 -374
  73. package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
  74. package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -294
  75. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -436
  76. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -313
  77. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
  78. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -248
@@ -1,545 +1,545 @@
1
- <template>
2
- <NcDialog
3
- :name="resolvedTitle"
4
- :size="size"
5
- :can-close="!loading"
6
- @closing="$emit('close')">
7
- <!-- Result phase (standard mode, not create-another) -->
8
- <div v-if="result !== null && !createAnother" class="cn-tabbed-form-dialog__result">
9
- <NcNoteCard v-if="result.success" type="success">
10
- {{ resolvedSuccessText }}
11
- </NcNoteCard>
12
- <NcNoteCard v-if="result.error" type="error">
13
- {{ result.error }}
14
- </NcNoteCard>
15
- </div>
16
-
17
- <!-- Form phase (or create-another mode where form stays visible) -->
18
- <div v-if="createAnother || result === null" class="cn-tabbed-form-dialog__form">
19
- <!-- Inline notifications for create-another mode -->
20
- <NcNoteCard v-if="createAnother && result && result.success" type="success">
21
- {{ resolvedSuccessText }}
22
- </NcNoteCard>
23
- <NcNoteCard v-if="result && result.error" type="error">
24
- {{ result.error }}
25
- </NcNoteCard>
26
-
27
- <!-- Optional content above tabs (e.g. metadata grid, detail cards) -->
28
- <slot name="above-tabs" :loading="loading" />
29
-
30
- <!-- Tabs -->
31
- <div class="cn-tabbed-form-dialog__tabs tabContainer">
32
- <BTabs
33
- v-model="activeTab"
34
- content-class="mt-3"
35
- justified
36
- @input="$emit('update:activeTab', $event)">
37
- <BTab
38
- v-for="tab in tabs"
39
- :key="tab.id"
40
- :disabled="tab.disabled">
41
- <template #title>
42
- <component :is="tab.icon" v-if="tab.icon" :size="16" />
43
- <span>{{ tab.title }}</span>
44
- </template>
45
- <div class="cn-tabbed-form-dialog__tab-content form-editor">
46
- <slot :name="'tab-' + tab.id" :loading="loading" />
47
- </div>
48
- </BTab>
49
- </BTabs>
50
- </div>
51
-
52
- <!-- Optional content below tabs (e.g. shared settings across all tabs) -->
53
- <slot name="below-tabs" :loading="loading" />
54
- </div>
55
-
56
- <template #actions>
57
- <!-- Create another checkbox (only in create mode) -->
58
- <NcCheckboxRadioSwitch
59
- v-if="showCreateAnother && isCreateMode"
60
- class="cn-tabbed-form-dialog__create-another"
61
- :disabled="loading"
62
- :checked.sync="createAnother">
63
- {{ createAnotherLabel }}
64
- </NcCheckboxRadioSwitch>
65
-
66
- <!-- Extra actions before Cancel -->
67
- <slot name="actions-left"
68
- :loading="loading"
69
- :is-create-mode="isCreateMode"
70
- :result="result" />
71
-
72
- <!-- Cancel / Close button -->
73
- <NcButton @click="handleClose">
74
- <template #icon>
75
- <Cancel :size="20" />
76
- </template>
77
- {{ result !== null && !createAnother ? closeLabel : cancelLabel }}
78
- </NcButton>
79
-
80
- <!-- Extra actions after primary -->
81
- <slot name="actions-right"
82
- :loading="loading"
83
- :is-create-mode="isCreateMode"
84
- :result="result" />
85
-
86
- <!-- Primary action button (Save / Create) -->
87
- <NcButton
88
- v-if="createAnother || result === null"
89
- type="primary"
90
- :disabled="loading || disableSave"
91
- @click="executeConfirm">
92
- <template #icon>
93
- <NcLoadingIcon v-if="loading" :size="20" />
94
- <slot v-else-if="$slots['confirm-icon']" name="confirm-icon" />
95
- <Plus v-else-if="isCreateMode" :size="20" />
96
- <ContentSaveOutline v-else :size="20" />
97
- </template>
98
- {{ resolvedConfirmLabel }}
99
- </NcButton>
100
- </template>
101
- </NcDialog>
102
- </template>
103
-
104
- <script>
105
- import { translate as t } from '@nextcloud/l10n'
106
- import {
107
- NcButton,
108
- NcDialog,
109
- NcLoadingIcon,
110
- NcNoteCard,
111
- NcCheckboxRadioSwitch,
112
- } from '@nextcloud/vue'
113
- import { BTabs, BTab } from 'bootstrap-vue'
114
-
115
- import Cancel from 'vue-material-design-icons/Cancel.vue'
116
- import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
117
- import Plus from 'vue-material-design-icons/Plus.vue'
118
-
119
- export default {
120
- name: 'CnTabbedFormDialog',
121
- components: {
122
- NcDialog,
123
- NcButton,
124
- NcLoadingIcon,
125
- NcNoteCard,
126
- NcCheckboxRadioSwitch,
127
- BTabs,
128
- BTab,
129
- Cancel,
130
- ContentSaveOutline,
131
- Plus,
132
- },
133
- props: {
134
- /**
135
- * Array of tab definitions. Each tab must have at least an `id` and `title`.
136
- * The optional `icon` field should be a Vue component reference (e.g. an imported MDI icon).
137
- * The optional `disabled` field prevents tab selection.
138
- *
139
- * @type {Array<{ id: string, title: string, icon?: object, disabled?: boolean }>}
140
- */
141
- tabs: {
142
- type: Array,
143
- required: true,
144
- validator: (tabs) => tabs.length > 0 && tabs.every(t => t.id && t.title),
145
- },
146
- /**
147
- * Existing item for edit mode. Pass null or undefined for create mode.
148
- * The component only checks for truthiness to determine create vs edit mode.
149
- *
150
- * @type {object|null}
151
- */
152
- item: {
153
- type: Object,
154
- default: null,
155
- },
156
- /**
157
- * Custom dialog title. When provided, overrides the auto-generated
158
- * "Create {entityName}" / "Edit {entityName}" title.
159
- *
160
- * @type {string}
161
- */
162
- dialogTitle: {
163
- type: String,
164
- default: '',
165
- },
166
- /**
167
- * Entity name used in auto-generated titles and success messages.
168
- * For example, "Organisation" produces "Create Organisation" and
169
- * "Organisation saved successfully".
170
- *
171
- * @type {string}
172
- */
173
- entityName: {
174
- type: String,
175
- default: () => t('nextcloud-vue', 'Item'),
176
- },
177
- /**
178
- * NcDialog size. One of 'small', 'normal', 'large', 'full'.
179
- *
180
- * @type {string}
181
- */
182
- size: {
183
- type: String,
184
- default: 'large',
185
- },
186
- /**
187
- * Whether to show the "Create Another" checkbox in create mode.
188
- * When checked and a save succeeds, the form stays open and a reset
189
- * event is emitted so the parent can clear form data.
190
- *
191
- * @type {boolean}
192
- */
193
- showCreateAnother: {
194
- type: Boolean,
195
- default: false,
196
- },
197
- /**
198
- * Whether the primary save/create button is disabled.
199
- * The parent controls validation externally.
200
- *
201
- * @type {boolean}
202
- */
203
- disableSave: {
204
- type: Boolean,
205
- default: false,
206
- },
207
- /**
208
- * Custom success message shown in the result NcNoteCard.
209
- * Defaults to "{entityName} saved successfully".
210
- *
211
- * @type {string}
212
- */
213
- successText: {
214
- type: String,
215
- default: '',
216
- },
217
- /**
218
- * Cancel button label.
219
- *
220
- * @type {string}
221
- */
222
- cancelLabel: {
223
- type: String,
224
- default: () => t('nextcloud-vue', 'Cancel'),
225
- },
226
- /**
227
- * Close button label shown in the result phase.
228
- *
229
- * @type {string}
230
- */
231
- closeLabel: {
232
- type: String,
233
- default: () => t('nextcloud-vue', 'Close'),
234
- },
235
- /**
236
- * Primary confirm button label. Defaults to "Create" in create mode
237
- * or "Save" in edit mode.
238
- *
239
- * @type {string}
240
- */
241
- confirmLabel: {
242
- type: String,
243
- default: '',
244
- },
245
- /**
246
- * Label for the "Create Another" checkbox.
247
- *
248
- * @type {string}
249
- */
250
- createAnotherLabel: {
251
- type: String,
252
- default: () => t('nextcloud-vue', 'Create another'),
253
- },
254
- },
255
- data() {
256
- return {
257
- /** @type {number} Current active tab index */
258
- activeTab: 0,
259
- /** @type {boolean} Whether the "create another" checkbox is checked */
260
- createAnother: false,
261
- /** @type {boolean} Whether an API operation is in progress */
262
- loading: false,
263
- /**
264
- * Result of the last operation.
265
- * null = form phase, { success: true } = success, { error: 'msg' } = error
266
- *
267
- * @type {{ success?: boolean, error?: string }|null}
268
- */
269
- result: null,
270
- /** @type {number|null} Timeout ID for auto-close after success */
271
- closeTimeout: null,
272
- /** @type {number|null} Timeout ID for clearing success in create-another mode */
273
- successClearTimeout: null,
274
- }
275
- },
276
- computed: {
277
- /**
278
- * Whether the dialog is in create mode (no existing item).
279
- *
280
- * @return {boolean}
281
- */
282
- isCreateMode() {
283
- return !this.item
284
- },
285
- /**
286
- * Resolved dialog title. Uses dialogTitle prop if provided,
287
- * otherwise auto-generates from entityName and mode.
288
- *
289
- * @return {string}
290
- */
291
- resolvedTitle() {
292
- if (this.dialogTitle) {
293
- return this.dialogTitle
294
- }
295
- return this.isCreateMode
296
- ? t('nextcloud-vue', 'Create {title}', { title: this.entityName })
297
- : t('nextcloud-vue', 'Edit {title}', { title: this.entityName })
298
- },
299
- /**
300
- * Resolved success text for NcNoteCard.
301
- *
302
- * @return {string}
303
- */
304
- resolvedSuccessText() {
305
- if (this.successText) {
306
- return this.successText
307
- }
308
- return t('nextcloud-vue', '{title} saved successfully.', { title: this.entityName })
309
- },
310
- /**
311
- * Resolved primary button label.
312
- *
313
- * @return {string}
314
- */
315
- resolvedConfirmLabel() {
316
- if (this.confirmLabel) {
317
- return this.confirmLabel
318
- }
319
- return this.isCreateMode ? t('nextcloud-vue', 'Create') : t('nextcloud-vue', 'Save')
320
- },
321
- },
322
- beforeDestroy() {
323
- clearTimeout(this.closeTimeout)
324
- clearTimeout(this.successClearTimeout)
325
- },
326
- methods: {
327
- /**
328
- * Set the result of the save operation. Call this from the parent
329
- * after the API call completes.
330
- *
331
- * When success is true and create-another is not checked, the dialog
332
- * auto-closes after 2 seconds. When create-another is checked,
333
- * the success message shows inline for 2 seconds, then clears and
334
- * emits the `reset` event.
335
- *
336
- * @param {{ success?: boolean, error?: string }} resultData - Result data to pass to the dialog
337
- * @public
338
- */
339
- setResult(resultData) {
340
- this.loading = false
341
- this.result = resultData
342
-
343
- if (resultData.success) {
344
- if (this.createAnother) {
345
- // Create-another mode: show success briefly, then reset
346
- this.successClearTimeout = setTimeout(() => {
347
- this.result = null
348
- this.activeTab = 0
349
- /**
350
- * Emitted after a successful save in create-another mode.
351
- * The parent should clear its form data.
352
- *
353
- * @event reset
354
- */
355
- this.$emit('reset')
356
- }, 2000)
357
- } else {
358
- // Standard mode: auto-close after 2 seconds
359
- this.closeTimeout = setTimeout(() => {
360
- /**
361
- * Emitted when the dialog should be closed.
362
- *
363
- * @event close
364
- */
365
- this.$emit('close')
366
- }, 2000)
367
- }
368
- }
369
- },
370
-
371
- /**
372
- * Reset the dialog to its initial form state.
373
- * Clears any result, resets loading, and returns to the first tab.
374
- *
375
- * @public
376
- */
377
- resetDialog() {
378
- clearTimeout(this.closeTimeout)
379
- clearTimeout(this.successClearTimeout)
380
- this.result = null
381
- this.loading = false
382
- this.activeTab = 0
383
- },
384
-
385
- /**
386
- * Handle the primary action button click.
387
- * Sets loading state and emits the confirm event.
388
- *
389
- * @private
390
- */
391
- executeConfirm() {
392
- this.loading = true
393
- this.result = null
394
- /**
395
- * Emitted when the user clicks Save/Create.
396
- * The parent should perform the API call and then call setResult().
397
- *
398
- * @event confirm
399
- */
400
- this.$emit('confirm')
401
- },
402
-
403
- /**
404
- * Handle close button click. Clears timeouts and emits close.
405
- *
406
- * @private
407
- */
408
- handleClose() {
409
- clearTimeout(this.closeTimeout)
410
- clearTimeout(this.successClearTimeout)
411
- this.result = null
412
- this.loading = false
413
- this.createAnother = false
414
- this.activeTab = 0
415
- this.$emit('close')
416
- },
417
- },
418
- }
419
- </script>
420
-
421
- <style scoped>
422
- /* Result phase container */
423
- .cn-tabbed-form-dialog__result {
424
- padding: 16px 0;
425
- }
426
-
427
- /* Form phase container */
428
- .cn-tabbed-form-dialog__form {
429
- display: flex;
430
- flex-direction: column;
431
- gap: 8px;
432
- }
433
-
434
- /* Tabs wrapper */
435
- .cn-tabbed-form-dialog__tabs {
436
- display: flex;
437
- flex-direction: column;
438
- gap: 12px;
439
- }
440
-
441
- /* Tab content area — also uses .form-editor for compatibility */
442
- .cn-tabbed-form-dialog__tab-content {
443
- display: flex;
444
- flex-direction: column;
445
- gap: 16px;
446
- padding: 16px 0;
447
- }
448
-
449
- /* Create another checkbox — push to the left in actions area */
450
- .cn-tabbed-form-dialog__create-another {
451
- margin-right: auto;
452
- }
453
-
454
- /* Bootstrap-Vue tab container styling */
455
- .tabContainer > * ul > li {
456
- display: flex;
457
- flex: 1;
458
- }
459
-
460
- .tabContainer > * ul > li:hover {
461
- background-color: var(--color-background-hover);
462
- }
463
-
464
- .tabContainer > * ul > li > a {
465
- flex: 1;
466
- text-align: center;
467
- }
468
-
469
- .tabContainer > * ul > li > .active {
470
- background: transparent !important;
471
- color: var(--color-main-text) !important;
472
- border-bottom: var(--default-grid-baseline) solid var(--color-primary-element) !important;
473
- }
474
-
475
- .tabContainer > * ul[role="tablist"] {
476
- display: flex;
477
- margin: 10px 8px 0 8px;
478
- justify-content: space-between;
479
- border-bottom: 1px solid var(--color-border);
480
- }
481
-
482
- .tabContainer > * ul[role="tablist"] > * a[role="tab"] {
483
- padding-inline-start: 10px;
484
- padding-inline-end: 10px;
485
- padding-block-start: 10px;
486
- padding-block-end: 10px;
487
- }
488
-
489
- .tabContainer > * div[role="tabpanel"] {
490
- margin-block-start: var(--OR-margin-10);
491
- }
492
-
493
- :deep(.nav-tabs) {
494
- border-bottom: 1px solid var(--color-border);
495
- margin-bottom: 15px;
496
- display: flex;
497
- }
498
-
499
- :deep(.nav-tabs .nav-item) {
500
- display: flex;
501
- flex: 1;
502
- }
503
-
504
- :deep(.nav-tabs .nav-link) {
505
- flex: 1;
506
- text-align: center;
507
- border: none;
508
- border-bottom: 2px solid transparent;
509
- color: var(--color-text-maxcontrast);
510
- padding: 8px 16px;
511
- display: flex;
512
- align-items: center;
513
- gap: 8px;
514
- justify-content: center;
515
- }
516
-
517
- :deep(.nav-tabs .nav-link.active) {
518
- color: var(--color-main-text);
519
- border-bottom: 2px solid var(--color-primary);
520
- background-color: transparent;
521
- }
522
-
523
- :deep(.nav-tabs .nav-link:hover) {
524
- border-bottom: 2px solid var(--color-border);
525
- }
526
-
527
- :deep(.nav-tabs .nav-link.disabled) {
528
- cursor: not-allowed;
529
- opacity: 0.5;
530
- color: var(--color-text-maxcontrast);
531
- pointer-events: auto;
532
- }
533
- :deep(.nav-tabs .nav-link.disabled *) {
534
- cursor: not-allowed;
535
- }
536
-
537
- :deep(.nav-tabs .nav-link.disabled:hover) {
538
- border-bottom: 2px solid transparent;
539
- }
540
-
541
- :deep(.tab-content) {
542
- padding: 16px;
543
- background-color: var(--color-main-background);
544
- }
545
- </style>
1
+ <template>
2
+ <NcDialog
3
+ :name="resolvedTitle"
4
+ :size="size"
5
+ :can-close="!loading"
6
+ @closing="$emit('close')">
7
+ <!-- Result phase (standard mode, not create-another) -->
8
+ <div v-if="result !== null && !createAnother" class="cn-tabbed-form-dialog__result">
9
+ <NcNoteCard v-if="result.success" type="success">
10
+ {{ resolvedSuccessText }}
11
+ </NcNoteCard>
12
+ <NcNoteCard v-if="result.error" type="error">
13
+ {{ result.error }}
14
+ </NcNoteCard>
15
+ </div>
16
+
17
+ <!-- Form phase (or create-another mode where form stays visible) -->
18
+ <div v-if="createAnother || result === null" class="cn-tabbed-form-dialog__form">
19
+ <!-- Inline notifications for create-another mode -->
20
+ <NcNoteCard v-if="createAnother && result && result.success" type="success">
21
+ {{ resolvedSuccessText }}
22
+ </NcNoteCard>
23
+ <NcNoteCard v-if="result && result.error" type="error">
24
+ {{ result.error }}
25
+ </NcNoteCard>
26
+
27
+ <!-- Optional content above tabs (e.g. metadata grid, detail cards) -->
28
+ <slot name="above-tabs" :loading="loading" />
29
+
30
+ <!-- Tabs -->
31
+ <div class="cn-tabbed-form-dialog__tabs tabContainer">
32
+ <BTabs
33
+ v-model="activeTab"
34
+ content-class="mt-3"
35
+ justified
36
+ @input="$emit('update:activeTab', $event)">
37
+ <BTab
38
+ v-for="tab in tabs"
39
+ :key="tab.id"
40
+ :disabled="tab.disabled">
41
+ <template #title>
42
+ <component :is="tab.icon" v-if="tab.icon" :size="16" />
43
+ <span>{{ tab.title }}</span>
44
+ </template>
45
+ <div class="cn-tabbed-form-dialog__tab-content form-editor">
46
+ <slot :name="'tab-' + tab.id" :loading="loading" />
47
+ </div>
48
+ </BTab>
49
+ </BTabs>
50
+ </div>
51
+
52
+ <!-- Optional content below tabs (e.g. shared settings across all tabs) -->
53
+ <slot name="below-tabs" :loading="loading" />
54
+ </div>
55
+
56
+ <template #actions>
57
+ <!-- Create another checkbox (only in create mode) -->
58
+ <NcCheckboxRadioSwitch
59
+ v-if="showCreateAnother && isCreateMode"
60
+ class="cn-tabbed-form-dialog__create-another"
61
+ :disabled="loading"
62
+ :checked.sync="createAnother">
63
+ {{ createAnotherLabel }}
64
+ </NcCheckboxRadioSwitch>
65
+
66
+ <!-- Extra actions before Cancel -->
67
+ <slot name="actions-left"
68
+ :loading="loading"
69
+ :is-create-mode="isCreateMode"
70
+ :result="result" />
71
+
72
+ <!-- Cancel / Close button -->
73
+ <NcButton @click="handleClose">
74
+ <template #icon>
75
+ <Cancel :size="20" />
76
+ </template>
77
+ {{ result !== null && !createAnother ? closeLabel : cancelLabel }}
78
+ </NcButton>
79
+
80
+ <!-- Extra actions after primary -->
81
+ <slot name="actions-right"
82
+ :loading="loading"
83
+ :is-create-mode="isCreateMode"
84
+ :result="result" />
85
+
86
+ <!-- Primary action button (Save / Create) -->
87
+ <NcButton
88
+ v-if="createAnother || result === null"
89
+ type="primary"
90
+ :disabled="loading || disableSave"
91
+ @click="executeConfirm">
92
+ <template #icon>
93
+ <NcLoadingIcon v-if="loading" :size="20" />
94
+ <slot v-else-if="$slots['confirm-icon']" name="confirm-icon" />
95
+ <Plus v-else-if="isCreateMode" :size="20" />
96
+ <ContentSaveOutline v-else :size="20" />
97
+ </template>
98
+ {{ resolvedConfirmLabel }}
99
+ </NcButton>
100
+ </template>
101
+ </NcDialog>
102
+ </template>
103
+
104
+ <script>
105
+ import { translate as t } from '@nextcloud/l10n'
106
+ import {
107
+ NcButton,
108
+ NcDialog,
109
+ NcLoadingIcon,
110
+ NcNoteCard,
111
+ NcCheckboxRadioSwitch,
112
+ } from '@nextcloud/vue'
113
+ import { BTabs, BTab } from 'bootstrap-vue'
114
+
115
+ import Cancel from 'vue-material-design-icons/Cancel.vue'
116
+ import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
117
+ import Plus from 'vue-material-design-icons/Plus.vue'
118
+
119
+ export default {
120
+ name: 'CnTabbedFormDialog',
121
+ components: {
122
+ NcDialog,
123
+ NcButton,
124
+ NcLoadingIcon,
125
+ NcNoteCard,
126
+ NcCheckboxRadioSwitch,
127
+ BTabs,
128
+ BTab,
129
+ Cancel,
130
+ ContentSaveOutline,
131
+ Plus,
132
+ },
133
+ props: {
134
+ /**
135
+ * Array of tab definitions. Each tab must have at least an `id` and `title`.
136
+ * The optional `icon` field should be a Vue component reference (e.g. an imported MDI icon).
137
+ * The optional `disabled` field prevents tab selection.
138
+ *
139
+ * @type {Array<{ id: string, title: string, icon?: object, disabled?: boolean }>}
140
+ */
141
+ tabs: {
142
+ type: Array,
143
+ required: true,
144
+ validator: (tabs) => tabs.length > 0 && tabs.every(t => t.id && t.title),
145
+ },
146
+ /**
147
+ * Existing item for edit mode. Pass null or undefined for create mode.
148
+ * The component only checks for truthiness to determine create vs edit mode.
149
+ *
150
+ * @type {object|null}
151
+ */
152
+ item: {
153
+ type: Object,
154
+ default: null,
155
+ },
156
+ /**
157
+ * Custom dialog title. When provided, overrides the auto-generated
158
+ * "Create {entityName}" / "Edit {entityName}" title.
159
+ *
160
+ * @type {string}
161
+ */
162
+ dialogTitle: {
163
+ type: String,
164
+ default: '',
165
+ },
166
+ /**
167
+ * Entity name used in auto-generated titles and success messages.
168
+ * For example, "Organisation" produces "Create Organisation" and
169
+ * "Organisation saved successfully".
170
+ *
171
+ * @type {string}
172
+ */
173
+ entityName: {
174
+ type: String,
175
+ default: () => t('nextcloud-vue', 'Item'),
176
+ },
177
+ /**
178
+ * NcDialog size. One of 'small', 'normal', 'large', 'full'.
179
+ *
180
+ * @type {string}
181
+ */
182
+ size: {
183
+ type: String,
184
+ default: 'large',
185
+ },
186
+ /**
187
+ * Whether to show the "Create Another" checkbox in create mode.
188
+ * When checked and a save succeeds, the form stays open and a reset
189
+ * event is emitted so the parent can clear form data.
190
+ *
191
+ * @type {boolean}
192
+ */
193
+ showCreateAnother: {
194
+ type: Boolean,
195
+ default: false,
196
+ },
197
+ /**
198
+ * Whether the primary save/create button is disabled.
199
+ * The parent controls validation externally.
200
+ *
201
+ * @type {boolean}
202
+ */
203
+ disableSave: {
204
+ type: Boolean,
205
+ default: false,
206
+ },
207
+ /**
208
+ * Custom success message shown in the result NcNoteCard.
209
+ * Defaults to "{entityName} saved successfully".
210
+ *
211
+ * @type {string}
212
+ */
213
+ successText: {
214
+ type: String,
215
+ default: '',
216
+ },
217
+ /**
218
+ * Cancel button label.
219
+ *
220
+ * @type {string}
221
+ */
222
+ cancelLabel: {
223
+ type: String,
224
+ default: () => t('nextcloud-vue', 'Cancel'),
225
+ },
226
+ /**
227
+ * Close button label shown in the result phase.
228
+ *
229
+ * @type {string}
230
+ */
231
+ closeLabel: {
232
+ type: String,
233
+ default: () => t('nextcloud-vue', 'Close'),
234
+ },
235
+ /**
236
+ * Primary confirm button label. Defaults to "Create" in create mode
237
+ * or "Save" in edit mode.
238
+ *
239
+ * @type {string}
240
+ */
241
+ confirmLabel: {
242
+ type: String,
243
+ default: '',
244
+ },
245
+ /**
246
+ * Label for the "Create Another" checkbox.
247
+ *
248
+ * @type {string}
249
+ */
250
+ createAnotherLabel: {
251
+ type: String,
252
+ default: () => t('nextcloud-vue', 'Create another'),
253
+ },
254
+ },
255
+ data() {
256
+ return {
257
+ /** @type {number} Current active tab index */
258
+ activeTab: 0,
259
+ /** @type {boolean} Whether the "create another" checkbox is checked */
260
+ createAnother: false,
261
+ /** @type {boolean} Whether an API operation is in progress */
262
+ loading: false,
263
+ /**
264
+ * Result of the last operation.
265
+ * null = form phase, { success: true } = success, { error: 'msg' } = error
266
+ *
267
+ * @type {{ success?: boolean, error?: string }|null}
268
+ */
269
+ result: null,
270
+ /** @type {number|null} Timeout ID for auto-close after success */
271
+ closeTimeout: null,
272
+ /** @type {number|null} Timeout ID for clearing success in create-another mode */
273
+ successClearTimeout: null,
274
+ }
275
+ },
276
+ computed: {
277
+ /**
278
+ * Whether the dialog is in create mode (no existing item).
279
+ *
280
+ * @return {boolean}
281
+ */
282
+ isCreateMode() {
283
+ return !this.item
284
+ },
285
+ /**
286
+ * Resolved dialog title. Uses dialogTitle prop if provided,
287
+ * otherwise auto-generates from entityName and mode.
288
+ *
289
+ * @return {string}
290
+ */
291
+ resolvedTitle() {
292
+ if (this.dialogTitle) {
293
+ return this.dialogTitle
294
+ }
295
+ return this.isCreateMode
296
+ ? t('nextcloud-vue', 'Create {title}', { title: this.entityName })
297
+ : t('nextcloud-vue', 'Edit {title}', { title: this.entityName })
298
+ },
299
+ /**
300
+ * Resolved success text for NcNoteCard.
301
+ *
302
+ * @return {string}
303
+ */
304
+ resolvedSuccessText() {
305
+ if (this.successText) {
306
+ return this.successText
307
+ }
308
+ return t('nextcloud-vue', '{title} saved successfully.', { title: this.entityName })
309
+ },
310
+ /**
311
+ * Resolved primary button label.
312
+ *
313
+ * @return {string}
314
+ */
315
+ resolvedConfirmLabel() {
316
+ if (this.confirmLabel) {
317
+ return this.confirmLabel
318
+ }
319
+ return this.isCreateMode ? t('nextcloud-vue', 'Create') : t('nextcloud-vue', 'Save')
320
+ },
321
+ },
322
+ beforeDestroy() {
323
+ clearTimeout(this.closeTimeout)
324
+ clearTimeout(this.successClearTimeout)
325
+ },
326
+ methods: {
327
+ /**
328
+ * Set the result of the save operation. Call this from the parent
329
+ * after the API call completes.
330
+ *
331
+ * When success is true and create-another is not checked, the dialog
332
+ * auto-closes after 2 seconds. When create-another is checked,
333
+ * the success message shows inline for 2 seconds, then clears and
334
+ * emits the `reset` event.
335
+ *
336
+ * @param {{ success?: boolean, error?: string }} resultData - Result data to pass to the dialog
337
+ * @public
338
+ */
339
+ setResult(resultData) {
340
+ this.loading = false
341
+ this.result = resultData
342
+
343
+ if (resultData.success) {
344
+ if (this.createAnother) {
345
+ // Create-another mode: show success briefly, then reset
346
+ this.successClearTimeout = setTimeout(() => {
347
+ this.result = null
348
+ this.activeTab = 0
349
+ /**
350
+ * Emitted after a successful save in create-another mode.
351
+ * The parent should clear its form data.
352
+ *
353
+ * @event reset
354
+ */
355
+ this.$emit('reset')
356
+ }, 2000)
357
+ } else {
358
+ // Standard mode: auto-close after 2 seconds
359
+ this.closeTimeout = setTimeout(() => {
360
+ /**
361
+ * Emitted when the dialog should be closed.
362
+ *
363
+ * @event close
364
+ */
365
+ this.$emit('close')
366
+ }, 2000)
367
+ }
368
+ }
369
+ },
370
+
371
+ /**
372
+ * Reset the dialog to its initial form state.
373
+ * Clears any result, resets loading, and returns to the first tab.
374
+ *
375
+ * @public
376
+ */
377
+ resetDialog() {
378
+ clearTimeout(this.closeTimeout)
379
+ clearTimeout(this.successClearTimeout)
380
+ this.result = null
381
+ this.loading = false
382
+ this.activeTab = 0
383
+ },
384
+
385
+ /**
386
+ * Handle the primary action button click.
387
+ * Sets loading state and emits the confirm event.
388
+ *
389
+ * @private
390
+ */
391
+ executeConfirm() {
392
+ this.loading = true
393
+ this.result = null
394
+ /**
395
+ * Emitted when the user clicks Save/Create.
396
+ * The parent should perform the API call and then call setResult().
397
+ *
398
+ * @event confirm
399
+ */
400
+ this.$emit('confirm')
401
+ },
402
+
403
+ /**
404
+ * Handle close button click. Clears timeouts and emits close.
405
+ *
406
+ * @private
407
+ */
408
+ handleClose() {
409
+ clearTimeout(this.closeTimeout)
410
+ clearTimeout(this.successClearTimeout)
411
+ this.result = null
412
+ this.loading = false
413
+ this.createAnother = false
414
+ this.activeTab = 0
415
+ this.$emit('close')
416
+ },
417
+ },
418
+ }
419
+ </script>
420
+
421
+ <style scoped>
422
+ /* Result phase container */
423
+ .cn-tabbed-form-dialog__result {
424
+ padding: 16px 0;
425
+ }
426
+
427
+ /* Form phase container */
428
+ .cn-tabbed-form-dialog__form {
429
+ display: flex;
430
+ flex-direction: column;
431
+ gap: 8px;
432
+ }
433
+
434
+ /* Tabs wrapper */
435
+ .cn-tabbed-form-dialog__tabs {
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: 12px;
439
+ }
440
+
441
+ /* Tab content area — also uses .form-editor for compatibility */
442
+ .cn-tabbed-form-dialog__tab-content {
443
+ display: flex;
444
+ flex-direction: column;
445
+ gap: 16px;
446
+ padding: 16px 0;
447
+ }
448
+
449
+ /* Create another checkbox — push to the left in actions area */
450
+ .cn-tabbed-form-dialog__create-another {
451
+ margin-right: auto;
452
+ }
453
+
454
+ /* Bootstrap-Vue tab container styling */
455
+ .tabContainer > * ul > li {
456
+ display: flex;
457
+ flex: 1;
458
+ }
459
+
460
+ .tabContainer > * ul > li:hover {
461
+ background-color: var(--color-background-hover);
462
+ }
463
+
464
+ .tabContainer > * ul > li > a {
465
+ flex: 1;
466
+ text-align: center;
467
+ }
468
+
469
+ .tabContainer > * ul > li > .active {
470
+ background: transparent !important;
471
+ color: var(--color-main-text) !important;
472
+ border-bottom: var(--default-grid-baseline) solid var(--color-primary-element) !important;
473
+ }
474
+
475
+ .tabContainer > * ul[role="tablist"] {
476
+ display: flex;
477
+ margin: 10px 8px 0 8px;
478
+ justify-content: space-between;
479
+ border-bottom: 1px solid var(--color-border);
480
+ }
481
+
482
+ .tabContainer > * ul[role="tablist"] > * a[role="tab"] {
483
+ padding-inline-start: 10px;
484
+ padding-inline-end: 10px;
485
+ padding-block-start: 10px;
486
+ padding-block-end: 10px;
487
+ }
488
+
489
+ .tabContainer > * div[role="tabpanel"] {
490
+ margin-block-start: var(--OR-margin-10);
491
+ }
492
+
493
+ :deep(.nav-tabs) {
494
+ border-bottom: 1px solid var(--color-border);
495
+ margin-bottom: 15px;
496
+ display: flex;
497
+ }
498
+
499
+ :deep(.nav-tabs .nav-item) {
500
+ display: flex;
501
+ flex: 1;
502
+ }
503
+
504
+ :deep(.nav-tabs .nav-link) {
505
+ flex: 1;
506
+ text-align: center;
507
+ border: none;
508
+ border-bottom: 2px solid transparent;
509
+ color: var(--color-text-maxcontrast);
510
+ padding: 8px 16px;
511
+ display: flex;
512
+ align-items: center;
513
+ gap: 8px;
514
+ justify-content: center;
515
+ }
516
+
517
+ :deep(.nav-tabs .nav-link.active) {
518
+ color: var(--color-main-text);
519
+ border-bottom: 2px solid var(--color-primary);
520
+ background-color: transparent;
521
+ }
522
+
523
+ :deep(.nav-tabs .nav-link:hover) {
524
+ border-bottom: 2px solid var(--color-border);
525
+ }
526
+
527
+ :deep(.nav-tabs .nav-link.disabled) {
528
+ cursor: not-allowed;
529
+ opacity: 0.5;
530
+ color: var(--color-text-maxcontrast);
531
+ pointer-events: auto;
532
+ }
533
+ :deep(.nav-tabs .nav-link.disabled *) {
534
+ cursor: not-allowed;
535
+ }
536
+
537
+ :deep(.nav-tabs .nav-link.disabled:hover) {
538
+ border-bottom: 2px solid transparent;
539
+ }
540
+
541
+ :deep(.tab-content) {
542
+ padding: 16px;
543
+ background-color: var(--color-main-background);
544
+ }
545
+ </style>