@conduction/nextcloud-vue 2.146.0 → 2.148.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/CnDetailPage/CnDetailPage.vue2.js +71 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +73 -9
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +37 -13
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +8 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +2 -2
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +3 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +26 -15
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -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 +191 -145
- package/dist/esm/utils/builtInFormatters.js +53 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/objectParties.js +30 -1
- package/dist/esm/utils/objectParties.js.map +1 -1
- 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 +1548 -1296
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +191 -145
- package/l10n/en.json +2 -1
- package/l10n/nl.json +2 -1
- package/package.json +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +73 -10
- package/src/components/CnFactsWidget/CnFactsWidget.vue +101 -9
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +8 -0
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +2 -1
- package/src/components/CnStepper/CnStepper.vue +10 -2
- package/src/css/dialog.css +5 -2
- package/src/css/look-board-index.css +14 -0
- package/src/css/look-board.css +3 -0
- package/src/dialogs/CnAccessPurposeDialog.vue +27 -12
- package/src/schemas/app-manifest-v2.schema.json +4 -2
- package/src/utils/builtInFormatters.js +52 -0
- package/src/utils/objectParties.js +29 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json
CHANGED
|
@@ -2397,7 +2397,8 @@
|
|
|
2397
2397
|
"From {date}": "From {date}",
|
|
2398
2398
|
"Until {date}": "Until {date}",
|
|
2399
2399
|
"More actions for {name}": "More actions for {name}",
|
|
2400
|
-
"Unlink contact": "Unlink contact"
|
|
2400
|
+
"Unlink contact": "Unlink contact",
|
|
2401
|
+
"{term}, extendable by {extension}": "{term}, extendable by {extension}"
|
|
2401
2402
|
},
|
|
2402
2403
|
"plurals": {
|
|
2403
2404
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2652,7 +2652,8 @@
|
|
|
2652
2652
|
"From {date}": "Vanaf {date}",
|
|
2653
2653
|
"Until {date}": "Tot {date}",
|
|
2654
2654
|
"More actions for {name}": "Meer acties voor {name}",
|
|
2655
|
-
"Unlink contact": "Contact ontkoppelen"
|
|
2655
|
+
"Unlink contact": "Contact ontkoppelen",
|
|
2656
|
+
"{term}, extendable by {extension}": "{term}, te verdagen met {extension}"
|
|
2656
2657
|
},
|
|
2657
2658
|
"plurals": {
|
|
2658
2659
|
"{count} member": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.148.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -438,7 +438,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
438
438
|
| `sideColumnFrom` | String | `''` | A field path, read like a `visibleWhen` field (e.g. `@context.caseType.sideCards`), whose list of ids picks the `sideColumn` entries this record shows and their order. Any other value keeps `sideColumn` as declared. |
|
|
439
439
|
| `tabsFrom` | String | `''` | A field path, read like a `visibleWhen` field, whose list of tab ids picks the tabs of the page's `tabs` widgets and their order for this record: `@context.caseType.tabs` reads the list a case type keeps (DqAanbesteding: Inschrijvingen, Publicaties, Bestanden, Historie). A tab's id is its `id`, else its `widgetId`, or the string entry. An empty or missing list keeps the declared tabs; edit mode shows them all. |
|
|
440
440
|
| `verbEndpoints` | Object | `null` | App routes that replace a record source's own route for a verb, by verb id (manifest `config.verbEndpoints`), such as `{ "complete": "/apps/dossiq/api/case-tasks/{id}/complete" }` for an app that checks something of its own before the engine completes the task. `{id}` is the record id. Only read with `entitySource`. |
|
|
441
|
-
| `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label }
|
|
441
|
+
| `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, checkUrl?, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label, hint? }`, a `hint` being a line under the option that says what choosing it does; "Other" with a written purpose is always added); `checkUrl` is asked first (GET), and an answer `{ required: false }` opens the record without the dialog, because the app recorded the purpose itself; `url` is the app endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}` filled in). Nothing of the record shows until the endpoint accepts, and the same record is not asked again in the same browser session. A refusal keeps the record closed. `null` asks nothing. |
|
|
442
442
|
| `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
|
|
443
443
|
|
|
444
444
|
## A body widget's own cards in the side column
|
|
@@ -772,7 +772,7 @@
|
|
|
772
772
|
the reader has stated why they open it and the purpose is recorded. -->
|
|
773
773
|
<div v-else-if="contentWithheld" class="cn-detail-page__access-purpose" data-testid="cn-detail-page-access-purpose">
|
|
774
774
|
<CnAccessPurposeDialog
|
|
775
|
-
v-if="accessPurposeAsking"
|
|
775
|
+
v-if="accessPurposeAsking && !accessPurposeChecking"
|
|
776
776
|
:title="accessPurposeText('title')"
|
|
777
777
|
:description="accessPurposeText('description')"
|
|
778
778
|
:options="accessPurposeOptions"
|
|
@@ -781,7 +781,7 @@
|
|
|
781
781
|
@submit="recordAccessPurpose"
|
|
782
782
|
@cancel="accessPurposeAsking = false" />
|
|
783
783
|
<NcEmptyContent
|
|
784
|
-
v-else-if="accessPurposeRequired"
|
|
784
|
+
v-else-if="accessPurposeRequired && !accessPurposeChecking"
|
|
785
785
|
:name="t('nextcloud-vue', 'State a purpose to open this')"
|
|
786
786
|
:description="t('nextcloud-vue', 'This record holds special categories of personal data. It opens once your purpose is recorded.')"
|
|
787
787
|
data-testid="cn-detail-page-access-purpose-refused">
|
|
@@ -3095,6 +3095,12 @@ export default {
|
|
|
3095
3095
|
* browser session. A refusal keeps the record closed. `null` (the
|
|
3096
3096
|
* default) asks nothing.
|
|
3097
3097
|
*
|
|
3098
|
+
* `checkUrl` (GET, `{objectId}` filled in) is asked first: an answer
|
|
3099
|
+
* `{ required: false }` opens the record without the dialog (the app
|
|
3100
|
+
* recorded the purpose itself, as for the case's own handler); any
|
|
3101
|
+
* other answer, or a failure, shows the dialog. An option's `hint`
|
|
3102
|
+
* is a line under it saying what choosing it does besides recording it.
|
|
3103
|
+
*
|
|
3098
3104
|
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
3099
3105
|
* @type {object|null}
|
|
3100
3106
|
*/
|
|
@@ -3413,6 +3419,10 @@ export default {
|
|
|
3413
3419
|
accessPurposeGranted: readGrantedPurposes(),
|
|
3414
3420
|
/** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */
|
|
3415
3421
|
accessPurposeAsking: true,
|
|
3422
|
+
/** Whether `accessPurpose.checkUrl` is being asked for the record in view. */
|
|
3423
|
+
accessPurposeChecking: false,
|
|
3424
|
+
/** The record the check last answered for, so it is asked once per record. */
|
|
3425
|
+
accessPurposeCheckedKey: '',
|
|
3416
3426
|
/** Whether a purpose is being recorded. */
|
|
3417
3427
|
accessPurposeBusy: false,
|
|
3418
3428
|
/** Why the last purpose was refused. */
|
|
@@ -4129,7 +4139,11 @@ export default {
|
|
|
4129
4139
|
const options = Array.isArray(this.accessPurpose?.options) ? this.accessPurpose.options : []
|
|
4130
4140
|
return options
|
|
4131
4141
|
.filter((option) => option && typeof option.value === 'string' && option.value !== '')
|
|
4132
|
-
.map((option) => ({
|
|
4142
|
+
.map((option) => ({
|
|
4143
|
+
value: option.value,
|
|
4144
|
+
label: this.cnTranslate(option.label || option.value),
|
|
4145
|
+
...(typeof option.hint === 'string' && option.hint !== '' ? { hint: this.cnTranslate(option.hint) } : {}),
|
|
4146
|
+
}))
|
|
4133
4147
|
},
|
|
4134
4148
|
|
|
4135
4149
|
/**
|
|
@@ -5832,6 +5846,17 @@ export default {
|
|
|
5832
5846
|
this.accessPurposeError = ''
|
|
5833
5847
|
},
|
|
5834
5848
|
|
|
5849
|
+
/** Once the page knows the record asks a purpose, the app is asked first whether it must (`checkUrl`). */
|
|
5850
|
+
accessPurposeRequired: {
|
|
5851
|
+
handler(required) {
|
|
5852
|
+
if (required && this.contentWithheld) {
|
|
5853
|
+
this.checkAccessPurpose()
|
|
5854
|
+
}
|
|
5855
|
+
},
|
|
5856
|
+
|
|
5857
|
+
immediate: true,
|
|
5858
|
+
},
|
|
5859
|
+
|
|
5835
5860
|
// Deep, like `sidebarTabs` / `sidebarProps` below: the manifest editors
|
|
5836
5861
|
// mutate `page.config.sidebar` IN PLACE, so the reference CnPageRenderer
|
|
5837
5862
|
// forwards never changes. A shallow watcher missed every such edit —
|
|
@@ -6089,6 +6114,50 @@ export default {
|
|
|
6089
6114
|
* @param {{purpose: string, note: string}} answer The chosen purpose.
|
|
6090
6115
|
* @return {Promise<void>}
|
|
6091
6116
|
*/
|
|
6117
|
+
/**
|
|
6118
|
+
* Ask the app whether the reader must state a purpose for this record
|
|
6119
|
+
* (`accessPurpose.checkUrl`); `{ required: false }` opens it at once.
|
|
6120
|
+
*
|
|
6121
|
+
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
6122
|
+
* @return {Promise<void>}
|
|
6123
|
+
*/
|
|
6124
|
+
async checkAccessPurpose() {
|
|
6125
|
+
const key = this.accessPurposeKey
|
|
6126
|
+
const url = String(this.accessPurpose?.checkUrl || '').replace(/\{objectId\}/g, encodeURIComponent(String(this.objectId || '')))
|
|
6127
|
+
if (url === '' || this.accessPurposeCheckedKey === key) {
|
|
6128
|
+
return
|
|
6129
|
+
}
|
|
6130
|
+
this.accessPurposeCheckedKey = key
|
|
6131
|
+
this.accessPurposeChecking = true
|
|
6132
|
+
try {
|
|
6133
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
6134
|
+
const { data } = await axios.get(prefixUrl(url))
|
|
6135
|
+
if (data && data.required === false && this.accessPurposeKey === key) {
|
|
6136
|
+
this.grantAccessPurpose(key)
|
|
6137
|
+
}
|
|
6138
|
+
} catch {
|
|
6139
|
+
// The check is a shortcut: without an answer the reader is asked as before.
|
|
6140
|
+
} finally {
|
|
6141
|
+
this.accessPurposeChecking = false
|
|
6142
|
+
}
|
|
6143
|
+
},
|
|
6144
|
+
|
|
6145
|
+
/**
|
|
6146
|
+
* Open the record and remember for this browser session that its
|
|
6147
|
+
* purpose is recorded.
|
|
6148
|
+
*
|
|
6149
|
+
* @param {string} key The record's session key.
|
|
6150
|
+
* @return {void}
|
|
6151
|
+
*/
|
|
6152
|
+
grantAccessPurpose(key) {
|
|
6153
|
+
this.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]
|
|
6154
|
+
try {
|
|
6155
|
+
window.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))
|
|
6156
|
+
} catch {
|
|
6157
|
+
// Without session storage the purpose is asked again on the next visit.
|
|
6158
|
+
}
|
|
6159
|
+
},
|
|
6160
|
+
|
|
6092
6161
|
async recordAccessPurpose(answer) {
|
|
6093
6162
|
const url = String(this.accessPurpose?.url || '').replace(/\{objectId\}/g, encodeURIComponent(String(this.objectId || '')))
|
|
6094
6163
|
if (url === '') {
|
|
@@ -6100,13 +6169,7 @@ export default {
|
|
|
6100
6169
|
try {
|
|
6101
6170
|
const { default: axios } = await import('@nextcloud/axios')
|
|
6102
6171
|
await axios.post(prefixUrl(url), { purpose: answer.purpose, note: answer.note })
|
|
6103
|
-
|
|
6104
|
-
this.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]
|
|
6105
|
-
try {
|
|
6106
|
-
window.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))
|
|
6107
|
-
} catch {
|
|
6108
|
-
// Without session storage the purpose is asked again on the next visit.
|
|
6109
|
-
}
|
|
6172
|
+
this.grantAccessPurpose(this.accessPurposeKey)
|
|
6110
6173
|
} catch (error) {
|
|
6111
6174
|
const data = error?.response?.data
|
|
6112
6175
|
this.accessPurposeError = (data && (data.message || data.error)) || t('nextcloud-vue', 'Your purpose could not be recorded, so the record stays closed.')
|
|
@@ -175,6 +175,15 @@
|
|
|
175
175
|
</span>
|
|
176
176
|
<span class="cn-facts-widget__line">{{ block.value }}</span>
|
|
177
177
|
</div>
|
|
178
|
+
<div v-else-if="block.kind === 'pills'" class="cn-facts-widget__pills">
|
|
179
|
+
<CnStatusBadge
|
|
180
|
+
v-for="pill in block.pills"
|
|
181
|
+
:key="pill.key"
|
|
182
|
+
:label="pill.value"
|
|
183
|
+
:variant="pill.variant"
|
|
184
|
+
size="small"
|
|
185
|
+
:data-testid="`cn-fact-${pill.key}`" />
|
|
186
|
+
</div>
|
|
178
187
|
<div v-else-if="block.kind === 'pill'">
|
|
179
188
|
<CnStatusBadge
|
|
180
189
|
:label="block.value"
|
|
@@ -191,6 +200,12 @@
|
|
|
191
200
|
data-testid="cn-facts-notice">
|
|
192
201
|
{{ notice.text }}
|
|
193
202
|
</NcNoteCard>
|
|
203
|
+
<div
|
|
204
|
+
v-if="footnote && record"
|
|
205
|
+
class="cn-facts-widget__footnote"
|
|
206
|
+
data-testid="cn-facts-footnote">
|
|
207
|
+
{{ footnote }}
|
|
208
|
+
</div>
|
|
194
209
|
<button
|
|
195
210
|
v-if="link && link.sidebarTab && record"
|
|
196
211
|
type="button"
|
|
@@ -247,6 +262,7 @@ import { useRefLabels } from '../../composables/useRefLabels.js'
|
|
|
247
262
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
248
263
|
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
249
264
|
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
265
|
+
import { formatTermDuration } from '../../utils/builtInFormatters.js'
|
|
250
266
|
import { conditionalLine } from '../../utils/conditionalLine.js'
|
|
251
267
|
import { parseDateValue } from '../../utils/dateVariant.js'
|
|
252
268
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
@@ -296,6 +312,30 @@ function resolveSourceTokens(value, record) {
|
|
|
296
312
|
}
|
|
297
313
|
|
|
298
314
|
/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */
|
|
315
|
+
/**
|
|
316
|
+
* Two or more pills in a row become one `pills` block, drawn on one line.
|
|
317
|
+
*
|
|
318
|
+
* @param {Array<object>} blocks The blocks in order.
|
|
319
|
+
* @return {Array<object>}
|
|
320
|
+
* @spec openspec/changes/facts-fields-from-context/specs/facts-widget/spec.md#requirement-a-facts-card-can-take-its-entries-from-the-page-context
|
|
321
|
+
*/
|
|
322
|
+
function groupPills(blocks) {
|
|
323
|
+
const out = []
|
|
324
|
+
blocks.forEach((block) => {
|
|
325
|
+
const last = out[out.length - 1]
|
|
326
|
+
if (block.kind !== 'pill') {
|
|
327
|
+
out.push(block)
|
|
328
|
+
} else if (last && last.kind === 'pills') {
|
|
329
|
+
last.pills.push(block)
|
|
330
|
+
} else if (last && last.kind === 'pill') {
|
|
331
|
+
out[out.length - 1] = { kind: 'pills', key: `pills-${last.key}`, pills: [last, block] }
|
|
332
|
+
} else {
|
|
333
|
+
out.push(block)
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
return out
|
|
337
|
+
}
|
|
338
|
+
|
|
299
339
|
const KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text', 'meter', 'progress'])
|
|
300
340
|
|
|
301
341
|
/** The schemes a facts link opens, besides a path on this server. */
|
|
@@ -436,15 +476,17 @@ export default {
|
|
|
436
476
|
* `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.
|
|
437
477
|
* `route` (`{ name, params?, query? }`, `@source.<path>` in a param) on a `title` or a `row` links its value to that route; a param that does not fill leaves it plain.
|
|
438
478
|
* `format: 'user'` shows a Nextcloud user id as the user's display name, read once per id; the entry stays empty until the name is known.
|
|
439
|
-
* `template` ("{available} {unit}") fills its `{path}` placeholders from the record instead of reading `path
|
|
479
|
+
* `template` ("{available} {unit}") fills its `{path}` placeholders from the record instead of reading `path` (`{path:format}` formats one, "{from:year}"); it is empty when every placeholder is. `suffix` (" kg") is added to a value that is not empty, its words translated.
|
|
440
480
|
* `labelPath` ("@context.caseType.handlerLabel") names a value on the record or the page's `@context` records that labels the row when it holds text; `label` is the fallback.
|
|
441
481
|
* `emptyValue` ("No") is shown, translated, when the entry has no value, so the entry stays drawn whatever `hideEmpty` says (an override that was never set reads "No").
|
|
442
482
|
* - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).
|
|
443
483
|
* - `actions` — action buttons under the facts in the `headerActions` shape (`api-call`, `open-form`, `navigate`, `open-modal`, ...; `confirm`, `variant`). `@source.<field>` names a field of the record shown, and an action whose token names an empty field is not drawn. `visibleWhen` reads the record shown; `adminOnly: true` draws the action for an instance admin only (visibility, never authorisation). An api-call or a saved form refreshes the page, and the facts read their source again.
|
|
484
|
+
* - `fieldsFrom` — `@context.<name>.<path>`, a list of entries on a record the page reads beside this one (a case type's `requestFields`); when it holds entries they are drawn instead of `fields`.
|
|
444
485
|
* - `rowsPath` — a path to a list of `{ label, value }` the record holds, drawn as rows after `fields` (a preview the server builds).
|
|
445
486
|
* - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar "Advies ontvangen"); nothing shows while the value is empty.
|
|
446
487
|
* - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.
|
|
447
|
-
*
|
|
488
|
+
* - `footnote` — a plain muted line under the facts, without a band (DqZaakBesluiten "Na het ondertekenen ..."); the text is translated.
|
|
489
|
+
* `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: "3 oktober"), `duration` (an ISO period in words, "4 weken"; `extendBy` names the extension period on the record: "4 weken, te verdagen met 2"), `year` ("2026"), `list` (an array joined by ", ", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, "1.5 MB"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is "20 jaar") or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
|
|
448
490
|
* `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default " · "); empty parts are dropped.
|
|
449
491
|
* - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. `{ register, schema, filter, sort? }` without `id` reads the first object that matches `filter` in `sort` order (`{ field, dir }`): the latest count of a shift. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
|
|
450
492
|
* - `endpointSource` — `{ url, method?, params?, responsePath? }`, the block a stat or chart takes: the facts read an app endpoint, shared and cached like those.
|
|
@@ -503,6 +545,29 @@ export default {
|
|
|
503
545
|
},
|
|
504
546
|
|
|
505
547
|
computed: {
|
|
548
|
+
/**
|
|
549
|
+
* The entries to draw: the list `fieldsFrom` names on the page's
|
|
550
|
+
* `@context` records when it holds entries (a case type's
|
|
551
|
+
* `requestFields`), else `fields`.
|
|
552
|
+
*
|
|
553
|
+
* @return {Array<object>}
|
|
554
|
+
* @spec openspec/changes/facts-fields-from-context/specs/facts-widget/spec.md#requirement-a-facts-card-can-take-its-entries-from-the-page-context
|
|
555
|
+
*/
|
|
556
|
+
entries() {
|
|
557
|
+
const from = this.content?.fieldsFrom
|
|
558
|
+
if (typeof from === 'string' && from.startsWith('@context.')) {
|
|
559
|
+
const rows = this.cnDetailVisibilityRows && typeof this.cnDetailVisibilityRows === 'object' && 'value' in this.cnDetailVisibilityRows
|
|
560
|
+
? this.cnDetailVisibilityRows.value
|
|
561
|
+
: this.cnDetailVisibilityRows
|
|
562
|
+
const named = readPath({ '@context': rows || {} }, from)
|
|
563
|
+
const list = Array.isArray(named) ? named.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)) : []
|
|
564
|
+
if (list.length > 0) {
|
|
565
|
+
return list
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return Array.isArray(this.content?.fields) ? this.content.fields : []
|
|
569
|
+
},
|
|
570
|
+
|
|
506
571
|
/**
|
|
507
572
|
* Whether the rows sit in two columns, label above value.
|
|
508
573
|
*
|
|
@@ -547,7 +612,7 @@ export default {
|
|
|
547
612
|
* @spec openspec/changes/facts-entry-count/specs/facts-widget/spec.md#requirement-a-facts-entry-can-count-a-related-list
|
|
548
613
|
*/
|
|
549
614
|
countJobsKey() {
|
|
550
|
-
const fields =
|
|
615
|
+
const fields = this.entries
|
|
551
616
|
const keys = fields
|
|
552
617
|
.filter((field) => field && field.countFrom && typeof field.countFrom === 'object' && field.countFrom.register && field.countFrom.schema)
|
|
553
618
|
.map((field) => this.countKey(field.countFrom))
|
|
@@ -708,7 +773,7 @@ export default {
|
|
|
708
773
|
*/
|
|
709
774
|
blocks() {
|
|
710
775
|
const sourceRecord = this.record
|
|
711
|
-
const fields =
|
|
776
|
+
const fields = this.entries
|
|
712
777
|
// A source that finds nothing still leaves the entries that read the
|
|
713
778
|
// page's own object (`from: "object"`).
|
|
714
779
|
const ownEntries = Boolean(this.objectData) && fields.some((field) => field && field.from === 'object')
|
|
@@ -787,7 +852,7 @@ export default {
|
|
|
787
852
|
out.push({ kind: 'rows', key: 'rows-answered', rows: [row] })
|
|
788
853
|
}
|
|
789
854
|
})
|
|
790
|
-
return out
|
|
855
|
+
return groupPills(out)
|
|
791
856
|
},
|
|
792
857
|
|
|
793
858
|
/**
|
|
@@ -879,6 +944,12 @@ export default {
|
|
|
879
944
|
return { text, type: ['success', 'warning', 'error'].includes(notice.variant) ? notice.variant : 'info' }
|
|
880
945
|
},
|
|
881
946
|
|
|
947
|
+
/** @return {string} The translated footnote, or '' when none is set. */
|
|
948
|
+
footnote() {
|
|
949
|
+
const footnote = this.content?.footnote
|
|
950
|
+
return typeof footnote === 'string' && footnote.trim() !== '' ? this.tr(footnote) : ''
|
|
951
|
+
},
|
|
952
|
+
|
|
882
953
|
/** @return {string} The reveal button's text. */
|
|
883
954
|
showLabel() {
|
|
884
955
|
return t('nextcloud-vue', 'Show')
|
|
@@ -1116,7 +1187,7 @@ export default {
|
|
|
1116
1187
|
* @spec openspec/changes/facts-entry-count/specs/facts-widget/spec.md#requirement-a-facts-entry-can-count-a-related-list
|
|
1117
1188
|
*/
|
|
1118
1189
|
async loadCounts() {
|
|
1119
|
-
const fields =
|
|
1190
|
+
const fields = this.entries
|
|
1120
1191
|
const seen = new Set()
|
|
1121
1192
|
for (const field of fields) {
|
|
1122
1193
|
const countFrom = field && field.countFrom
|
|
@@ -1193,8 +1264,8 @@ export default {
|
|
|
1193
1264
|
*/
|
|
1194
1265
|
templateValue(record, template) {
|
|
1195
1266
|
let filled = false
|
|
1196
|
-
const text = this.tr(template).replace(/\{([\w.@]+)\}/g, (match, path) => {
|
|
1197
|
-
const value = this.format(readPath(record, path), {}, record)
|
|
1267
|
+
const text = this.tr(template).replace(/\{([\w.@]+)(?::([\w-]+))?\}/g, (match, path, format) => {
|
|
1268
|
+
const value = this.format(readPath(record, path), format ? { format } : {}, record)
|
|
1198
1269
|
filled = filled || value !== ''
|
|
1199
1270
|
return value
|
|
1200
1271
|
})
|
|
@@ -1586,7 +1657,7 @@ export default {
|
|
|
1586
1657
|
* @return {Array<{spec: object, raw: unknown}>}
|
|
1587
1658
|
*/
|
|
1588
1659
|
refSpecs(record) {
|
|
1589
|
-
const fields =
|
|
1660
|
+
const fields = this.entries
|
|
1590
1661
|
const specs = []
|
|
1591
1662
|
const visit = (spec) => {
|
|
1592
1663
|
if (!spec || typeof spec !== 'object') {
|
|
@@ -1819,6 +1890,14 @@ export default {
|
|
|
1819
1890
|
.filter((item) => item !== '')
|
|
1820
1891
|
return items.join(', ')
|
|
1821
1892
|
}
|
|
1893
|
+
if (format === 'year') {
|
|
1894
|
+
const date = parseDateValue(raw)
|
|
1895
|
+
return date === null ? '' : String(date.getFullYear())
|
|
1896
|
+
}
|
|
1897
|
+
// A term in words, with the extension at `extendBy` on the record: "4 weken, te verdagen met 2".
|
|
1898
|
+
if (format === 'duration') {
|
|
1899
|
+
return formatTermDuration(raw, typeof spec.extendBy === 'string' && record ? readPath(record, spec.extendBy) : null)
|
|
1900
|
+
}
|
|
1822
1901
|
// The month written out, the year only outside this year: "3 oktober".
|
|
1823
1902
|
if (format === 'longdate') {
|
|
1824
1903
|
return formatLongDate(raw)
|
|
@@ -1960,6 +2039,12 @@ export default {
|
|
|
1960
2039
|
min-width: 0;
|
|
1961
2040
|
}
|
|
1962
2041
|
|
|
2042
|
+
.cn-facts-widget__pills {
|
|
2043
|
+
display: flex;
|
|
2044
|
+
flex-wrap: wrap;
|
|
2045
|
+
gap: 8px;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
1963
2048
|
/* `padding: 0`: Nextcloud's core stylesheet pads every `dl` 12px at the top
|
|
1964
2049
|
and bottom, which put a side card's facts 24px under its title, against
|
|
1965
2050
|
the board's 12 (DqZaak "Behandeling", DqZaakDocumenten). */
|
|
@@ -2216,6 +2301,13 @@ export default {
|
|
|
2216
2301
|
margin: 4px 0 0;
|
|
2217
2302
|
}
|
|
2218
2303
|
|
|
2304
|
+
/* A div, not a p: a theme sets every paragraph's size with !important. */
|
|
2305
|
+
.cn-facts-widget__footnote {
|
|
2306
|
+
color: var(--color-text-maxcontrast);
|
|
2307
|
+
font-size: 13px;
|
|
2308
|
+
line-height: 1.2;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2219
2311
|
.cn-facts-widget__link {
|
|
2220
2312
|
font-size: 14px;
|
|
2221
2313
|
font-weight: 600;
|
|
@@ -121,7 +121,7 @@ export default {
|
|
|
121
121
|
| `icon` | Object\|Function | `null` | MDI icon component shown in the card header |
|
|
122
122
|
| `objectType` | String | `''` | Registered object type slug used by `objectStore.saveObject()` |
|
|
123
123
|
| `store` | Object | `null` | Explicit Pinia objectStore instance; auto-detected via Pinia when omitted |
|
|
124
|
-
| `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. |
|
|
124
|
+
| `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. `format: 'duration'` reads an ISO period in words (`P28D` "4 weken"), and `extendBy` names the object's extension period, read as "4 weken, te verdagen met 2". |
|
|
125
125
|
| `columns` | Number | `3` | Number of CSS grid columns in the widget |
|
|
126
126
|
| `collapsedFields` (`collapsed-fields`) | Number | `null` | Number of fields shown while collapsed, with a "Show all N fields" button for the rest. `null` fits the cell: the whole rows that fit stay, and the button only appears when the cell overflows |
|
|
127
127
|
| `hideEmpty` | Boolean | `false` | Hide fields with no value instead of rendering an em dash. For discriminated supertypes (one `ticket` schema holding request/complaint/contactmoment), so the grid is type-aware without the schema enumerating which fields belong to which variant. Read grid only — the field being edited, a field with unsaved changes, and the Edit form stay visible; `false`/`0` are values and are never hidden. |
|
|
@@ -438,10 +438,12 @@ import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
|
|
|
438
438
|
import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
|
|
439
439
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
440
440
|
import { useObjectStore } from '../../store/index.js'
|
|
441
|
+
import { formatTermDuration } from '../../utils/builtInFormatters.js'
|
|
441
442
|
import { foldDottedFields, isDottedKey, spreadDottedFields } from '../../utils/dottedFields.js'
|
|
442
443
|
import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
443
444
|
import { humaniseKey } from '../../utils/humaniseKey.js'
|
|
444
445
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
446
|
+
import { readPath } from '../../utils/readPath.js'
|
|
445
447
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
446
448
|
import { saveEdited } from '../../utils/saveEdited.js'
|
|
447
449
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
@@ -1103,6 +1105,12 @@ export default {
|
|
|
1103
1105
|
// label, beside a list that already read "Ingediend".
|
|
1104
1106
|
const enumLabel = this.enumDisplayLabel(field, raw)
|
|
1105
1107
|
const empty = this.emptyValueFor(field, raw)
|
|
1108
|
+
if (empty === null && field.format === 'duration' && raw !== null && raw !== undefined && raw !== '') {
|
|
1109
|
+
// A term in words, with the extension at `extendBy` when the object holds one.
|
|
1110
|
+
const extension = typeof field.extendBy === 'string' ? readPath(this.objectData || {}, field.extendBy) : null
|
|
1111
|
+
values[field.key] = formatTermDuration(raw, extension)
|
|
1112
|
+
continue
|
|
1113
|
+
}
|
|
1106
1114
|
values[field.key] = empty !== null ? empty : (enumLabel !== null ? enumLabel : formatValue(raw, prop || {}))
|
|
1107
1115
|
}
|
|
1108
1116
|
return values
|
|
@@ -248,6 +248,7 @@ import {
|
|
|
248
248
|
indicatorVerdict,
|
|
249
249
|
kindCode,
|
|
250
250
|
newPartyRecord,
|
|
251
|
+
partyKindAccepted,
|
|
251
252
|
representedByMap,
|
|
252
253
|
roleLabel,
|
|
253
254
|
rolesInOrder,
|
|
@@ -892,7 +893,7 @@ export default {
|
|
|
892
893
|
if (!party?.partyKind || !this.kinds || (enforce && this.kinds.source !== enforce)) {
|
|
893
894
|
return false
|
|
894
895
|
}
|
|
895
|
-
return !this.kinds.kinds
|
|
896
|
+
return !partyKindAccepted(party.partyKind, this.kinds.kinds, this.listing?.kinds)
|
|
896
897
|
},
|
|
897
898
|
|
|
898
899
|
/**
|
|
@@ -154,6 +154,14 @@ export default {
|
|
|
154
154
|
font: inherit;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/* Nextcloud's server.css gives every plain button a 3px margin and the
|
|
158
|
+
clickable-area height (`button:not(.button-vue, …):not(…)`, 0,2,1), which
|
|
159
|
+
made a done step 40px tall around its 28px circle. Two classes outrank it. */
|
|
160
|
+
.cn-stepper button.cn-stepper__step {
|
|
161
|
+
margin: 0;
|
|
162
|
+
min-height: 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
157
165
|
button.cn-stepper__step {
|
|
158
166
|
cursor: pointer;
|
|
159
167
|
}
|
|
@@ -230,14 +238,14 @@ button.cn-stepper__step {
|
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
/* The board look: one row, 28px circles, the label beside the circle. */
|
|
233
|
-
/* The board leaves
|
|
241
|
+
/* The board leaves 16px between the stepper and the step's body (DqInstellen). */
|
|
234
242
|
.cn-stepper--board {
|
|
235
243
|
flex-wrap: wrap;
|
|
236
244
|
align-items: center;
|
|
237
245
|
justify-content: flex-start;
|
|
238
246
|
gap: 8px;
|
|
239
247
|
padding: 0;
|
|
240
|
-
margin: 0 0
|
|
248
|
+
margin: 0 0 16px;
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
.cn-stepper--board .cn-stepper__item {
|
package/src/css/dialog.css
CHANGED
|
@@ -265,8 +265,11 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
265
265
|
margin-bottom: 4px;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
/* Footer: 16px 24px, hairline on top, right aligned, 40px buttons, 10px gap
|
|
269
|
-
|
|
268
|
+
/* Footer: 16px 24px, hairline on top, right aligned, 40px buttons, 10px gap,
|
|
269
|
+
flush with the dialog's bottom. NcDialog's own footer rule (6px 0 12px
|
|
270
|
+
margin, 12px end padding) ties a two-class selector and wins when its
|
|
271
|
+
stylesheet loads later, so `.dialog` is added to outrank it. */
|
|
272
|
+
.cn-dialog-board .dialog .dialog__actions {
|
|
270
273
|
display: flex;
|
|
271
274
|
flex-wrap: wrap;
|
|
272
275
|
align-items: center;
|
|
@@ -2363,6 +2363,20 @@
|
|
|
2363
2363
|
padding-inline-start: 12px;
|
|
2364
2364
|
}
|
|
2365
2365
|
|
|
2366
|
+
/* The boards draw the leading icon inside the title cell, so the title's
|
|
2367
|
+
heading starts over the icon (DqOrganisaties "Naam" at 76, the icon's x),
|
|
2368
|
+
as an avatar shares its column's heading (DqContacten). The blank icon
|
|
2369
|
+
heading stays for the column count; the title heading reaches back over it.
|
|
2370
|
+
@spec openspec/changes/board-head-over-row-icon/specs/data-table/spec.md#requirement-a-board-title-heading-starts-over-the-row-icon */
|
|
2371
|
+
.cn-look-board .cn-table-container--board .cn-data-table th.cn-table-col--icon + th:not(.cn-look-nextcloud *) {
|
|
2372
|
+
padding-inline-start: 0;
|
|
2373
|
+
overflow: visible;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
.cn-look-board .cn-table-container--board .cn-data-table th.cn-table-col--icon + th:not(.cn-look-nextcloud *) > :first-child {
|
|
2377
|
+
margin-inline-start: -32px;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2366
2380
|
/* ---- The table beside the open side panel (DqZakenSorteren, DqFilterBeheren) */
|
|
2367
2381
|
/* The table takes its column's width: its configured column widths make way
|
|
2368
2382
|
for the content, titles and names wrap (a long word hyphenated, an
|
package/src/css/look-board.css
CHANGED
|
@@ -3220,6 +3220,8 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
|
|
|
3220
3220
|
Nextcloud's input height stretched it to 13x36. Its weight outranks the
|
|
3221
3221
|
18px other native radios take.
|
|
3222
3222
|
@spec openspec/changes/board-setup-choice-radio/specs/board-setup-choice-radio/spec.md#requirement-a-setup-steps-choice-radio-is-20px */
|
|
3223
|
+
/* The board's native radio keeps the browser's 3px end margin before the
|
|
3224
|
+
12px gap, so the option title starts 56px in (DqInstellen), not 53. */
|
|
3223
3225
|
.cn-look-board .cn-setup-step .cn-choice-cards input.cn-choice-cards__row-input:not(.cn-look-nextcloud *) {
|
|
3224
3226
|
box-sizing: border-box;
|
|
3225
3227
|
width: 20px;
|
|
@@ -3227,6 +3229,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
|
|
|
3227
3229
|
height: 20px;
|
|
3228
3230
|
min-height: 20px;
|
|
3229
3231
|
accent-color: var(--color-primary-element);
|
|
3232
|
+
margin-inline-end: 3px;
|
|
3230
3233
|
}
|
|
3231
3234
|
|
|
3232
3235
|
/* The chosen setup choice is edged by a real 2px border, as DqInstellen
|
|
@@ -17,17 +17,23 @@
|
|
|
17
17
|
<legend class="cn-access-purpose__legend">
|
|
18
18
|
{{ t('nextcloud-vue', 'Purpose') }}
|
|
19
19
|
</legend>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
<template v-for="option in choices" :key="option.value">
|
|
21
|
+
<NcCheckboxRadioSwitch
|
|
22
|
+
v-model="purpose"
|
|
23
|
+
type="radio"
|
|
24
|
+
name="cn-access-purpose"
|
|
25
|
+
:value="option.value"
|
|
26
|
+
:disabled="loading"
|
|
27
|
+
:data-testid="`cn-access-purpose-option-${option.value}`">
|
|
28
|
+
{{ option.label }}
|
|
29
|
+
</NcCheckboxRadioSwitch>
|
|
30
|
+
<!-- What choosing this purpose does besides recording it ("you become this case's handler"). -->
|
|
31
|
+
<p v-if="option.hint"
|
|
32
|
+
class="cn-access-purpose__hint"
|
|
33
|
+
:data-testid="`cn-access-purpose-hint-${option.value}`">
|
|
34
|
+
{{ option.hint }}
|
|
35
|
+
</p>
|
|
36
|
+
</template>
|
|
31
37
|
</fieldset>
|
|
32
38
|
<NcTextArea
|
|
33
39
|
v-if="purpose === OTHER"
|
|
@@ -94,8 +100,10 @@ export default {
|
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
102
|
* The purposes offered, already translated. "Other" is added at the end.
|
|
103
|
+
* A `hint` is a line under the option that says what choosing it does
|
|
104
|
+
* besides recording it.
|
|
97
105
|
*
|
|
98
|
-
* @type {Array<{value: string, label: string}>}
|
|
106
|
+
* @type {Array<{value: string, label: string, hint?: string}>}
|
|
99
107
|
*/
|
|
100
108
|
options: {
|
|
101
109
|
type: Array,
|
|
@@ -176,6 +184,13 @@ export default {
|
|
|
176
184
|
border: 0;
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
.cn-access-purpose__hint {
|
|
188
|
+
margin: 0 0 6px;
|
|
189
|
+
padding-inline-start: 36px;
|
|
190
|
+
color: var(--color-text-maxcontrast);
|
|
191
|
+
font-size: 13px;
|
|
192
|
+
}
|
|
193
|
+
|
|
179
194
|
.cn-access-purpose__legend {
|
|
180
195
|
margin-bottom: 4px;
|
|
181
196
|
font-weight: 600;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.157.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -5811,11 +5811,13 @@
|
|
|
5811
5811
|
"additionalProperties": false,
|
|
5812
5812
|
"properties": {
|
|
5813
5813
|
"value": { "type": "string", "minLength": 1 },
|
|
5814
|
-
"label": { "type": "string", "minLength": 1, "description": "i18n key." }
|
|
5814
|
+
"label": { "type": "string", "minLength": 1, "description": "i18n key." },
|
|
5815
|
+
"hint": { "type": "string", "description": "i18n key of a line under the option: what choosing it does besides recording it." }
|
|
5815
5816
|
}
|
|
5816
5817
|
}
|
|
5817
5818
|
},
|
|
5818
5819
|
"url": { "type": "string", "minLength": 1, "description": "App endpoint that records the purpose; {objectId} is filled in." },
|
|
5820
|
+
"checkUrl": { "type": "string", "minLength": 1, "description": "App endpoint asked first (GET, {objectId} filled in): an answer { required: false } opens the record without the dialog, because the app recorded the purpose itself." },
|
|
5819
5821
|
"title": { "type": "string", "description": "i18n key of the dialog's title." },
|
|
5820
5822
|
"description": { "type": "string", "description": "i18n key of the sentence under the title." }
|
|
5821
5823
|
}
|