@conduction/nextcloud-vue 2.147.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 +6 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- 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/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 +160 -126
- package/dist/esm/utils/builtInFormatters.js +53 -1
- package/dist/esm/utils/builtInFormatters.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 +1357 -1217
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +160 -126
- 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 +6 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +8 -0
- 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/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.')
|
|
@@ -262,6 +262,7 @@ import { useRefLabels } from '../../composables/useRefLabels.js'
|
|
|
262
262
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
263
263
|
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
264
264
|
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
265
|
+
import { formatTermDuration } from '../../utils/builtInFormatters.js'
|
|
265
266
|
import { conditionalLine } from '../../utils/conditionalLine.js'
|
|
266
267
|
import { parseDateValue } from '../../utils/dateVariant.js'
|
|
267
268
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
@@ -485,7 +486,7 @@ export default {
|
|
|
485
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.
|
|
486
487
|
* - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.
|
|
487
488
|
* - `footnote` — a plain muted line under the facts, without a band (DqZaakBesluiten "Na het ondertekenen ..."); the text is translated.
|
|
488
|
-
* `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"), `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).
|
|
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).
|
|
489
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.
|
|
490
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).
|
|
491
492
|
* - `endpointSource` — `{ url, method?, params?, responsePath? }`, the block a stat or chart takes: the facts read an app endpoint, shared and cached like those.
|
|
@@ -1893,6 +1894,10 @@ export default {
|
|
|
1893
1894
|
const date = parseDateValue(raw)
|
|
1894
1895
|
return date === null ? '' : String(date.getFullYear())
|
|
1895
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
|
+
}
|
|
1896
1901
|
// The month written out, the year only outside this year: "3 oktober".
|
|
1897
1902
|
if (format === 'longdate') {
|
|
1898
1903
|
return formatLongDate(raw)
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -524,6 +524,57 @@ export function formatIsoDuration(value) {
|
|
|
524
524
|
return words[period.unit] ? words[period.unit]() : String(value)
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
/** The ISO designator of each date unit a term is written in. */
|
|
528
|
+
const UNIT_DESIGNATOR = { years: 'Y', months: 'M', weeks: 'W', days: 'D' }
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* A one-unit ISO-8601 period, a day count that is whole weeks read as weeks
|
|
532
|
+
* (a term of `P28D` is "4 weeks"); null when it is not a one-unit period.
|
|
533
|
+
*
|
|
534
|
+
* @param {unknown} value The stored period.
|
|
535
|
+
* @return {{amount: number, unit: string}|null}
|
|
536
|
+
*/
|
|
537
|
+
function termPeriod(value) {
|
|
538
|
+
if (value === null || value === undefined || value === '') {
|
|
539
|
+
return null
|
|
540
|
+
}
|
|
541
|
+
const period = parseDuration(String(value))
|
|
542
|
+
if (period === null || !UNIT_DESIGNATOR[period.unit]) {
|
|
543
|
+
return null
|
|
544
|
+
}
|
|
545
|
+
if (period.unit === 'days' && period.amount > 0 && period.amount % 7 === 0) {
|
|
546
|
+
return { amount: period.amount / 7, unit: 'weeks' }
|
|
547
|
+
}
|
|
548
|
+
return period
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* A term in words, with the extension it may get (`format: 'duration'`):
|
|
553
|
+
* `P28D` reads "4 weeks", and with an extension of `P14D` "4 weeks,
|
|
554
|
+
* extendable by 2" (the unit is said once when both share it). A value that
|
|
555
|
+
* is not a one-unit period is returned as written, '' for null or empty.
|
|
556
|
+
*
|
|
557
|
+
* @spec openspec/changes/data-widget-duration-format/specs/data-widget/spec.md#requirement-a-term-reads-in-words-with-its-extension
|
|
558
|
+
* @param {unknown} value The stored term.
|
|
559
|
+
* @param {unknown} [extension] The stored extension period, if any.
|
|
560
|
+
* @return {string}
|
|
561
|
+
*/
|
|
562
|
+
export function formatTermDuration(value, extension = null) {
|
|
563
|
+
const term = termPeriod(value)
|
|
564
|
+
if (term === null) {
|
|
565
|
+
return value === null || value === undefined ? '' : String(value)
|
|
566
|
+
}
|
|
567
|
+
const words = formatIsoDuration(`P${term.amount}${UNIT_DESIGNATOR[term.unit]}`)
|
|
568
|
+
const extra = termPeriod(extension)
|
|
569
|
+
if (extra === null || extra.amount === 0) {
|
|
570
|
+
return words
|
|
571
|
+
}
|
|
572
|
+
const extraWords = extra.unit === term.unit
|
|
573
|
+
? String(extra.amount)
|
|
574
|
+
: formatIsoDuration(`P${extra.amount}${UNIT_DESIGNATOR[extra.unit]}`)
|
|
575
|
+
return t('nextcloud-vue', '{term}, extendable by {extension}', { term: words, extension: extraWords })
|
|
576
|
+
}
|
|
577
|
+
|
|
527
578
|
/**
|
|
528
579
|
* A Nextcloud user id as its display name (`userName` registry key): the id
|
|
529
580
|
* while the name is looked up, and for a user nobody answers for.
|
|
@@ -553,6 +604,7 @@ export const BUILT_IN_FORMATTERS = {
|
|
|
553
604
|
connectionSettingsLabel: formatConnectionSettingsLabel,
|
|
554
605
|
masked: formatMasked,
|
|
555
606
|
isoDuration: formatIsoDuration,
|
|
607
|
+
termDuration: formatTermDuration,
|
|
556
608
|
valueMap: formatValueMap,
|
|
557
609
|
userName: formatUserName,
|
|
558
610
|
}
|