@conduction/nextcloud-vue 2.100.0 → 2.101.0
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/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +51 -7
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +104 -73
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
- package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
- package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +251 -71
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +317 -209
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +4 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +167 -20
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useEndpointSource.js +28 -6
- package/dist/esm/composables/useEndpointSource.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +37 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +35 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +769 -251
- package/dist/esm/utils/languageMapText.js +50 -0
- package/dist/esm/utils/languageMapText.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +3 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/mapList.js +71 -0
- package/dist/esm/utils/mapList.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8977 -6843
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +769 -251
- package/l10n/en.json +37 -1
- package/l10n/nl.json +35 -1
- package/package.json +1 -1
- package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
- package/src/components/CnDashboardPage/CnDashboardPage.vue +89 -20
- package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
- package/src/components/CnExportsWidget/index.js +7 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
- package/src/components/CnFilesBrowser/filesReview.js +57 -0
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
- package/src/components/CnGroupedCardsWidget/index.js +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +240 -4
- package/src/components/CnIndexPage/useNamedSource.js +4 -1
- package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
- package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
- package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
- package/src/components/CnSavedViewAlertWidget/index.js +7 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
- package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +3 -0
- package/src/composables/indexSources.js +166 -19
- package/src/composables/useEndpointSource.js +28 -6
- package/src/css/index-page.css +117 -0
- package/src/css/look-board.css +44 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +31 -1
- package/src/utils/languageMapText.js +46 -0
- package/src/utils/libraryWidgetKeys.js +3 -0
- package/src/utils/mapList.js +67 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -17,7 +17,30 @@
|
|
|
17
17
|
v-for="(row, index) in rows"
|
|
18
18
|
:key="row.value"
|
|
19
19
|
class="cn-ordered-list-field__row"
|
|
20
|
-
:
|
|
20
|
+
:class="{
|
|
21
|
+
'cn-ordered-list-field__row--dragging': dragIndex === index,
|
|
22
|
+
'cn-ordered-list-field__row--over': dropIndex === index && dragIndex !== index,
|
|
23
|
+
}"
|
|
24
|
+
:data-testid="`cn-ordered-list-row-${row.value}`"
|
|
25
|
+
@dragover.prevent="onDragOver(index)"
|
|
26
|
+
@drop.prevent="onDrop(index)">
|
|
27
|
+
<!-- A drag handle (DqPersoonlijkeInstellingen). It is a button: a
|
|
28
|
+
keyboard user moves the row with the arrow keys on it, and the
|
|
29
|
+
up and down buttons stay for anyone who neither drags nor uses
|
|
30
|
+
arrow keys. -->
|
|
31
|
+
<button
|
|
32
|
+
v-if="!disabled"
|
|
33
|
+
type="button"
|
|
34
|
+
class="cn-ordered-list-field__handle"
|
|
35
|
+
draggable="true"
|
|
36
|
+
:aria-label="t('nextcloud-vue', 'Reorder {item}: drag, or press the up and down arrow keys', { item: row.label })"
|
|
37
|
+
:data-testid="`cn-ordered-list-handle-${row.value}`"
|
|
38
|
+
@dragstart="onDragStart($event, index)"
|
|
39
|
+
@dragend="onDragEnd"
|
|
40
|
+
@keydown.up.prevent="move(index, index - 1, 'handle')"
|
|
41
|
+
@keydown.down.prevent="move(index, index + 1, 'handle')">
|
|
42
|
+
<DragVertical :size="20" />
|
|
43
|
+
</button>
|
|
21
44
|
<span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
|
|
22
45
|
<span v-if="row.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
|
|
23
46
|
<CnIcon :name="row.icon" :size="18" />
|
|
@@ -104,6 +127,7 @@ import { NcSelect } from '@nextcloud/vue'
|
|
|
104
127
|
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
105
128
|
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
106
129
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
130
|
+
import DragVertical from 'vue-material-design-icons/DragVertical.vue'
|
|
107
131
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
108
132
|
|
|
109
133
|
let orderedListUid = 0
|
|
@@ -134,7 +158,7 @@ let orderedListUid = 0
|
|
|
134
158
|
export default {
|
|
135
159
|
name: 'CnOrderedListField',
|
|
136
160
|
|
|
137
|
-
components: { ChevronDown, ChevronUp, Close, CnIcon, NcSelect },
|
|
161
|
+
components: { ChevronDown, ChevronUp, Close, CnIcon, DragVertical, NcSelect },
|
|
138
162
|
|
|
139
163
|
props: {
|
|
140
164
|
/** The values in their order. */
|
|
@@ -205,6 +229,12 @@ export default {
|
|
|
205
229
|
return {
|
|
206
230
|
labelId: `cn-ordered-list-label-${orderedListUid}`,
|
|
207
231
|
announcement: '',
|
|
232
|
+
/** The value whose handle takes focus after the next render, or null. */
|
|
233
|
+
pendingHandleFocus: null,
|
|
234
|
+
/** The index of the row being dragged, or -1. */
|
|
235
|
+
dragIndex: -1,
|
|
236
|
+
/** The index of the row the drag is over, or -1. */
|
|
237
|
+
dropIndex: -1,
|
|
208
238
|
}
|
|
209
239
|
},
|
|
210
240
|
|
|
@@ -244,6 +274,20 @@ export default {
|
|
|
244
274
|
},
|
|
245
275
|
},
|
|
246
276
|
|
|
277
|
+
watch: {
|
|
278
|
+
// The parent's new order arrives as new rows; the moved row's handle
|
|
279
|
+
// lost focus when its row moved in the DOM, so it takes it back.
|
|
280
|
+
rows: {
|
|
281
|
+
flush: 'post',
|
|
282
|
+
handler() {
|
|
283
|
+
if (this.pendingHandleFocus !== null) {
|
|
284
|
+
this.focusPendingHandle()
|
|
285
|
+
this.pendingHandleFocus = null
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
|
|
247
291
|
methods: {
|
|
248
292
|
t,
|
|
249
293
|
|
|
@@ -270,13 +314,15 @@ export default {
|
|
|
270
314
|
},
|
|
271
315
|
|
|
272
316
|
/**
|
|
273
|
-
* Move a row to another place and keep focus on the pressed
|
|
274
|
-
* where it still makes sense
|
|
317
|
+
* Move a row to another place and keep focus on the pressed control
|
|
318
|
+
* where it still makes sense: the row's handle after an arrow key on
|
|
319
|
+
* it, else the up or down button.
|
|
275
320
|
*
|
|
276
321
|
* @param {number} from Its index.
|
|
277
322
|
* @param {number} to The index it moves to.
|
|
323
|
+
* @param {string} [via] `handle` when the move came from the handle.
|
|
278
324
|
*/
|
|
279
|
-
move(from, to) {
|
|
325
|
+
move(from, to, via = '') {
|
|
280
326
|
if (to < 0 || to >= this.rows.length) {
|
|
281
327
|
return
|
|
282
328
|
}
|
|
@@ -286,7 +332,16 @@ export default {
|
|
|
286
332
|
const row = this.rows[from]
|
|
287
333
|
this.emitValue(values)
|
|
288
334
|
this.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })
|
|
335
|
+
if (via === 'handle') {
|
|
336
|
+
// Re-focused once the parent hands the new order back: moving the
|
|
337
|
+
// row in the DOM takes focus off its handle.
|
|
338
|
+
this.pendingHandleFocus = row.value
|
|
339
|
+
}
|
|
289
340
|
this.$nextTick(() => {
|
|
341
|
+
if (via === 'handle') {
|
|
342
|
+
this.focusPendingHandle()
|
|
343
|
+
return
|
|
344
|
+
}
|
|
290
345
|
const direction = to < from ? 'up' : 'down'
|
|
291
346
|
const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)
|
|
292
347
|
const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`)
|
|
@@ -296,6 +351,61 @@ export default {
|
|
|
296
351
|
})
|
|
297
352
|
},
|
|
298
353
|
|
|
354
|
+
/** Put focus back on the handle of the row just moved by its handle. */
|
|
355
|
+
focusPendingHandle() {
|
|
356
|
+
if (this.pendingHandleFocus === null) {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
const handle = this.$el.querySelector(`[data-testid="cn-ordered-list-handle-${this.pendingHandleFocus}"]`)
|
|
360
|
+
if (handle) {
|
|
361
|
+
handle.focus()
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Start dragging a row by its handle.
|
|
367
|
+
*
|
|
368
|
+
* @param {DragEvent} event The drag event.
|
|
369
|
+
* @param {number} index The row's index.
|
|
370
|
+
*/
|
|
371
|
+
onDragStart(event, index) {
|
|
372
|
+
this.dragIndex = index
|
|
373
|
+
if (event && event.dataTransfer) {
|
|
374
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
375
|
+
event.dataTransfer.setData('text/plain', String(this.rows[index].value))
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Mark the row a drag is over as where the row will land.
|
|
381
|
+
*
|
|
382
|
+
* @param {number} index The row's index.
|
|
383
|
+
*/
|
|
384
|
+
onDragOver(index) {
|
|
385
|
+
if (this.dragIndex !== -1) {
|
|
386
|
+
this.dropIndex = index
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Drop the dragged row in the place of the row it is over.
|
|
392
|
+
*
|
|
393
|
+
* @param {number} index The row's index.
|
|
394
|
+
*/
|
|
395
|
+
onDrop(index) {
|
|
396
|
+
const from = this.dragIndex
|
|
397
|
+
this.onDragEnd()
|
|
398
|
+
if (from !== -1 && from !== index) {
|
|
399
|
+
this.move(from, index, 'handle')
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
/** End a drag, dropped or not. */
|
|
404
|
+
onDragEnd() {
|
|
405
|
+
this.dragIndex = -1
|
|
406
|
+
this.dropIndex = -1
|
|
407
|
+
},
|
|
408
|
+
|
|
299
409
|
/**
|
|
300
410
|
* Remove a row.
|
|
301
411
|
*
|
|
@@ -371,6 +481,42 @@ export default {
|
|
|
371
481
|
border-top: 1px solid var(--color-border);
|
|
372
482
|
}
|
|
373
483
|
|
|
484
|
+
.cn-ordered-list-field__row--dragging {
|
|
485
|
+
opacity: 0.5;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* Where a dragged row will land: a primary line on the row's top edge. */
|
|
489
|
+
.cn-ordered-list-field__row--over {
|
|
490
|
+
box-shadow: inset 0 2px 0 var(--color-primary-element);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.cn-ordered-list-field__handle {
|
|
494
|
+
display: inline-flex;
|
|
495
|
+
flex: none;
|
|
496
|
+
align-items: center;
|
|
497
|
+
justify-content: center;
|
|
498
|
+
width: 28px;
|
|
499
|
+
min-width: 28px;
|
|
500
|
+
height: 34px;
|
|
501
|
+
min-height: 34px;
|
|
502
|
+
margin: 0 -6px 0 -8px;
|
|
503
|
+
padding: 0;
|
|
504
|
+
border: 0;
|
|
505
|
+
border-radius: var(--border-radius-element, 8px);
|
|
506
|
+
background: transparent;
|
|
507
|
+
color: var(--color-text-maxcontrast);
|
|
508
|
+
cursor: grab;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.cn-ordered-list-field__handle:active {
|
|
512
|
+
cursor: grabbing;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.cn-ordered-list-field__handle:focus-visible {
|
|
516
|
+
outline: 2px solid var(--color-primary-element);
|
|
517
|
+
outline-offset: 1px;
|
|
518
|
+
}
|
|
519
|
+
|
|
374
520
|
.cn-ordered-list-field__position {
|
|
375
521
|
flex: none;
|
|
376
522
|
min-width: 18px;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-saved-view-alert" data-testid="cn-saved-view-alert">
|
|
7
|
+
<p v-if="!view" class="cn-saved-view-alert__empty" data-testid="cn-saved-view-alert-empty">
|
|
8
|
+
{{ emptyText }}
|
|
9
|
+
</p>
|
|
10
|
+
<template v-else>
|
|
11
|
+
<dl class="cn-saved-view-alert__facts">
|
|
12
|
+
<dt>{{ t('nextcloud-vue', 'Name') }}</dt>
|
|
13
|
+
<dd data-testid="cn-saved-view-alert-name">
|
|
14
|
+
{{ view.name }}
|
|
15
|
+
</dd>
|
|
16
|
+
<template v-if="countText">
|
|
17
|
+
<dt>{{ t('nextcloud-vue', 'Now') }}</dt>
|
|
18
|
+
<dd data-testid="cn-saved-view-alert-count">
|
|
19
|
+
{{ countText }}
|
|
20
|
+
</dd>
|
|
21
|
+
</template>
|
|
22
|
+
</dl>
|
|
23
|
+
<NcCheckboxRadioSwitch
|
|
24
|
+
type="switch"
|
|
25
|
+
:modelValue="enabled"
|
|
26
|
+
:disabled="saving || !canEdit"
|
|
27
|
+
data-testid="cn-saved-view-alert-switch"
|
|
28
|
+
@update:modelValue="onToggle">
|
|
29
|
+
<span class="cn-saved-view-alert__switch-text">
|
|
30
|
+
<span>{{ t('nextcloud-vue', 'Tell me above a number') }}</span>
|
|
31
|
+
<span class="cn-saved-view-alert__hint">{{ hintText }}</span>
|
|
32
|
+
</span>
|
|
33
|
+
</NcCheckboxRadioSwitch>
|
|
34
|
+
<NcTextField v-if="enabled"
|
|
35
|
+
:modelValue="String(threshold)"
|
|
36
|
+
type="number"
|
|
37
|
+
min="0"
|
|
38
|
+
:label="t('nextcloud-vue', 'Threshold')"
|
|
39
|
+
:disabled="saving || !canEdit"
|
|
40
|
+
data-testid="cn-saved-view-alert-threshold"
|
|
41
|
+
@update:modelValue="onThreshold" />
|
|
42
|
+
<p v-if="error"
|
|
43
|
+
class="cn-saved-view-alert__error"
|
|
44
|
+
role="alert"
|
|
45
|
+
data-testid="cn-saved-view-alert-error">
|
|
46
|
+
{{ error }}
|
|
47
|
+
</p>
|
|
48
|
+
</template>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
import { getCurrentUser } from '@nextcloud/auth'
|
|
54
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
55
|
+
import { NcCheckboxRadioSwitch, NcTextField } from '@nextcloud/vue'
|
|
56
|
+
import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* CnSavedViewAlertWidget — the saved view an index page has applied, with its
|
|
60
|
+
* count alert (DqZoekresultaten "Opgeslagen zoekopdracht"): its name, how many
|
|
61
|
+
* rows it holds now, and a switch that asks OpenRegister to tell the reader
|
|
62
|
+
* when the count reaches a threshold.
|
|
63
|
+
*
|
|
64
|
+
* The alert is the view's own `alert` (`{ operator: 'gte', threshold,
|
|
65
|
+
* recipients }`), saved with a PATCH on the view; OpenRegister evaluates it on
|
|
66
|
+
* its sweep and fires on the crossing, not on the state. Switching it off
|
|
67
|
+
* clears it. The widget reads the page through the `cnIndexList` CnIndexPage
|
|
68
|
+
* provides, so it belongs in an index page's `sidePanel`
|
|
69
|
+
* (`type: "saved-view-alert"`).
|
|
70
|
+
*
|
|
71
|
+
* Example (manifest):
|
|
72
|
+
* ```json
|
|
73
|
+
* { "id": "alert", "type": "saved-view-alert", "title": "Saved search", "content": { "noun": { "one": "case", "other": "cases" } } }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export default {
|
|
77
|
+
name: 'CnSavedViewAlertWidget',
|
|
78
|
+
|
|
79
|
+
components: { NcCheckboxRadioSwitch, NcTextField },
|
|
80
|
+
|
|
81
|
+
inject: {
|
|
82
|
+
/** The index page's list: its applied saved view and its total. */
|
|
83
|
+
cnIndexList: { default: null },
|
|
84
|
+
/** Host translate function provided by CnAppRoot. */
|
|
85
|
+
cnTranslate: { default: () => (key) => key },
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
props: {
|
|
89
|
+
/**
|
|
90
|
+
* The widget content: `{ noun?: { one, other }, emptyText? }`. `noun`
|
|
91
|
+
* names what is counted ("9 zaken"); `emptyText` shows while no saved
|
|
92
|
+
* view is applied. Both are translated with the app's catalogue.
|
|
93
|
+
*/
|
|
94
|
+
content: {
|
|
95
|
+
type: Object,
|
|
96
|
+
default: () => ({}),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
data() {
|
|
101
|
+
return { saving: false, error: '', draft: null }
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
computed: {
|
|
105
|
+
/** @return {object|null} The applied saved view, or null. */
|
|
106
|
+
view() {
|
|
107
|
+
return (this.cnIndexList && this.cnIndexList.appliedView) || null
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/** @return {boolean} Whether the view carries an alert. */
|
|
111
|
+
enabled() {
|
|
112
|
+
const alert = this.view && this.view.alert
|
|
113
|
+
return Boolean(alert && typeof alert === 'object' && Number.isFinite(Number(alert.threshold)))
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/** @return {number} The threshold being edited, else the view's, else 10. */
|
|
117
|
+
threshold() {
|
|
118
|
+
if (this.draft !== null) {
|
|
119
|
+
return this.draft
|
|
120
|
+
}
|
|
121
|
+
const value = this.enabled ? Number(this.view.alert.threshold) : 10
|
|
122
|
+
return Number.isFinite(value) ? value : 10
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/** @return {boolean} Whether the reader may change the view: its owner, or when the view says nothing. */
|
|
126
|
+
canEdit() {
|
|
127
|
+
const owner = this.view && this.view.owner
|
|
128
|
+
if (!owner) {
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
const user = getCurrentUser()
|
|
132
|
+
return Boolean(user && user.uid === owner)
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/** @return {string} "9 zaken", or '' while the total is unknown. */
|
|
136
|
+
countText() {
|
|
137
|
+
const total = this.cnIndexList ? this.cnIndexList.total : undefined
|
|
138
|
+
if (typeof total !== 'number') {
|
|
139
|
+
return ''
|
|
140
|
+
}
|
|
141
|
+
const noun = this.content && this.content.noun
|
|
142
|
+
if (noun && typeof noun === 'object' && noun.one && noun.other) {
|
|
143
|
+
return `${total} ${this.cnTranslate(total === 1 ? noun.one : noun.other)}`
|
|
144
|
+
}
|
|
145
|
+
return n('nextcloud-vue', '%n result', '%n results', total)
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** @return {string} The line under the switch. */
|
|
149
|
+
hintText() {
|
|
150
|
+
return t('nextcloud-vue', 'A message to you when there are {threshold} or more', { threshold: this.threshold })
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** @return {string} What shows while no saved view is applied. */
|
|
154
|
+
emptyText() {
|
|
155
|
+
const own = this.content && this.content.emptyText
|
|
156
|
+
return own ? this.cnTranslate(own) : t('nextcloud-vue', 'Save this search or open a saved one to set an alert on it.')
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
methods: {
|
|
161
|
+
t,
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Switch the alert on (at the shown threshold) or off.
|
|
165
|
+
*
|
|
166
|
+
* @param {boolean} on The new state.
|
|
167
|
+
* @return {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
async onToggle(on) {
|
|
170
|
+
await this.save(on ? this.alertFor(this.threshold) : null)
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Save a new threshold, when the alert is on.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} value The typed value.
|
|
177
|
+
* @return {Promise<void>}
|
|
178
|
+
*/
|
|
179
|
+
async onThreshold(value) {
|
|
180
|
+
const number = Math.max(0, Math.floor(Number(value)))
|
|
181
|
+
if (!Number.isFinite(number)) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
this.draft = number
|
|
185
|
+
await this.save(this.alertFor(number))
|
|
186
|
+
this.draft = null
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The alert the view stores: reaching the threshold tells the reader.
|
|
191
|
+
*
|
|
192
|
+
* @param {number} threshold The threshold.
|
|
193
|
+
* @return {{operator: string, threshold: number, recipients: Array<string>}}
|
|
194
|
+
*/
|
|
195
|
+
alertFor(threshold) {
|
|
196
|
+
const user = getCurrentUser()
|
|
197
|
+
return { operator: 'gte', threshold, recipients: user && user.uid ? [user.uid] : [] }
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* PATCH the view's alert and hand the saved view back to the page.
|
|
202
|
+
*
|
|
203
|
+
* @param {object|null} alert The alert, or null to clear it.
|
|
204
|
+
* @return {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async save(alert) {
|
|
207
|
+
if (!this.view) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
this.saving = true
|
|
211
|
+
this.error = ''
|
|
212
|
+
try {
|
|
213
|
+
const saved = await useSavedViewsApi().patchView(this.view.uuid || this.view.id, { alert })
|
|
214
|
+
if (saved && this.cnIndexList && typeof this.cnIndexList.replaceSavedView === 'function') {
|
|
215
|
+
this.cnIndexList.replaceSavedView(saved)
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
this.error = (error && error.response && error.response.data && error.response.data.error) || t('nextcloud-vue', 'The alert could not be saved.')
|
|
219
|
+
} finally {
|
|
220
|
+
this.saving = false
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
</script>
|
|
226
|
+
|
|
227
|
+
<style scoped>
|
|
228
|
+
.cn-saved-view-alert {
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
gap: 12px;
|
|
232
|
+
font-size: 14px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.cn-saved-view-alert__facts {
|
|
236
|
+
display: grid;
|
|
237
|
+
grid-template-columns: max-content 1fr;
|
|
238
|
+
gap: 6px 16px;
|
|
239
|
+
margin: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.cn-saved-view-alert__facts dt {
|
|
243
|
+
color: var(--color-text-maxcontrast);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.cn-saved-view-alert__facts dd {
|
|
247
|
+
margin: 0;
|
|
248
|
+
font-weight: 500;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.cn-saved-view-alert__switch-text {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 2px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.cn-saved-view-alert__hint,
|
|
258
|
+
.cn-saved-view-alert__empty {
|
|
259
|
+
color: var(--color-text-maxcontrast);
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.cn-saved-view-alert__empty,
|
|
264
|
+
.cn-saved-view-alert__error {
|
|
265
|
+
margin: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.cn-saved-view-alert__error {
|
|
269
|
+
color: var(--color-error-text, var(--color-error));
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
}
|
|
272
|
+
</style>
|
|
@@ -438,7 +438,7 @@ export default {
|
|
|
438
438
|
*/
|
|
439
439
|
setup(props) {
|
|
440
440
|
const { isBoard } = useLook()
|
|
441
|
-
const wantsSource = (props.steps || []).some((s) => s && s.optionsSource)
|
|
441
|
+
const wantsSource = (props.steps || []).some((s) => s && (s.optionsSource || s.visibleWhen))
|
|
442
442
|
if (!wantsSource) {
|
|
443
443
|
return { isBoard }
|
|
444
444
|
}
|
|
@@ -506,7 +506,7 @@ export default {
|
|
|
506
506
|
|
|
507
507
|
setupSteps() {
|
|
508
508
|
const skipped = new Set(this.skippedSteps.map((entry) => entry.step.id))
|
|
509
|
-
return this.declaredSteps.filter((s) => !skipped.has(s.id))
|
|
509
|
+
return this.declaredSteps.filter((s) => !skipped.has(s.id) && this.stepConditionHolds(s))
|
|
510
510
|
},
|
|
511
511
|
|
|
512
512
|
/**
|
|
@@ -869,6 +869,39 @@ export default {
|
|
|
869
869
|
})
|
|
870
870
|
},
|
|
871
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Whether a step's `visibleWhen` holds: the value at that path in the
|
|
874
|
+
* setup status (`"flags.hasPayouts"`, dots walk into objects) is truthy,
|
|
875
|
+
* or equals `equals` when the step gives `{ path, equals }`. A step
|
|
876
|
+
* without `visibleWhen` always shows; one with it stays hidden until the
|
|
877
|
+
* status says so, also while the status loads (DqInstellen: the dwangsom
|
|
878
|
+
* step only when the app pays penalties).
|
|
879
|
+
*
|
|
880
|
+
* @param {object} step The step declaration.
|
|
881
|
+
* @return {boolean}
|
|
882
|
+
*/
|
|
883
|
+
stepConditionHolds(step) {
|
|
884
|
+
const when = step && step.visibleWhen
|
|
885
|
+
if (when === undefined || when === null || when === '') {
|
|
886
|
+
return true
|
|
887
|
+
}
|
|
888
|
+
const path = typeof when === 'string' ? when : when.path
|
|
889
|
+
if (typeof path !== 'string' || path === '') {
|
|
890
|
+
return true
|
|
891
|
+
}
|
|
892
|
+
let node = this.setupStatus
|
|
893
|
+
for (const key of path.split('.')) {
|
|
894
|
+
if (node === null || node === undefined || typeof node !== 'object') {
|
|
895
|
+
return false
|
|
896
|
+
}
|
|
897
|
+
node = node[key]
|
|
898
|
+
}
|
|
899
|
+
if (typeof when === 'object' && Object.hasOwn(when, 'equals')) {
|
|
900
|
+
return node === when.equals
|
|
901
|
+
}
|
|
902
|
+
return Boolean(node)
|
|
903
|
+
},
|
|
904
|
+
|
|
872
905
|
/**
|
|
873
906
|
* The option list a `choice` step declares `optionsSource` for, read
|
|
874
907
|
* from the setup status document the app already serves.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import CnAuditTrailWidget from '../CnAuditTrailWidget/CnAuditTrailWidget.vue'
|
|
18
18
|
import CnBannerWidget from '../CnBannerWidget/CnBannerWidget.vue'
|
|
19
19
|
import CnDividerWidget from '../CnDividerWidget/CnDividerWidget.vue'
|
|
20
|
+
import CnExportsWidget from '../CnExportsWidget/CnExportsWidget.vue'
|
|
20
21
|
import CnHeaderWidget from '../CnHeaderWidget/CnHeaderWidget.vue'
|
|
21
22
|
import CnIntegrationWidget from '../CnIntegrationWidget/CnIntegrationWidget.vue'
|
|
22
23
|
import CnNavCardGrid from '../CnNavCardGrid/CnNavCardGrid.vue'
|
|
@@ -24,6 +25,7 @@ import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
|
|
|
24
25
|
import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
|
|
25
26
|
import CnObjectMetadataWidget from '../CnObjectMetadataWidget/CnObjectMetadataWidget.vue'
|
|
26
27
|
import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
|
|
28
|
+
import CnSavedViewAlertWidget from '../CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue'
|
|
27
29
|
import CnTextWidget from '../CnTextWidget/CnTextWidget.vue'
|
|
28
30
|
import CnTimelineWidget from '../CnTimelineWidget/CnTimelineWidget.vue'
|
|
29
31
|
import CnWidgetCardGrid from '../CnWidgetCardGrid/CnWidgetCardGrid.vue'
|
|
@@ -89,4 +91,6 @@ export const BUILT_IN_WIDGETS = {
|
|
|
89
91
|
header: CnHeaderWidget,
|
|
90
92
|
text: CnTextWidget,
|
|
91
93
|
divider: CnDividerWidget,
|
|
94
|
+
'saved-view-alert': CnSavedViewAlertWidget,
|
|
95
|
+
exports: CnExportsWidget,
|
|
92
96
|
}
|
|
@@ -75,6 +75,7 @@ import '../CnTasksWidget/index.js'
|
|
|
75
75
|
import '../CnSectionsWidget/index.js'
|
|
76
76
|
import '../CnFactsWidget/index.js'
|
|
77
77
|
import '../CnDeviceFramesWidget/index.js'
|
|
78
|
+
import '../CnGroupedCardsWidget/index.js'
|
|
78
79
|
import '../CnChecklistRunWidget/index.js'
|
|
79
80
|
import '../CnWorkingCalendarWidget/index.js'
|
|
80
81
|
import '../CnPartyActionsWidget/index.js'
|
package/src/components/index.js
CHANGED
|
@@ -30,6 +30,8 @@ export { CnDateAxisView } from './CnDateAxisView/index.js'
|
|
|
30
30
|
export { CnObjectRow } from './CnObjectRow/index.js'
|
|
31
31
|
export { CnObjectList } from './CnObjectList/index.js'
|
|
32
32
|
export { CnOrderedListField } from './CnOrderedListField/index.js'
|
|
33
|
+
export { CnExportsWidget } from './CnExportsWidget/index.js'
|
|
34
|
+
export { CnSavedViewAlertWidget } from './CnSavedViewAlertWidget/index.js'
|
|
33
35
|
export { CnObjectKanban } from './CnObjectKanban/index.js'
|
|
34
36
|
export { CnObjectCalendar } from './CnObjectCalendar/index.js'
|
|
35
37
|
export { CnFolderTree } from './CnFolderTree/index.js'
|
|
@@ -224,6 +226,7 @@ export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
|
|
|
224
226
|
export { CnSectionsWidget } from './CnSectionsWidget/index.js'
|
|
225
227
|
export { CnFactsWidget } from './CnFactsWidget/index.js'
|
|
226
228
|
export { CnDeviceFramesWidget } from './CnDeviceFramesWidget/index.js'
|
|
229
|
+
export { CnGroupedCardsWidget } from './CnGroupedCardsWidget/index.js'
|
|
227
230
|
export { CnChecklistRunWidget } from './CnChecklistRunWidget/index.js'
|
|
228
231
|
export { CnWorkingCalendarWidget } from './CnWorkingCalendarWidget/index.js'
|
|
229
232
|
export { CnPartyActionsWidget } from './CnPartyActionsWidget/index.js'
|