@conduction/nextcloud-vue 2.101.0 → 2.103.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/CnActionsBar/CnActionsBar.vue2.js +22 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +29 -4
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +49 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +84 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +8 -6
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +26 -2
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
- package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
- package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +9 -0
- package/dist/esm/composables/indexSources.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 +5 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +5 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/pageViews.js +8 -1
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +593 -459
- package/dist/esm/utils/builtInFormatters.js +24 -4
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/rowClassRules.js +43 -0
- package/dist/esm/utils/rowClassRules.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/esm/utils/visibleWhen.js +32 -4
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +10093 -8858
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +593 -459
- package/l10n/en.json +5 -1
- package/l10n/nl.json +5 -1
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +1 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
- package/src/components/CnChartWidget/CnChartWidget.md +3 -2
- package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
- package/src/components/CnConversationThread/CnConversationThread.md +17 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
- package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
- package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +50 -7
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnFormDialog/CnFormDialog.md +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
- package/src/components/CnIndexPage/CnIndexPage.md +15 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +92 -9
- package/src/components/CnIndexPage/useNamedSource.js +26 -2
- package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
- package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
- package/src/components/CnSearchSelect/index.js +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +9 -0
- package/src/css/dialog.css +5 -3
- package/src/css/look-board-index.css +10 -0
- package/src/index.js +1 -0
- package/src/mixins/pageViews.js +8 -1
- package/src/schemas/app-manifest-v2.schema.json +107 -8
- package/src/utils/builtInFormatters.js +24 -4
- package/src/utils/rowClassRules.js +39 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +32 -4
|
@@ -239,6 +239,11 @@ export default {
|
|
|
239
239
|
CnMenuItemIcon,
|
|
240
240
|
},
|
|
241
241
|
|
|
242
|
+
inject: {
|
|
243
|
+
/** Host translate function provided by CnAppRoot, for the item labels. */
|
|
244
|
+
cnTranslate: { default: () => (key) => key },
|
|
245
|
+
},
|
|
246
|
+
|
|
242
247
|
props: {
|
|
243
248
|
/**
|
|
244
249
|
* Persisted widget content `{items, style, orientation, showIcons,
|
|
@@ -288,7 +293,26 @@ export default {
|
|
|
288
293
|
*/
|
|
289
294
|
items() {
|
|
290
295
|
const arr = this.content?.items
|
|
291
|
-
|
|
296
|
+
if (!Array.isArray(arr)) {
|
|
297
|
+
return []
|
|
298
|
+
}
|
|
299
|
+
// Labels are manifest strings: they go through the app's translate,
|
|
300
|
+
// children included, so "Case types" reads "Zaaktypen" (DqCatalogus).
|
|
301
|
+
const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
|
|
302
|
+
const translated = (list) => list.map((item) => {
|
|
303
|
+
if (!item || typeof item !== 'object') {
|
|
304
|
+
return item
|
|
305
|
+
}
|
|
306
|
+
const out = { ...item }
|
|
307
|
+
if (typeof item.label === 'string' && item.label !== '') {
|
|
308
|
+
out.label = translate(item.label)
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(item.children)) {
|
|
311
|
+
out.children = translated(item.children)
|
|
312
|
+
}
|
|
313
|
+
return out
|
|
314
|
+
})
|
|
315
|
+
return translated(arr)
|
|
292
316
|
},
|
|
293
317
|
|
|
294
318
|
/**
|
|
@@ -1385,7 +1385,8 @@ export default {
|
|
|
1385
1385
|
* is the budget). Measures the cell's remaining height below the table
|
|
1386
1386
|
* top, reserves room for the "View all" footer, and derives the row
|
|
1387
1387
|
* budget from the first rendered row's height. No-ops (fitRows null =
|
|
1388
|
-
* render all fetched rows) outside a fixed-height cell
|
|
1388
|
+
* render all fetched rows) outside a fixed-height cell and inside a
|
|
1389
|
+
* tab panel, unless the content sets `fit: true`.
|
|
1389
1390
|
*
|
|
1390
1391
|
* @return {void}
|
|
1391
1392
|
*/
|
|
@@ -1407,6 +1408,14 @@ export default {
|
|
|
1407
1408
|
this.fitRows = null
|
|
1408
1409
|
return
|
|
1409
1410
|
}
|
|
1411
|
+
// A list inside a tab panel does not own the cell, as on a
|
|
1412
|
+
// dashboard: it renders every fetched row unless the content asks
|
|
1413
|
+
// for the fit with `fit: true`.
|
|
1414
|
+
const panel = this.$el.closest('[role="tabpanel"]')
|
|
1415
|
+
if (panel && cell.contains(panel) && !(this.content && this.content.fit === true)) {
|
|
1416
|
+
this.fitRows = null
|
|
1417
|
+
return
|
|
1418
|
+
}
|
|
1410
1419
|
const table = this.$el.querySelector('.cn-object-list-widget__table table')
|
|
1411
1420
|
if (!table) {
|
|
1412
1421
|
return
|
|
@@ -59,6 +59,7 @@ In `manifest.json` (the usual entry-point — CnIndexPage mounts the bar for you
|
|
|
59
59
|
- `selectLabel` (String, default `'Filter'`) — accessible label / placeholder for the dropdown control.
|
|
60
60
|
- `placeholder` (String) — placeholder text for the dropdown (falls back to `selectLabel`).
|
|
61
61
|
- `maxVisible` (Number, default `0`) — chips mode only: how many pills render inline before the rest move behind one more chip, a `⋯` pill that opens a small panel of the hidden lenses (also as chips). `0` renders every tab. The visible set is the first `maxVisible` entries of `tabs`, so their declared order decides what stays in the strip; an active hidden tab moves its label and fill onto that chip. Manifest: `config.quickFilterMaxVisible`.
|
|
62
|
+
- `fit` (Boolean, default `false`) — chips mode only: show as many chips as the bar's own width holds and move the rest behind the overflow chip, measured again when the width, the tabs or the counts change. The bar takes the room its parent gives it; CnIndexPage's `quickFilterPlacement: "toolbar"` gives it what the Filter button and the view switch leave on their row. With `maxVisible` too, the smaller of the two wins.
|
|
62
63
|
- `counts` (Object or Array, default `null`): how many records each tab matches, keyed by tab index. A tab with a number shows it after its label. CnIndexPage fills this for tabs that set `showCount`.
|
|
63
64
|
|
|
64
65
|
### Events
|
|
@@ -24,8 +24,28 @@
|
|
|
24
24
|
</div>
|
|
25
25
|
<!-- Chips mode (default): the clickable tab strip. -->
|
|
26
26
|
<div v-else
|
|
27
|
+
ref="chipsRoot"
|
|
27
28
|
class="cn-quick-filter-bar"
|
|
28
|
-
:class="{ 'cn-quick-filter-bar--inline': inline }">
|
|
29
|
+
:class="{ 'cn-quick-filter-bar--inline': inline, 'cn-quick-filter-bar--fit': fit }">
|
|
30
|
+
<!-- With `fit`: an invisible copy of every chip and the overflow chip,
|
|
31
|
+
measured to work out how many chips the bar's width holds. -->
|
|
32
|
+
<div v-if="fit"
|
|
33
|
+
ref="measure"
|
|
34
|
+
class="cn-quick-filter-bar__measure"
|
|
35
|
+
aria-hidden="true">
|
|
36
|
+
<span v-for="entry in entries"
|
|
37
|
+
:key="'m:' + entry.tab.label + ':' + entry.index"
|
|
38
|
+
class="cn-quick-filter-bar__tab"
|
|
39
|
+
:class="{ 'cn-quick-filter-bar__tab--active': isChipActive(entry.index) }"
|
|
40
|
+
data-measure="chip">
|
|
41
|
+
<span class="cn-quick-filter-bar__label">{{ tl(entry.tab.label) }}</span>
|
|
42
|
+
<span v-if="countOf(entry) !== null" class="cn-quick-filter-bar__count">{{ ' ' + countOf(entry) }}</span>
|
|
43
|
+
</span>
|
|
44
|
+
<span class="cn-quick-filter-bar__tab cn-quick-filter-bar__more" data-measure="more">
|
|
45
|
+
<span v-if="overflowLabel" class="cn-quick-filter-bar__label">{{ overflowLabel }}</span>
|
|
46
|
+
<DotsHorizontal :size="16" class="cn-quick-filter-bar__icon" />
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
29
49
|
<!-- The tablist holds ONLY tabs: the overflow chip is a sibling, not a
|
|
30
50
|
child, because a role="tablist" may not contain a menu button. -->
|
|
31
51
|
<!-- Board look: the tabs are chips, pressed buttons in a group, not a
|
|
@@ -266,6 +286,19 @@ export default {
|
|
|
266
286
|
type: [Object, Array],
|
|
267
287
|
default: null,
|
|
268
288
|
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Chips mode only: show as many chips as the bar's own width holds and
|
|
292
|
+
* move the rest into the overflow chip, measured again whenever the
|
|
293
|
+
* width, the tabs or the counts change. The bar takes the room it is
|
|
294
|
+
* given (CnIndexPage's `quickFilterPlacement: "toolbar"` gives it what
|
|
295
|
+
* the Filter button and the view switch leave on their row). With
|
|
296
|
+
* `maxVisible` too, the smaller of the two wins.
|
|
297
|
+
*/
|
|
298
|
+
fit: {
|
|
299
|
+
type: Boolean,
|
|
300
|
+
default: false,
|
|
301
|
+
},
|
|
269
302
|
},
|
|
270
303
|
|
|
271
304
|
emits: ['update:active-index', 'update:selected-indices'],
|
|
@@ -274,6 +307,8 @@ export default {
|
|
|
274
307
|
return {
|
|
275
308
|
/** Whether the overflow chip's panel is open. */
|
|
276
309
|
moreOpen: false,
|
|
310
|
+
/** With `fit`: how many chips the width holds; null before the first measure. */
|
|
311
|
+
fitCount: null,
|
|
277
312
|
}
|
|
278
313
|
},
|
|
279
314
|
|
|
@@ -303,10 +338,24 @@ export default {
|
|
|
303
338
|
* @return {Array<{tab: object, index: number}>}
|
|
304
339
|
*/
|
|
305
340
|
visibleEntries() {
|
|
306
|
-
|
|
341
|
+
const limit = this.effectiveMax
|
|
342
|
+
if (!limit || this.tabs.length <= limit) {
|
|
307
343
|
return this.entries
|
|
308
344
|
}
|
|
309
|
-
return this.entries.slice(0,
|
|
345
|
+
return this.entries.slice(0, limit)
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* How many chips render: `maxVisible`, and with `fit` no more than the
|
|
350
|
+
* width holds; 0 for no limit.
|
|
351
|
+
*
|
|
352
|
+
* @return {number}
|
|
353
|
+
*/
|
|
354
|
+
effectiveMax() {
|
|
355
|
+
if (!this.fit || this.fitCount === null) {
|
|
356
|
+
return this.maxVisible
|
|
357
|
+
}
|
|
358
|
+
return this.maxVisible ? Math.min(this.maxVisible, this.fitCount) : this.fitCount
|
|
310
359
|
},
|
|
311
360
|
|
|
312
361
|
/**
|
|
@@ -316,10 +365,11 @@ export default {
|
|
|
316
365
|
* @return {Array<{tab: object, index: number}>}
|
|
317
366
|
*/
|
|
318
367
|
overflowEntries() {
|
|
319
|
-
|
|
368
|
+
const limit = this.effectiveMax
|
|
369
|
+
if (!limit || this.tabs.length <= limit) {
|
|
320
370
|
return []
|
|
321
371
|
}
|
|
322
|
-
return this.entries.slice(
|
|
372
|
+
return this.entries.slice(limit)
|
|
323
373
|
},
|
|
324
374
|
|
|
325
375
|
/**
|
|
@@ -399,7 +449,79 @@ export default {
|
|
|
399
449
|
},
|
|
400
450
|
},
|
|
401
451
|
|
|
452
|
+
watch: {
|
|
453
|
+
tabs() {
|
|
454
|
+
this.scheduleFit()
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
counts: {
|
|
458
|
+
deep: true,
|
|
459
|
+
handler() {
|
|
460
|
+
this.scheduleFit()
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
mounted() {
|
|
466
|
+
if (this.fit && typeof ResizeObserver === 'function' && this.$refs.chipsRoot) {
|
|
467
|
+
this.fitObserver = new ResizeObserver(() => this.measureFit())
|
|
468
|
+
this.fitObserver.observe(this.$refs.chipsRoot)
|
|
469
|
+
}
|
|
470
|
+
this.scheduleFit()
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
beforeUnmount() {
|
|
474
|
+
if (this.fitObserver) {
|
|
475
|
+
this.fitObserver.disconnect()
|
|
476
|
+
this.fitObserver = null
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
|
|
402
480
|
methods: {
|
|
481
|
+
/** Measure the fit after the next render. */
|
|
482
|
+
scheduleFit() {
|
|
483
|
+
if (this.fit) {
|
|
484
|
+
this.$nextTick(() => this.measureFit())
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Work out how many chips the bar's width holds, from the invisible
|
|
490
|
+
* copy: every chip that fits, and when not all do, room left for the
|
|
491
|
+
* overflow chip. At least one chip always shows.
|
|
492
|
+
*/
|
|
493
|
+
measureFit() {
|
|
494
|
+
const box = this.$refs.measure
|
|
495
|
+
const root = this.$refs.chipsRoot
|
|
496
|
+
if (!this.fit || !box || !root) {
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
const available = root.clientWidth
|
|
500
|
+
const chips = [...box.querySelectorAll('[data-measure="chip"]')].map((el) => el.offsetWidth)
|
|
501
|
+
const more = box.querySelector('[data-measure="more"]')
|
|
502
|
+
const moreWidth = more ? more.offsetWidth : 0
|
|
503
|
+
const gap = parseFloat(getComputedStyle(box).columnGap) || 8
|
|
504
|
+
if (available <= 0 || chips.length === 0) {
|
|
505
|
+
this.fitCount = null
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
const total = chips.reduce((sum, width) => sum + width, 0) + gap * (chips.length - 1)
|
|
509
|
+
if (total <= available) {
|
|
510
|
+
this.fitCount = null
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
let used = moreWidth
|
|
514
|
+
let count = 0
|
|
515
|
+
for (const width of chips) {
|
|
516
|
+
if (used + gap + width > available) {
|
|
517
|
+
break
|
|
518
|
+
}
|
|
519
|
+
used += gap + width
|
|
520
|
+
count++
|
|
521
|
+
}
|
|
522
|
+
this.fitCount = Math.max(1, count)
|
|
523
|
+
},
|
|
524
|
+
|
|
403
525
|
/**
|
|
404
526
|
* A manifest string through the host's label lookup (`cnTranslate`), so a key
|
|
405
527
|
* such as "Mine" reads in the user's language.
|
|
@@ -531,6 +653,38 @@ export default {
|
|
|
531
653
|
/* Inline variant — bare strip for embedding inside another bar (e.g. the
|
|
532
654
|
CnActionsBar #filters slot), so it reads as a control beside the view toggle
|
|
533
655
|
rather than a separate bordered row. */
|
|
656
|
+
/* With `fit`: one line, as wide as it is given; the chips it cannot hold
|
|
657
|
+
are in the overflow chip. The measuring copy takes no room and is never
|
|
658
|
+
seen or read. */
|
|
659
|
+
.cn-quick-filter-bar--fit {
|
|
660
|
+
position: relative;
|
|
661
|
+
flex-wrap: nowrap;
|
|
662
|
+
min-width: 0;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.cn-quick-filter-bar--fit .cn-quick-filter-bar__tabs {
|
|
666
|
+
flex-wrap: nowrap;
|
|
667
|
+
min-width: 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.cn-quick-filter-bar__measure {
|
|
671
|
+
position: absolute;
|
|
672
|
+
inset-block-start: 0;
|
|
673
|
+
inset-inline-start: 0;
|
|
674
|
+
display: flex;
|
|
675
|
+
gap: 8px;
|
|
676
|
+
width: 0;
|
|
677
|
+
height: 0;
|
|
678
|
+
overflow: hidden;
|
|
679
|
+
visibility: hidden;
|
|
680
|
+
pointer-events: none;
|
|
681
|
+
white-space: nowrap;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.cn-quick-filter-bar__measure > * {
|
|
685
|
+
flex: none;
|
|
686
|
+
}
|
|
687
|
+
|
|
534
688
|
.cn-quick-filter-bar--inline {
|
|
535
689
|
padding: 0;
|
|
536
690
|
border-bottom: none;
|
|
@@ -201,13 +201,38 @@
|
|
|
201
201
|
</span>
|
|
202
202
|
</li>
|
|
203
203
|
</ul>
|
|
204
|
+
<!-- `footerAction`: one action under the list, also under the empty
|
|
205
|
+
text ("Beroep registreren"). With `disabledWhen` it greys out and
|
|
206
|
+
says why (`disabledReason`: "Kan zodra de beslissing is
|
|
207
|
+
verzonden"). -->
|
|
208
|
+
<div
|
|
209
|
+
v-if="footerAction && !error"
|
|
210
|
+
class="cn-record-list__footer"
|
|
211
|
+
data-testid="cn-record-list-footer">
|
|
212
|
+
<NcButton
|
|
213
|
+
variant="secondary"
|
|
214
|
+
:disabled="footerDisabled"
|
|
215
|
+
:aria-describedby="footerDisabled && footerAction.reason ? footerReasonId : null"
|
|
216
|
+
data-testid="cn-record-list-footer-action"
|
|
217
|
+
@click="runFooterAction">
|
|
218
|
+
<template v-if="footerAction.icon" #icon>
|
|
219
|
+
<CnIcon :name="footerAction.icon" :size="16" />
|
|
220
|
+
</template>
|
|
221
|
+
{{ footerAction.label }}
|
|
222
|
+
</NcButton>
|
|
223
|
+
<span
|
|
224
|
+
v-if="footerDisabled && footerAction.reason"
|
|
225
|
+
:id="footerReasonId"
|
|
226
|
+
class="cn-record-list__footer-reason"
|
|
227
|
+
data-testid="cn-record-list-footer-reason">{{ footerAction.reason }}</span>
|
|
228
|
+
</div>
|
|
204
229
|
</div>
|
|
205
230
|
</template>
|
|
206
231
|
|
|
207
232
|
<script>
|
|
208
233
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
209
234
|
import { translate as t } from '@nextcloud/l10n'
|
|
210
|
-
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
235
|
+
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
|
211
236
|
import { inject, ref } from 'vue'
|
|
212
237
|
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
213
238
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
@@ -224,8 +249,12 @@ import { lensUnavailableText, readLensReports } from '../../utils/lensAvailabili
|
|
|
224
249
|
import { pageFilter, pageSort } from '../../utils/pageControls.js'
|
|
225
250
|
import { readPath } from '../../utils/readPath.js'
|
|
226
251
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
252
|
+
import { evaluateVisibleWhen } from '../../utils/visibleWhen.js'
|
|
227
253
|
|
|
228
254
|
const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
|
|
255
|
+
|
|
256
|
+
/** Gives each list's footer reason its own element id. */
|
|
257
|
+
let footerSeq = 0
|
|
229
258
|
const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice']
|
|
230
259
|
|
|
231
260
|
/** The tones a `notice` list takes. */
|
|
@@ -279,7 +308,7 @@ function unwrap(value) {
|
|
|
279
308
|
export default {
|
|
280
309
|
name: 'CnRecordListWidget',
|
|
281
310
|
|
|
282
|
-
components: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },
|
|
311
|
+
components: { ChevronRight, CnIcon, CnStatusBadge, NcButton, NcLoadingIcon },
|
|
283
312
|
|
|
284
313
|
inject: {
|
|
285
314
|
/** Detail-page object context (`{ objectId, object, register, schema }`). */
|
|
@@ -314,6 +343,10 @@ export default {
|
|
|
314
343
|
* `presentation: "notice"` draws the rows as sentences in one box toned by
|
|
315
344
|
* `variant` (`info`, `warning` or `error`), after an optional `lead`
|
|
316
345
|
* (`{total}` filled in).
|
|
346
|
+
* `footerAction` (`{ label, icon?, type, ..., disabledWhen?,
|
|
347
|
+
* disabledReason? }`) ends the card with one action, also under the
|
|
348
|
+
* empty text; while the visibleWhen condition `disabledWhen` holds the
|
|
349
|
+
* button is disabled and `disabledReason` shows under it.
|
|
317
350
|
*
|
|
318
351
|
* @type {object}
|
|
319
352
|
*/
|
|
@@ -367,10 +400,48 @@ export default {
|
|
|
367
400
|
error: false,
|
|
368
401
|
/** Personal-lens reports (`@self.lenses`) of the latest response. */
|
|
369
402
|
lenses: {},
|
|
403
|
+
/** Whether `footerAction.disabledWhen` holds. */
|
|
404
|
+
footerDisabled: false,
|
|
405
|
+
footerReasonId: `cn-record-list-footer-reason-${++footerSeq}`,
|
|
370
406
|
}
|
|
371
407
|
},
|
|
372
408
|
|
|
373
409
|
computed: {
|
|
410
|
+
/**
|
|
411
|
+
* The action under the list (`footerAction`: `{ label, icon?, type,
|
|
412
|
+
* ..., disabledWhen?, disabledReason? }`), labels through the host
|
|
413
|
+
* translate, or null without a label.
|
|
414
|
+
*
|
|
415
|
+
* @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
|
|
416
|
+
* @return {{label: string, icon: string, reason: string, action: object}|null}
|
|
417
|
+
*/
|
|
418
|
+
footerAction() {
|
|
419
|
+
const action = this.content?.footerAction
|
|
420
|
+
if (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
label: this.translate(action.label),
|
|
425
|
+
icon: typeof action.icon === 'string' ? action.icon : '',
|
|
426
|
+
reason: typeof action.disabledReason === 'string' && action.disabledReason !== '' ? this.translate(action.disabledReason) : '',
|
|
427
|
+
action,
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* What `footerAction.disabledWhen` depends on, so it is evaluated again
|
|
433
|
+
* when the condition, the record or the workspace changes.
|
|
434
|
+
*
|
|
435
|
+
* @return {string}
|
|
436
|
+
*/
|
|
437
|
+
footerDisabledKey() {
|
|
438
|
+
const cond = this.content?.footerAction?.disabledWhen
|
|
439
|
+
if (!cond) {
|
|
440
|
+
return ''
|
|
441
|
+
}
|
|
442
|
+
return JSON.stringify({ cond, object: this.tokenCtx.object, workspace: this.tokenCtx.workspace, total: this.total })
|
|
443
|
+
},
|
|
444
|
+
|
|
374
445
|
/** @return {string} `rows`, `feed`, `inline` or `cards`. */
|
|
375
446
|
/**
|
|
376
447
|
* The tone of a `notice` list (`content.variant`): info, warning or error.
|
|
@@ -595,6 +666,13 @@ export default {
|
|
|
595
666
|
this.fetchRows()
|
|
596
667
|
},
|
|
597
668
|
|
|
669
|
+
footerDisabledKey: {
|
|
670
|
+
immediate: true,
|
|
671
|
+
handler() {
|
|
672
|
+
this.evaluateFooterDisabled()
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
|
|
598
676
|
endpointKey() {
|
|
599
677
|
this.fetchRows()
|
|
600
678
|
},
|
|
@@ -621,6 +699,47 @@ export default {
|
|
|
621
699
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
622
700
|
},
|
|
623
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Settle whether the footer action is disabled: `disabledWhen` is a
|
|
704
|
+
* visibleWhen condition read against the page's record (`field`), the
|
|
705
|
+
* workspace, an endpoint or a source; `total` is the list's total.
|
|
706
|
+
*
|
|
707
|
+
* @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
|
|
708
|
+
* @return {Promise<void>}
|
|
709
|
+
*/
|
|
710
|
+
async evaluateFooterDisabled() {
|
|
711
|
+
const cond = this.content?.footerAction?.disabledWhen
|
|
712
|
+
if (!cond) {
|
|
713
|
+
this.footerDisabled = false
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
const ctx = { ...this.tokenCtx, object: { ...(this.tokenCtx.object || {}), total: this.total } }
|
|
717
|
+
const key = this.footerDisabledKey
|
|
718
|
+
const disabled = await evaluateVisibleWhen(cond, ctx)
|
|
719
|
+
if (key === this.footerDisabledKey) {
|
|
720
|
+
this.footerDisabled = disabled
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Run the footer action through the page's dispatcher, with the page's
|
|
726
|
+
* record as the token context.
|
|
727
|
+
*
|
|
728
|
+
* @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
|
|
729
|
+
* @return {unknown} What the dispatcher returns.
|
|
730
|
+
*/
|
|
731
|
+
runFooterAction() {
|
|
732
|
+
const declared = this.footerAction?.action
|
|
733
|
+
if (!declared || this.footerDisabled) {
|
|
734
|
+
return undefined
|
|
735
|
+
}
|
|
736
|
+
const { disabledWhen, disabledReason, ...action } = declared
|
|
737
|
+
if (typeof this.cnDispatchAction === 'function') {
|
|
738
|
+
return this.cnDispatchAction(action)
|
|
739
|
+
}
|
|
740
|
+
return dispatchAction(action, { router: this.$router || null, tokenCtx: this.tokenCtx, source: { register: this.content?.register, schema: this.content?.schema } })
|
|
741
|
+
},
|
|
742
|
+
|
|
624
743
|
/**
|
|
625
744
|
* A row's `bar`: its value, the maximum it is measured against (the
|
|
626
745
|
* row's `maxField`, else the largest row), the filled share and
|
|
@@ -1196,6 +1315,19 @@ export default {
|
|
|
1196
1315
|
padding: 0;
|
|
1197
1316
|
}
|
|
1198
1317
|
|
|
1318
|
+
.cn-record-list__footer {
|
|
1319
|
+
display: flex;
|
|
1320
|
+
flex-direction: column;
|
|
1321
|
+
align-items: flex-start;
|
|
1322
|
+
gap: 6px;
|
|
1323
|
+
margin-top: 10px;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
.cn-record-list__footer-reason {
|
|
1327
|
+
font-size: 13px;
|
|
1328
|
+
color: var(--color-text-maxcontrast);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1199
1331
|
.cn-record-list__empty,
|
|
1200
1332
|
.cn-record-list__error {
|
|
1201
1333
|
color: var(--color-text-maxcontrast);
|