@conduction/nextcloud-vue 2.56.0-beta.63 → 2.56.0-beta.65
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/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +115 -12
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +36 -9
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +2 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +20 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +68 -61
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +37 -12
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +1 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +8 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +131 -64
- package/dist/esm/utils/actionsDispatcher.js +5 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/savedViewHelpers.js +40 -3
- package/dist/esm/utils/savedViewHelpers.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +479 -251
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +131 -64
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +134 -13
- package/src/components/CnAppRoot/CnAppRoot.vue +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +35 -11
- package/src/components/CnIndexPage/CnIndexPage.md +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +37 -12
- package/src/components/CnIndexSidebar/CnIndexSidebar.md +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +2 -2
- package/src/components/CnWorkQueue/CnWorkQueue.vue +8 -1
- package/src/css/board.css +5 -2
- package/src/css/look-board-detail.css +19 -7
- package/src/css/look-board-index.css +14 -10
- package/src/css/look-board-reports.css +1 -1
- package/src/css/look-board.css +54 -9
- package/src/utils/actionsDispatcher.js +4 -1
- package/src/utils/savedViewHelpers.js +39 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<template>
|
|
6
6
|
<CnDialog
|
|
7
7
|
:name="text(config.title) || t('nextcloud-vue', 'Confirm')"
|
|
8
|
-
:eyebrow="
|
|
8
|
+
:eyebrow="eyebrowText"
|
|
9
9
|
:subtitle="text(config.subtitle)"
|
|
10
10
|
width="form"
|
|
11
11
|
:noClose="busy"
|
|
@@ -63,6 +63,24 @@
|
|
|
63
63
|
</ul>
|
|
64
64
|
</section>
|
|
65
65
|
|
|
66
|
+
<!-- Fields the action asks for, sent beside the reason. -->
|
|
67
|
+
<div v-for="field in fields"
|
|
68
|
+
:key="field.field"
|
|
69
|
+
class="cn-action-dialog__field"
|
|
70
|
+
:data-testid="`cn-action-dialog-field-${field.field}`">
|
|
71
|
+
<NcSelect v-if="field.type === 'select'"
|
|
72
|
+
:modelValue="selectedOption(field)"
|
|
73
|
+
:options="optionsOf(field)"
|
|
74
|
+
:inputLabel="text(field.label) || field.field"
|
|
75
|
+
:clearable="field.required === false"
|
|
76
|
+
@update:modelValue="(option) => pick(field, option)" />
|
|
77
|
+
<NcTextField v-else
|
|
78
|
+
:modelValue="values[field.field] || ''"
|
|
79
|
+
:type="field.type === 'datetime' ? 'datetime-local' : (field.type === 'date' ? 'date' : 'text')"
|
|
80
|
+
:label="text(field.label) || field.field"
|
|
81
|
+
@update:modelValue="(value) => setValue(field.field, value)" />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
66
84
|
<div v-if="config.reason" class="cn-action-dialog__field">
|
|
67
85
|
<NcTextArea
|
|
68
86
|
:id="reasonId"
|
|
@@ -116,7 +134,7 @@ import axios from '@nextcloud/axios'
|
|
|
116
134
|
import { emit } from '@nextcloud/event-bus'
|
|
117
135
|
import { translate as t } from '@nextcloud/l10n'
|
|
118
136
|
import { generateUrl } from '@nextcloud/router'
|
|
119
|
-
import { NcButton, NcNoteCard, NcTextArea } from '@nextcloud/vue'
|
|
137
|
+
import { NcButton, NcNoteCard, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
|
|
120
138
|
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
|
|
121
139
|
import CircleOutline from 'vue-material-design-icons/CircleOutline.vue'
|
|
122
140
|
import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
|
|
@@ -141,7 +159,8 @@ let reasonSeq = 0
|
|
|
141
159
|
* note and Archive it.
|
|
142
160
|
*
|
|
143
161
|
* `config`:
|
|
144
|
-
* - `title`, `
|
|
162
|
+
* - `title`, `subtitle`, `doneText`: texts, through the host translate;
|
|
163
|
+
* `eyebrow` a template filled from the record ("Close off · {identifier}").
|
|
145
164
|
* - `summary`: `{ source?, title, meta?, badge?, icon? }`: templates filled from
|
|
146
165
|
* the record (`source` an endpointSource to read it from, else `object`).
|
|
147
166
|
* - `facts`: `[{ title?, items: [{ label, value }] }]`, `value` a template.
|
|
@@ -149,8 +168,14 @@ let reasonSeq = 0
|
|
|
149
168
|
* `{ id, met, label?, detail? }` read from `source`; `labels[id]` gives an
|
|
150
169
|
* item's `{ label, detail?, unmetDetail? }`; the ids in `required` must be
|
|
151
170
|
* met before the action can run. `met: null` reads as unknown.
|
|
152
|
-
* - `permission`: `{ source, allowedPath, reasonPath? }`:
|
|
153
|
-
* while `allowedPath` reads true; `reasonPath` says
|
|
171
|
+
* - `permission`: `{ source, listPath?, match?, allowedPath, reasonPath? }`:
|
|
172
|
+
* the action runs only while `allowedPath` reads true; `reasonPath` says
|
|
173
|
+
* why not. With `listPath` the paths read the entry of that list whose
|
|
174
|
+
* fields equal `match` (`{ act: "archive" }`).
|
|
175
|
+
* - `fields`: `[{ field, type?: "text"|"date"|"datetime"|"select", label?,
|
|
176
|
+
* required?, options?: [{ value, label, fills? }] }]`: inputs sent beside
|
|
177
|
+
* the reason, required by default; a select option's `fills` sets other
|
|
178
|
+
* fields that are still empty.
|
|
154
179
|
* - `reason`: `{ label?, hint?, required?, field? }`: a text area; required by
|
|
155
180
|
* default; sent as `field` (default `reason`).
|
|
156
181
|
* - `note`: `{ variant?, title?, text }`: a read-only note.
|
|
@@ -163,7 +188,7 @@ let reasonSeq = 0
|
|
|
163
188
|
export default {
|
|
164
189
|
name: 'CnActionDialog',
|
|
165
190
|
|
|
166
|
-
components: { CheckCircle, CircleOutline, CnDialog, CnIcon, HelpCircleOutline, NcButton, NcNoteCard, NcTextArea },
|
|
191
|
+
components: { CheckCircle, CircleOutline, CnDialog, CnIcon, HelpCircleOutline, NcButton, NcNoteCard, NcSelect, NcTextArea, NcTextField },
|
|
167
192
|
|
|
168
193
|
inject: {
|
|
169
194
|
/** The host app's translate (CnAppRoot `cnTranslate`). */
|
|
@@ -209,6 +234,7 @@ export default {
|
|
|
209
234
|
checklistError: '',
|
|
210
235
|
allowed: true,
|
|
211
236
|
reason: '',
|
|
237
|
+
values: {},
|
|
212
238
|
busy: false,
|
|
213
239
|
refusal: '',
|
|
214
240
|
done: false,
|
|
@@ -219,7 +245,21 @@ export default {
|
|
|
219
245
|
computed: {
|
|
220
246
|
/** @return {object} The token context. */
|
|
221
247
|
tokenCtx() {
|
|
222
|
-
return { objectId: this.
|
|
248
|
+
return { objectId: this.recordId, object: this.record || {} }
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The record's id: `objectId`, or the route's `id` when the prop is
|
|
253
|
+
* empty or a token the opener could not resolve.
|
|
254
|
+
*
|
|
255
|
+
* @return {string|number}
|
|
256
|
+
*/
|
|
257
|
+
recordId() {
|
|
258
|
+
const given = this.objectId
|
|
259
|
+
if (given !== '' && given !== null && given !== undefined && !String(given).startsWith('@')) {
|
|
260
|
+
return given
|
|
261
|
+
}
|
|
262
|
+
return (this.$route && this.$route.params && this.$route.params.id) || ''
|
|
223
263
|
},
|
|
224
264
|
|
|
225
265
|
/** @return {object} The confirm config. */
|
|
@@ -305,7 +345,27 @@ export default {
|
|
|
305
345
|
* @return {boolean}
|
|
306
346
|
*/
|
|
307
347
|
canConfirm() {
|
|
308
|
-
return !this.busy && this.allowed && this.requiredMet && (!this.reasonNeeded || this.reason.trim() !== '')
|
|
348
|
+
return !this.busy && this.allowed && this.requiredMet && this.fieldsFilled && (!this.reasonNeeded || this.reason.trim() !== '')
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/** @return {string} The eyebrow, filled from the record once it is known. */
|
|
352
|
+
eyebrowText() {
|
|
353
|
+
return this.record ? this.fill(this.config.eyebrow) || this.text(this.config.eyebrow) : this.text(this.config.eyebrow)
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/** @return {Array<object>} The fields with a name. */
|
|
357
|
+
fields() {
|
|
358
|
+
return (Array.isArray(this.config.fields) ? this.config.fields : []).filter((f) => f && f.field)
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Whether every required field has a value.
|
|
363
|
+
*
|
|
364
|
+
* @spec openspec/changes/action-dialog-fields-and-acts/specs/action-dialog/spec.md#requirement-an-action-dialog-asks-for-fields-and-reads-a-permission-from-a-list
|
|
365
|
+
* @return {boolean}
|
|
366
|
+
*/
|
|
367
|
+
fieldsFilled() {
|
|
368
|
+
return this.fields.every((f) => f.required === false || String(this.values[f.field] ?? '').trim() !== '')
|
|
309
369
|
},
|
|
310
370
|
},
|
|
311
371
|
|
|
@@ -380,6 +440,63 @@ export default {
|
|
|
380
440
|
}
|
|
381
441
|
},
|
|
382
442
|
|
|
443
|
+
/**
|
|
444
|
+
* The entry of a list whose fields equal `match`, or null.
|
|
445
|
+
*
|
|
446
|
+
* @param {unknown} list The list.
|
|
447
|
+
* @param {object} [match] The fields to match.
|
|
448
|
+
* @return {object|null}
|
|
449
|
+
*/
|
|
450
|
+
matchIn(list, match) {
|
|
451
|
+
const wanted = Object.entries(match || {})
|
|
452
|
+
return (Array.isArray(list) ? list : []).find((entry) => entry && wanted.every(([key, value]) => String(entry[key]) === String(value))) || null
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* A select field's options, labels translated.
|
|
457
|
+
*
|
|
458
|
+
* @param {object} field The field.
|
|
459
|
+
* @return {Array<{id: string, label: string, fills?: object}>}
|
|
460
|
+
*/
|
|
461
|
+
optionsOf(field) {
|
|
462
|
+
return (Array.isArray(field.options) ? field.options : []).map((o) => ({ id: String(o.value), label: this.text(o.label) || String(o.value), fills: o.fills }))
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* A select field's chosen option.
|
|
467
|
+
*
|
|
468
|
+
* @param {object} field The field.
|
|
469
|
+
* @return {object|null}
|
|
470
|
+
*/
|
|
471
|
+
selectedOption(field) {
|
|
472
|
+
return this.optionsOf(field).find((o) => o.id === String(this.values[field.field] ?? '')) || null
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Set a field's value.
|
|
477
|
+
*
|
|
478
|
+
* @param {string} name The field.
|
|
479
|
+
* @param {unknown} value The value.
|
|
480
|
+
*/
|
|
481
|
+
setValue(name, value) {
|
|
482
|
+
this.values = { ...this.values, [name]: value === null || value === undefined ? '' : String(value) }
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Choose a select option; its `fills` set the fields still empty.
|
|
487
|
+
*
|
|
488
|
+
* @param {object} field The select field.
|
|
489
|
+
* @param {{id: string, fills?: object}|null} option The option.
|
|
490
|
+
*/
|
|
491
|
+
pick(field, option) {
|
|
492
|
+
this.setValue(field.field, option ? option.id : '')
|
|
493
|
+
for (const [name, value] of Object.entries((option && option.fills) || {})) {
|
|
494
|
+
if (String(this.values[name] ?? '').trim() === '') {
|
|
495
|
+
this.setValue(name, this.text(value) || value)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
|
|
383
500
|
/** @return {Promise<void>} */
|
|
384
501
|
async loadPermission() {
|
|
385
502
|
const def = this.config.permission
|
|
@@ -388,8 +505,9 @@ export default {
|
|
|
388
505
|
}
|
|
389
506
|
try {
|
|
390
507
|
const data = await this.read(def.source)
|
|
391
|
-
|
|
392
|
-
this.
|
|
508
|
+
const entry = def.listPath ? this.matchIn(readPath(data, def.listPath), def.match) : data
|
|
509
|
+
this.allowed = readPath(entry, def.allowedPath || 'allowed') === true
|
|
510
|
+
this.refusal = this.allowed || !def.reasonPath ? '' : String(readPath(entry, def.reasonPath) || '')
|
|
393
511
|
} catch {
|
|
394
512
|
this.allowed = false
|
|
395
513
|
}
|
|
@@ -420,15 +538,18 @@ export default {
|
|
|
420
538
|
if (this.config.reason) {
|
|
421
539
|
body[this.config.reason.field || 'reason'] = reason
|
|
422
540
|
}
|
|
541
|
+
for (const field of this.fields) {
|
|
542
|
+
body[field.field] = this.values[field.field] ?? ''
|
|
543
|
+
}
|
|
423
544
|
const method = String(request.method || 'POST').toLowerCase()
|
|
424
545
|
await axios[['post', 'put', 'patch', 'delete'].includes(method) ? method : 'post'](/^https?:\/\//i.test(url) ? url : generateUrl(url), body)
|
|
425
546
|
} else if (this.confirmConfig.action && typeof this.cnDispatchAction === 'function') {
|
|
426
|
-
await this.cnDispatchAction({ ...this.confirmConfig.action, args: [...(this.confirmConfig.action.args || []), { reason }] })
|
|
547
|
+
await this.cnDispatchAction({ ...this.confirmConfig.action, args: [...(this.confirmConfig.action.args || []), { ...this.values, reason }] })
|
|
427
548
|
}
|
|
428
549
|
this.done = true
|
|
429
550
|
emit(PAGE_REFRESH, {})
|
|
430
|
-
/** @event done The action ran. Payload: `
|
|
431
|
-
this.$emit('done', { reason })
|
|
551
|
+
/** @event done The action ran. Payload: the field values and `reason`. */
|
|
552
|
+
this.$emit('done', { ...this.values, reason })
|
|
432
553
|
} catch (failure) {
|
|
433
554
|
const data = failure && failure.response && failure.response.data
|
|
434
555
|
this.refusal = String((data && (data.message || data.error)) || t('nextcloud-vue', 'The action could not be completed.'))
|
|
@@ -758,6 +758,7 @@ import { installModalStack, uninstallModalStack } from '../../utils/modalStack.j
|
|
|
758
758
|
import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
|
|
759
759
|
import { resolveBrand } from '../../utils/resolveBrand.js'
|
|
760
760
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
761
|
+
import { pageOffersSavedViews } from '../../utils/savedViewHelpers.js'
|
|
761
762
|
import { normalizeSavedViewMenu, SAVED_VIEW_MENU_KEY, SAVED_VIEW_MENU_PREFIX, syncSavedViewMenu, toggleSavedViewMenuEntry, withSavedViewMenu } from '../../utils/savedViewMenu.js'
|
|
762
763
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
763
764
|
|
|
@@ -2400,7 +2401,7 @@ export default {
|
|
|
2400
2401
|
*/
|
|
2401
2402
|
offersSavedViews() {
|
|
2402
2403
|
const pages = this.manifest && Array.isArray(this.manifest.pages) ? this.manifest.pages : []
|
|
2403
|
-
return pages.some((page) => page && page.
|
|
2404
|
+
return pages.some((page) => page && page.type === 'index' && pageOffersSavedViews(page.config))
|
|
2404
2405
|
},
|
|
2405
2406
|
|
|
2406
2407
|
/**
|
|
@@ -126,20 +126,13 @@
|
|
|
126
126
|
</component>
|
|
127
127
|
<!-- The one Follow control beside the title (manifest `config.follow`):
|
|
128
128
|
shown when the object carries `@self.watching`. There is no star:
|
|
129
|
-
a favourite is a follow with notifications off.
|
|
129
|
+
a favourite is a follow with notifications off. The board look
|
|
130
|
+
draws it on a row of its own under the breadcrumb. -->
|
|
130
131
|
<span
|
|
131
|
-
v-if="showFollowToggle"
|
|
132
|
+
v-if="showFollowToggle && !isBoard"
|
|
132
133
|
class="cn-detail-page__interactions"
|
|
133
134
|
data-testid="cn-detail-page-interactions">
|
|
134
|
-
<CnFollowToggle
|
|
135
|
-
:register="register"
|
|
136
|
-
:schema="schema"
|
|
137
|
-
:objectId="String(objectId)"
|
|
138
|
-
:watching="resolvedSelf.watching === true"
|
|
139
|
-
:notify="typeof resolvedSelf.watchNotify === 'boolean' ? resolvedSelf.watchNotify : null"
|
|
140
|
-
:watcherCount="typeof resolvedSelf.watcherCount === 'number' ? resolvedSelf.watcherCount : null"
|
|
141
|
-
:canManage="!!(resolvedSelf.can && resolvedSelf.can.manage === true)"
|
|
142
|
-
:notifies="followNotifies" />
|
|
135
|
+
<CnFollowToggle v-bind="followToggleProps" />
|
|
143
136
|
</span>
|
|
144
137
|
</div>
|
|
145
138
|
<!--
|
|
@@ -517,6 +510,17 @@
|
|
|
517
510
|
:schema="currentSchema"
|
|
518
511
|
:register="register" />
|
|
519
512
|
</div>
|
|
513
|
+
<!-- Row 3 of the board look's header: the Follow control under the
|
|
514
|
+
breadcrumb, clear of the title row and its actions (DqZaak
|
|
515
|
+
"Volgen"). -->
|
|
516
|
+
<div
|
|
517
|
+
v-if="isBoard && showFollowToggle"
|
|
518
|
+
class="cn-detail-page__header-row3"
|
|
519
|
+
data-testid="cn-detail-page-header-row3">
|
|
520
|
+
<span class="cn-detail-page__interactions" data-testid="cn-detail-page-interactions">
|
|
521
|
+
<CnFollowToggle v-bind="followToggleProps" />
|
|
522
|
+
</span>
|
|
523
|
+
</div>
|
|
520
524
|
<!-- A widget inside the header (manifest `config.headerWidget`), on
|
|
521
525
|
a row of its own under the title and the actions: the stages
|
|
522
526
|
bars of the board's case card. Drawn without a card of its own,
|
|
@@ -4319,6 +4323,26 @@ export default {
|
|
|
4319
4323
|
&& this.register !== '' && this.schema !== '' && this.objectId !== ''
|
|
4320
4324
|
},
|
|
4321
4325
|
|
|
4326
|
+
/**
|
|
4327
|
+
* The props of the Follow control, wherever the header draws it.
|
|
4328
|
+
*
|
|
4329
|
+
* @return {object} The CnFollowToggle props.
|
|
4330
|
+
* @spec openspec/changes/board-follow-row/specs/board-follow-row/spec.md#requirement-the-board-follow-control-sits-under-the-breadcrumb
|
|
4331
|
+
*/
|
|
4332
|
+
followToggleProps() {
|
|
4333
|
+
const self = this.resolvedSelf
|
|
4334
|
+
return {
|
|
4335
|
+
register: this.register,
|
|
4336
|
+
schema: this.schema,
|
|
4337
|
+
objectId: String(this.objectId),
|
|
4338
|
+
watching: self.watching === true,
|
|
4339
|
+
notify: typeof self.watchNotify === 'boolean' ? self.watchNotify : null,
|
|
4340
|
+
watcherCount: typeof self.watcherCount === 'number' ? self.watcherCount : null,
|
|
4341
|
+
canManage: !!(self.can && self.can.manage === true),
|
|
4342
|
+
notifies: this.followNotifies,
|
|
4343
|
+
}
|
|
4344
|
+
},
|
|
4345
|
+
|
|
4322
4346
|
/**
|
|
4323
4347
|
* The `_extend[]` values of the object read: the page's own, plus
|
|
4324
4348
|
* `@self.can` while the Follow toggle can render (OpenRegister returns
|
|
@@ -368,7 +368,7 @@ export default {
|
|
|
368
368
|
| `showMassImport` | Boolean | `true` | Whether to show the mass Import action |
|
|
369
369
|
| `showMassCopy` | Boolean | `true` | Whether to show the mass Copy action |
|
|
370
370
|
| `allowExport` | Boolean | `false` | Opt-in flag for the native Export menu (CSV/Excel) next to the Add button. Renders only when `true` AND the resolved schema is flagged `exportable: true`; navigates to OpenRegister's export leaf (`GET /apps/openregister/api/objects/{register}/{schema}/export`), passing `$route.query` through as filters. Distinct from `showMassExport`, which exports the fetched/selected rows via a blob download. |
|
|
371
|
-
| `allowSavedViews` | Boolean |
|
|
371
|
+
| `allowSavedViews` | Boolean | unset | Saved views (saved-views-ui). Unset, the sidebar's Views tab (save, apply, edit, share, add to menu, copy a default, delete) is on for every page with a sidebar and a source a view can be scoped to: register and schema, `entitySource` or `savedViewsScope`. `false` turns it off. `true` also forces the sidebar and adds the toolbar control: a Views dropdown listing the user's OpenRegister saved-search views (`GET /apps/openregister/api/views`). Applying one writes its stored filters/search/sort into the route query (`_search` and `_order` reserved keys + plain filter keys); "Save current view…" persists the current route-query state via POST and toasts either way, naming the view (a failure also keeps the dialog open with the reason); own views can be deleted after confirmation. Emits `apply-view`. |
|
|
372
372
|
| `savedViewsScope` | String | `''` | Which pages share this page's saved views. By default a view is shared by every page over the same `register` and `schema` and shown on no other page, since a view is filters over one schema's fields. Set a name to share views across pages over different sources, or to keep two pages over one source apart. Written into the saved view's `query.scope` on save; views saved before scoping existed carry no scope and stay visible everywhere. |
|
|
373
373
|
| `massActionNameField` | String | `'title'` | Property name used to display item names in dialogs |
|
|
374
374
|
| `nameFormatter` | Function | `null` | Custom formatter for item names in dialogs; overrides `massActionNameField` |
|
|
@@ -1379,7 +1379,7 @@ import { isRowActionVisible, rowActionPayload } from '../../utils/rowActionItem.
|
|
|
1379
1379
|
import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
|
|
1380
1380
|
import { compileRowClass } from '../../utils/rowClassRules.js'
|
|
1381
1381
|
import { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'
|
|
1382
|
-
import { buildRouteQueryFromViewState, buildViewCreatePayload, defaultFilterViews, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, savedViewScope, schemaSlug, viewMatchesScope } from '../../utils/savedViewHelpers.js'
|
|
1382
|
+
import { buildRouteQueryFromViewState, buildViewCreatePayload, defaultFilterViews, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, pageOffersSavedViews, savedViewScope, schemaSlug, viewMatchesScope } from '../../utils/savedViewHelpers.js'
|
|
1383
1383
|
import { savedViewBadge, savedViewOverrides } from '../../utils/savedViewOverrides.js'
|
|
1384
1384
|
import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
|
|
1385
1385
|
import { resolveScopeLayout, resolveScopePresentation } from '../../utils/scopeListLayout.js'
|
|
@@ -1500,11 +1500,22 @@ function declaredFilterFields(props) {
|
|
|
1500
1500
|
* @return {object} The `{ fieldKey: values }` map, empty when the link carries none.
|
|
1501
1501
|
*/
|
|
1502
1502
|
function namedFiltersFromRoute(instance, props) {
|
|
1503
|
+
const route = instance && instance.proxy && instance.proxy.$route
|
|
1504
|
+
return namedFiltersFromQuery((route && route.query) || {}, props)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* The named-source filters a query carries, as `namedFiltersFromRoute` reads them.
|
|
1509
|
+
*
|
|
1510
|
+
* @param {object} query The route query.
|
|
1511
|
+
* @param {object} props The CnIndexPage props.
|
|
1512
|
+
*
|
|
1513
|
+
* @return {object} The `{ fieldKey: values }` map.
|
|
1514
|
+
*/
|
|
1515
|
+
function namedFiltersFromQuery(query, props) {
|
|
1503
1516
|
if (!props.entitySource) {
|
|
1504
1517
|
return {}
|
|
1505
1518
|
}
|
|
1506
|
-
const route = instance && instance.proxy && instance.proxy.$route
|
|
1507
|
-
const query = (route && route.query) || {}
|
|
1508
1519
|
const properties = declaredFilterFields(props)
|
|
1509
1520
|
const out = {}
|
|
1510
1521
|
|
|
@@ -2947,9 +2958,11 @@ export default {
|
|
|
2947
2958
|
},
|
|
2948
2959
|
|
|
2949
2960
|
/**
|
|
2950
|
-
*
|
|
2951
|
-
*
|
|
2952
|
-
*
|
|
2961
|
+
* Saved views (saved-views-ui). Unset, the sidebar's Views tab is on
|
|
2962
|
+
* for every page with a sidebar and a source a view can be scoped to
|
|
2963
|
+
* (register and schema, `entitySource` or `savedViewsScope`); `false`
|
|
2964
|
+
* turns it off. `true` also forces the sidebar and adds the toolbar
|
|
2965
|
+
* control, where a Views dropdown lists the
|
|
2953
2966
|
* current user's OpenRegister saved-search views
|
|
2954
2967
|
* (`GET /apps/openregister/api/views`); applying one writes its
|
|
2955
2968
|
* stored filters/search/sort into the route query (reusing the
|
|
@@ -2961,7 +2974,7 @@ export default {
|
|
|
2961
2974
|
*/
|
|
2962
2975
|
allowSavedViews: {
|
|
2963
2976
|
type: Boolean,
|
|
2964
|
-
default:
|
|
2977
|
+
default: null,
|
|
2965
2978
|
},
|
|
2966
2979
|
|
|
2967
2980
|
/**
|
|
@@ -5523,7 +5536,7 @@ export default {
|
|
|
5523
5536
|
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-6
|
|
5524
5537
|
*/
|
|
5525
5538
|
savedViewsForSidebar() {
|
|
5526
|
-
if (!this.
|
|
5539
|
+
if (!this.savedViewsOffered) {
|
|
5527
5540
|
return null
|
|
5528
5541
|
}
|
|
5529
5542
|
return {
|
|
@@ -6664,7 +6677,16 @@ export default {
|
|
|
6664
6677
|
* @return {{ scope: string, register: string, schema: object|string|null }} The page's source.
|
|
6665
6678
|
*/
|
|
6666
6679
|
savedViewsPage() {
|
|
6667
|
-
return { scope: this.savedViewsScope, register: this.register, schema: this.schema }
|
|
6680
|
+
return { scope: this.savedViewsScope, register: this.register, schema: this.schema, source: this.entitySource }
|
|
6681
|
+
},
|
|
6682
|
+
|
|
6683
|
+
/**
|
|
6684
|
+
* Whether this page offers saved views in its sidebar.
|
|
6685
|
+
*
|
|
6686
|
+
* @return {boolean}
|
|
6687
|
+
*/
|
|
6688
|
+
savedViewsOffered() {
|
|
6689
|
+
return pageOffersSavedViews(this)
|
|
6668
6690
|
},
|
|
6669
6691
|
|
|
6670
6692
|
/**
|
|
@@ -8237,7 +8259,7 @@ export default {
|
|
|
8237
8259
|
created() {
|
|
8238
8260
|
this.applyViewModeFromRoute()
|
|
8239
8261
|
this.pushAiContext()
|
|
8240
|
-
if (this.
|
|
8262
|
+
if (this.savedViewsOffered) {
|
|
8241
8263
|
this.fetchSavedViews()
|
|
8242
8264
|
}
|
|
8243
8265
|
this.selfActions = createSelfModeActions({
|
|
@@ -11345,8 +11367,8 @@ export default {
|
|
|
11345
11367
|
|
|
11346
11368
|
/**
|
|
11347
11369
|
* Fetch the current user's saved views (own + public) from
|
|
11348
|
-
* OpenRegister's views API. Called from created() when
|
|
11349
|
-
*
|
|
11370
|
+
* OpenRegister's views API. Called from created() when the page
|
|
11371
|
+
* offers saved views.
|
|
11350
11372
|
*/
|
|
11351
11373
|
async fetchSavedViews() {
|
|
11352
11374
|
this.savedViewsLoading = true
|
|
@@ -11416,6 +11438,9 @@ export default {
|
|
|
11416
11438
|
query.savedView = this.appliedSavedViewId
|
|
11417
11439
|
}
|
|
11418
11440
|
const nav = this.$router.replace({ query })
|
|
11441
|
+
if (this.isNamedSource) {
|
|
11442
|
+
this.namedActiveFilters = namedFiltersFromQuery(query, this)
|
|
11443
|
+
}
|
|
11419
11444
|
// Swallow the duplicate-navigation rejection (Vue Router 3)
|
|
11420
11445
|
// when the applied view matches the current query.
|
|
11421
11446
|
if (nav && typeof nav.catch === 'function') {
|
|
@@ -216,7 +216,7 @@ With admin-only filter control:
|
|
|
216
216
|
| `propertiesGroupLabel` | String | `''` | Override label for the schema properties group. Falls back to `schema.title` |
|
|
217
217
|
| `defaultTab` | String | `'search-tab'` | Tab id that is active when the sidebar opens. Built-in ids: `'search-tab'`, `'columns-tab'` |
|
|
218
218
|
| `activeTab` | String | `''` | Switch to a tab while mounted, e.g. `'filters-tab'` |
|
|
219
|
-
| `savedViews` | Object | `null` | The page's saved views, shown in a third tab,
|
|
219
|
+
| `savedViews` | Object | `null` | The page's saved views, shown in a third tab, Views (`filters-tab`, CnSavedViewsPanel); every intent comes back as one `saved-view` event `{ action, view }` |
|
|
220
220
|
| `userIsAdmin` | Boolean | `true` | When `false`, schema properties with `adminOnly: true` are hidden from filters |
|
|
221
221
|
| `filterFields` | Object | `null` | Filter declarations for a page with no schema of its own. Wins over `schema`, and feeds the Search tab only |
|
|
222
222
|
|
|
@@ -287,11 +287,11 @@
|
|
|
287
287
|
</div>
|
|
288
288
|
</NcAppSidebarTab>
|
|
289
289
|
|
|
290
|
-
<!--
|
|
290
|
+
<!-- Views tab (id filters-tab): the page's saved views (save, list, manage, add to menu). -->
|
|
291
291
|
<NcAppSidebarTab
|
|
292
292
|
v-if="savedViews"
|
|
293
293
|
id="filters-tab"
|
|
294
|
-
:name="t('nextcloud-vue', '
|
|
294
|
+
:name="t('nextcloud-vue', 'Views')"
|
|
295
295
|
:order="3">
|
|
296
296
|
<template #icon>
|
|
297
297
|
<BookmarkOutline :size="20" />
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
|
|
94
94
|
<script>
|
|
95
95
|
import axios from '@nextcloud/axios'
|
|
96
|
+
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
96
97
|
import { translate as t } from '@nextcloud/l10n'
|
|
97
98
|
import { generateUrl } from '@nextcloud/router'
|
|
98
99
|
import { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
@@ -109,7 +110,8 @@ import { openRowTarget } from '../../utils/linkNavigation.js'
|
|
|
109
110
|
* not be read is named at the top rather than folded into "nothing is
|
|
110
111
|
* waiting". A group can be hidden until tomorrow, through the server, so the
|
|
111
112
|
* choice follows the reader to another device. dossiq's personal queue is
|
|
112
|
-
* the first; pipelinq's queue has the same shape.
|
|
113
|
+
* the first; pipelinq's queue has the same shape. It reads again on
|
|
114
|
+
* `cn:page:refresh`, so work planned or closed in a dialog shows at once.
|
|
113
115
|
*
|
|
114
116
|
* `content`:
|
|
115
117
|
* - `source`: an endpointSource answering `{ groups, hiddenGroups?, unavailable? }`.
|
|
@@ -176,9 +178,14 @@ export default {
|
|
|
176
178
|
},
|
|
177
179
|
|
|
178
180
|
mounted() {
|
|
181
|
+
subscribe('cn:page:refresh', this.load)
|
|
179
182
|
this.load()
|
|
180
183
|
},
|
|
181
184
|
|
|
185
|
+
beforeUnmount() {
|
|
186
|
+
unsubscribe('cn:page:refresh', this.load)
|
|
187
|
+
},
|
|
188
|
+
|
|
182
189
|
methods: {
|
|
183
190
|
t,
|
|
184
191
|
|
package/src/css/board.css
CHANGED
|
@@ -55,10 +55,13 @@
|
|
|
55
55
|
margin-inline-start: auto;
|
|
56
56
|
padding: 1px 8px;
|
|
57
57
|
border-radius: 10px;
|
|
58
|
+
--nldesign-color-on-surface: var(--cn-board-text-soft, var(--color-main-text));
|
|
58
59
|
background-color: var(--color-main-background);
|
|
59
|
-
color: var(--color-main-text);
|
|
60
|
+
color: var(--cn-board-text-soft, var(--color-main-text));
|
|
60
61
|
font-size: 12px;
|
|
61
|
-
font-weight:
|
|
62
|
+
font-weight: 700;
|
|
63
|
+
/* 16px tall on its 1px pad (DqWerkbord's column counts). */
|
|
64
|
+
line-height: 14px;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/* A column at its limit: the count in the error colour; "full" is said in words too. */
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
height: var(--cn-board-control-height) !important;
|
|
76
76
|
min-height: var(--cn-board-control-height) !important;
|
|
77
77
|
padding: 0 14px !important;
|
|
78
|
-
border: 1px solid var(--
|
|
78
|
+
border: 1px solid var(--cn-board-control-border) !important;
|
|
79
79
|
border-radius: var(--cn-board-control-radius) !important;
|
|
80
80
|
background: var(--color-main-background) !important;
|
|
81
81
|
color: var(--color-main-text) !important;
|
|
@@ -88,13 +88,13 @@
|
|
|
88
88
|
background: var(--color-background-hover) !important;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/* Each header icon is 16px
|
|
91
|
+
/* Each header icon is 16px; NcButton draws 20px in a 30px slot. The 8px to
|
|
92
|
+
the label is the board's icon-and-label gap (look-board.css). */
|
|
92
93
|
.cn-look-board .cn-detail-page .cn-detail-page__header-actions > .button-vue--secondary .button-vue__icon {
|
|
93
94
|
width: 16px;
|
|
94
95
|
min-width: 16px;
|
|
95
96
|
height: 16px;
|
|
96
97
|
min-height: 16px;
|
|
97
|
-
margin-inline-end: 8px;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
.cn-look-board .cn-detail-page .cn-detail-page__header-actions > .button-vue--secondary .button-vue__icon svg {
|
|
@@ -102,6 +102,17 @@
|
|
|
102
102
|
height: 16px;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/* Row 3: the Follow control under the breadcrumb, on the full width.
|
|
106
|
+
@spec openspec/changes/board-follow-row/specs/board-follow-row/spec.md#requirement-the-board-follow-control-sits-under-the-breadcrumb */
|
|
107
|
+
.cn-look-board .cn-detail-page__header-row3 {
|
|
108
|
+
align-items: center;
|
|
109
|
+
display: flex;
|
|
110
|
+
flex: 1 0 100%;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
gap: 8px;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
/* Row 2: pills, breadcrumb, dot, meta, chips. */
|
|
106
117
|
.cn-look-board .cn-detail-page__header-row2 {
|
|
107
118
|
align-items: center;
|
|
@@ -259,14 +270,14 @@
|
|
|
259
270
|
--nldesign-component-button-border-radius: 14px;
|
|
260
271
|
--nldesign-component-button-secondary-action-color: var(--color-main-text);
|
|
261
272
|
--nldesign-component-button-secondary-action-background-color: var(--color-main-background);
|
|
262
|
-
--nldesign-component-button-secondary-action-border-color: var(--
|
|
273
|
+
--nldesign-component-button-secondary-action-border-color: var(--cn-board-control-border);
|
|
263
274
|
--nldesign-component-button-secondary-action-hover-background-color: var(--color-background-hover);
|
|
264
275
|
box-sizing: border-box;
|
|
265
276
|
height: 28px;
|
|
266
277
|
min-height: 28px;
|
|
267
278
|
min-width: 0;
|
|
268
279
|
padding: 0 10px;
|
|
269
|
-
border: 1px solid var(--
|
|
280
|
+
border: 1px solid var(--cn-board-control-border);
|
|
270
281
|
border-radius: 14px;
|
|
271
282
|
background: var(--color-main-background);
|
|
272
283
|
color: var(--color-main-text);
|
|
@@ -404,13 +415,14 @@
|
|
|
404
415
|
|
|
405
416
|
/* The Actions button at the end of the strip: at the top, 8px in from the
|
|
406
417
|
end and 8px clear of the card, so the tabs, aligned to the bottom, sit 6px
|
|
407
|
-
lower than the button (DqZaak "Acties"). */
|
|
418
|
+
lower than the button, 12px in at both sides (DqZaak "Acties"). */
|
|
408
419
|
.cn-look-board .cn-tabs--board .cn-tabs__nav-end {
|
|
409
420
|
align-self: flex-start;
|
|
410
421
|
margin: 0 8px 8px 0;
|
|
411
422
|
}
|
|
412
423
|
|
|
413
424
|
.cn-look-board .cn-tabs--board .cn-tabs__nav-end .button-vue {
|
|
425
|
+
--button-padding: 12px;
|
|
414
426
|
min-height: 34px;
|
|
415
427
|
block-size: 34px;
|
|
416
428
|
}
|
|
@@ -734,7 +746,7 @@
|
|
|
734
746
|
.cn-look-board .cn-activity-tab--board .cn-activity-tab__kind {
|
|
735
747
|
align-items: center;
|
|
736
748
|
background: var(--color-main-background);
|
|
737
|
-
border: 1px solid var(--
|
|
749
|
+
border: 1px solid var(--cn-board-control-border);
|
|
738
750
|
border-radius: 18px;
|
|
739
751
|
color: var(--color-main-text);
|
|
740
752
|
cursor: pointer;
|