@conduction/nextcloud-vue 2.99.0 → 2.100.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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +6 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +25 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -8
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +136 -7
- 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/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +1 -1
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +38 -9
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +16 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -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 +348 -161
- 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 +1482 -998
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +348 -161
- package/l10n/en.json +7 -1
- package/l10n/nl.json +7 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +28 -9
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
- package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
- package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
- package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +136 -7
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +114 -7
- package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +38 -9
- package/src/components/CnSetupWizard/CnSetupWizard.vue +37 -3
- package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
- package/src/css/detail-page.css +15 -0
- package/src/css/dialog.css +48 -0
- package/src/css/look-board.css +83 -1
- package/src/schemas/app-manifest-v2.schema.json +25 -1
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1578,6 +1578,8 @@ export default {
|
|
|
1578
1578
|
*/
|
|
1579
1579
|
inject: {
|
|
1580
1580
|
cnCustomComponents: { default: () => ({}) },
|
|
1581
|
+
/** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
|
|
1582
|
+
cnPermissions: { default: () => [] },
|
|
1581
1583
|
/** The app id CnAppRoot provides, for the plural form of `countSubtitle`. */
|
|
1582
1584
|
cnAppId: { default: '' },
|
|
1583
1585
|
/**
|
|
@@ -1668,7 +1670,9 @@ export default {
|
|
|
1668
1670
|
|
|
1669
1671
|
/**
|
|
1670
1672
|
* A note after the breadcrumb and a middle dot (manifest `config.headerMeta`),
|
|
1671
|
-
* such as "{total} open": `{total}` and `{shown}` are the list's counts
|
|
1673
|
+
* such as "{total} open": `{total}` and `{shown}` are the list's counts,
|
|
1674
|
+
* and `{count:<name>}` the count of a list named in `headerMetaCounts`
|
|
1675
|
+
* ("{total} contracten bij {count:suppliers} leveranciers").
|
|
1672
1676
|
*
|
|
1673
1677
|
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
1674
1678
|
*/
|
|
@@ -1677,12 +1681,29 @@ export default {
|
|
|
1677
1681
|
default: '',
|
|
1678
1682
|
},
|
|
1679
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* The other lists `headerMeta` counts (manifest `config.headerMetaCounts`):
|
|
1686
|
+
* `{ <name>: { register, schema, filter? } }`, each read as one list
|
|
1687
|
+
* total and filled in for `{count:<name>}`. While a count loads, or when
|
|
1688
|
+
* it fails, the note is left out rather than shown with a gap.
|
|
1689
|
+
*
|
|
1690
|
+
* @type {Record<string, {register: string, schema: string, filter?: object}>|null}
|
|
1691
|
+
*/
|
|
1692
|
+
headerMetaCounts: {
|
|
1693
|
+
type: Object,
|
|
1694
|
+
default: null,
|
|
1695
|
+
},
|
|
1696
|
+
|
|
1680
1697
|
/**
|
|
1681
1698
|
* Tabs to sibling pages above the toolbar (manifest `config.pageTabs`),
|
|
1682
1699
|
* each `{ id?, label, route?, params?, query?, href?, countFrom?: {
|
|
1683
|
-
* register, schema, filter? } }`: DqPrivacy's
|
|
1684
|
-
* Datalekken". The current page's tab is marked;
|
|
1685
|
-
* count of that list.
|
|
1700
|
+
* register, schema, filter? }, adminOnly?, permission? }`: DqPrivacy's
|
|
1701
|
+
* "AVG-verzoeken | Datalekken". The current page's tab is marked;
|
|
1702
|
+
* `countFrom` adds the count of that list. A tab with `adminOnly` shows
|
|
1703
|
+
* only to an instance admin, one with `permission` only to a user who
|
|
1704
|
+
* holds it. That hides the tab and nothing more: the page it points to
|
|
1705
|
+
* must still refuse the user on the server. Wins over a named source's
|
|
1706
|
+
* own page tabs.
|
|
1686
1707
|
*
|
|
1687
1708
|
* @type {Array<object>|null}
|
|
1688
1709
|
* @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
|
|
@@ -3722,6 +3743,8 @@ export default {
|
|
|
3722
3743
|
filterStripCounts: null,
|
|
3723
3744
|
/** The `countFrom` counts of the manifest page tabs, by index. */
|
|
3724
3745
|
pageTabCounts: {},
|
|
3746
|
+
/** The `headerMetaCounts` totals, by name; a failed one is null. */
|
|
3747
|
+
headerMetaCountValues: {},
|
|
3725
3748
|
/** The tabs `quickFiltersFrom` produced, or null before they load. */
|
|
3726
3749
|
generatedQuickFilters: null,
|
|
3727
3750
|
/** Count per saved-view id, for the views control; null = none. */
|
|
@@ -6202,9 +6225,13 @@ export default {
|
|
|
6202
6225
|
if (!Array.isArray(this.pageTabs) || this.pageTabs.length === 0) {
|
|
6203
6226
|
return this.sourcePageTabs
|
|
6204
6227
|
}
|
|
6228
|
+
// The index is the tab's place in `pageTabs`, which is how its count
|
|
6229
|
+
// is keyed, so the tabs are numbered before any is left out.
|
|
6205
6230
|
return this.pageTabs
|
|
6206
|
-
.
|
|
6207
|
-
.
|
|
6231
|
+
.map((tab, index) => ({ tab, index }))
|
|
6232
|
+
.filter(({ tab }) => tab && typeof tab.label === 'string' && tab.label !== '' && (tab.route || tab.to || tab.href))
|
|
6233
|
+
.filter(({ tab }) => this.pageTabVisible(tab))
|
|
6234
|
+
.map(({ tab, index }) => {
|
|
6208
6235
|
const to = tab.route
|
|
6209
6236
|
? { name: tab.route, ...(tab.params ? { params: tab.params } : {}), ...(tab.query ? { query: tab.query } : {}) }
|
|
6210
6237
|
: (tab.to || tab.href)
|
|
@@ -6646,9 +6673,41 @@ export default {
|
|
|
6646
6673
|
return ''
|
|
6647
6674
|
}
|
|
6648
6675
|
const total = this.effectivePagination?.total
|
|
6649
|
-
|
|
6676
|
+
let missing = false
|
|
6677
|
+
const text = this.cnTranslate(headerMeta)
|
|
6650
6678
|
.replace('{shown}', String(this.effectiveObjects.length))
|
|
6651
6679
|
.replace('{total}', typeof total === 'number' ? String(total) : '')
|
|
6680
|
+
.replace(/\{count:([\w-]+)\}/g, (match, name) => {
|
|
6681
|
+
const value = this.headerMetaCountValues[name]
|
|
6682
|
+
if (typeof value !== 'number') {
|
|
6683
|
+
missing = true
|
|
6684
|
+
return ''
|
|
6685
|
+
}
|
|
6686
|
+
return String(value)
|
|
6687
|
+
})
|
|
6688
|
+
return missing ? '' : text
|
|
6689
|
+
},
|
|
6690
|
+
|
|
6691
|
+
/**
|
|
6692
|
+
* Whether the header note carries a count, so the board leaves out its
|
|
6693
|
+
* own "{shown} of {total}" line (DqPrivacy, DqContracten).
|
|
6694
|
+
*
|
|
6695
|
+
* @return {boolean}
|
|
6696
|
+
*/
|
|
6697
|
+
headerMetaCarriesCount() {
|
|
6698
|
+
const headerMeta = this.presented('headerMeta')
|
|
6699
|
+
return typeof headerMeta === 'string' && /\{(shown|total|count:[\w-]+)\}/.test(headerMeta)
|
|
6700
|
+
},
|
|
6701
|
+
|
|
6702
|
+
/**
|
|
6703
|
+
* What the header note's other counts depend on, as one key.
|
|
6704
|
+
*
|
|
6705
|
+
* @return {string}
|
|
6706
|
+
*/
|
|
6707
|
+
headerMetaCountKey() {
|
|
6708
|
+
const sources = this.headerMetaCounts && typeof this.headerMetaCounts === 'object' ? this.headerMetaCounts : {}
|
|
6709
|
+
const valid = Object.entries(sources).filter(([, from]) => from && from.register && from.schema)
|
|
6710
|
+
return valid.length > 0 ? JSON.stringify(valid) : ''
|
|
6652
6711
|
},
|
|
6653
6712
|
|
|
6654
6713
|
headerDescription() {
|
|
@@ -6661,6 +6720,10 @@ export default {
|
|
|
6661
6720
|
// reads "14 van 14" in Dutch; a page's own template goes
|
|
6662
6721
|
// through the app's label lookup.
|
|
6663
6722
|
const countText = this.presented('countText')
|
|
6723
|
+
// The note after the breadcrumb already says how many there are.
|
|
6724
|
+
if (!countText && !this.countSubtitle && this.headerMetaCarriesCount) {
|
|
6725
|
+
return this.description
|
|
6726
|
+
}
|
|
6664
6727
|
if (!countText && !this.countSubtitle) {
|
|
6665
6728
|
return t('nextcloud-vue', '{shown} of {total}', { shown, total })
|
|
6666
6729
|
}
|
|
@@ -6907,6 +6970,13 @@ export default {
|
|
|
6907
6970
|
},
|
|
6908
6971
|
},
|
|
6909
6972
|
|
|
6973
|
+
headerMetaCountKey: {
|
|
6974
|
+
immediate: true,
|
|
6975
|
+
handler() {
|
|
6976
|
+
this.loadHeaderMetaCounts()
|
|
6977
|
+
},
|
|
6978
|
+
},
|
|
6979
|
+
|
|
6910
6980
|
pageTabCountKey: {
|
|
6911
6981
|
immediate: true,
|
|
6912
6982
|
/** @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs */
|
|
@@ -7326,6 +7396,65 @@ export default {
|
|
|
7326
7396
|
]
|
|
7327
7397
|
},
|
|
7328
7398
|
|
|
7399
|
+
/**
|
|
7400
|
+
* Whether Nextcloud says the user administers this instance; false
|
|
7401
|
+
* when the user cannot be read.
|
|
7402
|
+
*
|
|
7403
|
+
* @return {boolean}
|
|
7404
|
+
*/
|
|
7405
|
+
viewerIsAdmin() {
|
|
7406
|
+
try {
|
|
7407
|
+
return getCurrentUser()?.isAdmin === true
|
|
7408
|
+
} catch {
|
|
7409
|
+
return false
|
|
7410
|
+
}
|
|
7411
|
+
},
|
|
7412
|
+
|
|
7413
|
+
/**
|
|
7414
|
+
* Whether a manifest page tab shows to this user: an `adminOnly` tab to
|
|
7415
|
+
* an instance admin, a tab with a `permission` to a user who holds it
|
|
7416
|
+
* (as a menu entry: with no permissions known, it shows). This only
|
|
7417
|
+
* decides whether the tab is drawn. It is not access control; the
|
|
7418
|
+
* page behind the tab must enforce its own access on the server.
|
|
7419
|
+
*
|
|
7420
|
+
* @param {object} tab The tab declaration.
|
|
7421
|
+
* @return {boolean} True when the tab is drawn.
|
|
7422
|
+
*/
|
|
7423
|
+
pageTabVisible(tab) {
|
|
7424
|
+
if (tab.adminOnly === true && !this.viewerIsAdmin()) {
|
|
7425
|
+
return false
|
|
7426
|
+
}
|
|
7427
|
+
if (tab.permission) {
|
|
7428
|
+
const held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []
|
|
7429
|
+
if (held.length > 0 && !held.includes(tab.permission)) {
|
|
7430
|
+
return false
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
return true
|
|
7434
|
+
},
|
|
7435
|
+
|
|
7436
|
+
/**
|
|
7437
|
+
* Read the totals of the lists `headerMetaCounts` names, one list
|
|
7438
|
+
* request each. A failed count is null and leaves the note out.
|
|
7439
|
+
*
|
|
7440
|
+
* @return {Promise<void>}
|
|
7441
|
+
*/
|
|
7442
|
+
async loadHeaderMetaCounts() {
|
|
7443
|
+
const key = this.headerMetaCountKey
|
|
7444
|
+
if (key === '') {
|
|
7445
|
+
this.headerMetaCountValues = {}
|
|
7446
|
+
return
|
|
7447
|
+
}
|
|
7448
|
+
const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
|
|
7449
|
+
const values = {}
|
|
7450
|
+
await Promise.all(JSON.parse(key).map(async ([name, from]) => {
|
|
7451
|
+
values[name] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx).catch(() => null)
|
|
7452
|
+
}))
|
|
7453
|
+
if (key === this.headerMetaCountKey) {
|
|
7454
|
+
this.headerMetaCountValues = values
|
|
7455
|
+
}
|
|
7456
|
+
},
|
|
7457
|
+
|
|
7329
7458
|
/**
|
|
7330
7459
|
* Count the lists behind the manifest page tabs (`countFrom`), one list
|
|
7331
7460
|
* request each. A failed count shows no number.
|
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
<p v-else-if="rows.length === 0" class="cn-record-list__empty" data-testid="cn-record-list-empty">
|
|
18
18
|
{{ emptyLabel }}
|
|
19
19
|
</p>
|
|
20
|
+
<!-- notice: the rows as sentences in one toned box ("2 waarschuwingen: ..."). -->
|
|
21
|
+
<div
|
|
22
|
+
v-else-if="presentation === 'notice'"
|
|
23
|
+
class="cn-record-list__notice"
|
|
24
|
+
:class="`cn-record-list__notice--${noticeVariant}`"
|
|
25
|
+
role="note"
|
|
26
|
+
data-testid="cn-record-list-notice">
|
|
27
|
+
<CnIcon
|
|
28
|
+
:name="noticeIcon"
|
|
29
|
+
:size="18"
|
|
30
|
+
class="cn-record-list__notice-icon"
|
|
31
|
+
aria-hidden="true" />
|
|
32
|
+
<p class="cn-record-list__notice-text">
|
|
33
|
+
{{ noticeText }}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
20
36
|
<ul v-else class="cn-record-list__list">
|
|
21
37
|
<li
|
|
22
38
|
v-for="item in items"
|
|
@@ -210,7 +226,22 @@ import { readPath } from '../../utils/readPath.js'
|
|
|
210
226
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
211
227
|
|
|
212
228
|
const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
|
|
213
|
-
const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards']
|
|
229
|
+
const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice']
|
|
230
|
+
|
|
231
|
+
/** The tones a `notice` list takes. */
|
|
232
|
+
const NOTICE_VARIANTS = ['info', 'warning', 'error']
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A row that is a plain string or number (an endpoint answering `["...", "..."]`)
|
|
236
|
+
* read as `{ value }`, so the templates reach it as `{value}` and a list
|
|
237
|
+
* without a `title` shows the value itself. An object row is returned as is.
|
|
238
|
+
*
|
|
239
|
+
* @param {(object|string|number)} row The row.
|
|
240
|
+
* @return {(object|string|number)} The row, or `{ value }` for a string or number.
|
|
241
|
+
*/
|
|
242
|
+
function scalarRow(row) {
|
|
243
|
+
return typeof row === 'string' || typeof row === 'number' ? { value: String(row) } : row
|
|
244
|
+
}
|
|
214
245
|
|
|
215
246
|
/**
|
|
216
247
|
* Unwrap a value that may be a ref.
|
|
@@ -266,7 +297,7 @@ export default {
|
|
|
266
297
|
props: {
|
|
267
298
|
/**
|
|
268
299
|
* The list config: `{ register, schema, filter?, sort?, limit?, extend?,
|
|
269
|
-
* presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
|
|
300
|
+
* presentation?, variant?, lead?, title?, meta?, text?, label?, pill?, trailing?, icon?,
|
|
270
301
|
* rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
|
|
271
302
|
* valueLabels?, endpointSource?, bar? }`. `bar` (`{ valueField, maxField?,
|
|
272
303
|
* overVariant? }`) draws a thin track under each row: filled to
|
|
@@ -275,7 +306,14 @@ export default {
|
|
|
275
306
|
* largest row. `endpointSource` (`{ url, method?,
|
|
276
307
|
* params?, responsePath? }`) reads the rows from an app endpoint instead
|
|
277
308
|
* of `register` / `schema`. `valueLabels` maps a field's stored values to the labels
|
|
278
|
-
* the templates show.
|
|
309
|
+
* the templates show. `headerNote` and `headerLink.label` fill `{total}`
|
|
310
|
+
* and `{count:<field>=<value>}`, the loaded rows with that value.
|
|
311
|
+
* A row that is a plain string or number (an endpoint
|
|
312
|
+
* answering `["…", "…"]`) is read as `{ value }`: templates reach it as
|
|
313
|
+
* `{value}`, and without `title` a row shows the value itself.
|
|
314
|
+
* `presentation: "notice"` draws the rows as sentences in one box toned by
|
|
315
|
+
* `variant` (`info`, `warning` or `error`), after an optional `lead`
|
|
316
|
+
* (`{total}` filled in).
|
|
279
317
|
*
|
|
280
318
|
* @type {object}
|
|
281
319
|
*/
|
|
@@ -334,6 +372,35 @@ export default {
|
|
|
334
372
|
|
|
335
373
|
computed: {
|
|
336
374
|
/** @return {string} `rows`, `feed`, `inline` or `cards`. */
|
|
375
|
+
/**
|
|
376
|
+
* The tone of a `notice` list (`content.variant`): info, warning or error.
|
|
377
|
+
*
|
|
378
|
+
* @return {string} The variant, `info` when unset or unknown.
|
|
379
|
+
* @spec openspec/changes/record-list-string-rows/specs/record-list-string-rows/spec.md#requirement-a-record-list-draws-rows-that-are-plain-strings
|
|
380
|
+
*/
|
|
381
|
+
noticeVariant() {
|
|
382
|
+
const value = this.content?.variant
|
|
383
|
+
return NOTICE_VARIANTS.includes(value) ? value : 'info'
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/** @return {string} The icon of the notice tone. */
|
|
387
|
+
noticeIcon() {
|
|
388
|
+
return this.noticeVariant === 'info' ? 'InformationOutline' : 'AlertOutline'
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The rows of a `notice` list as one paragraph: each row's title, as
|
|
393
|
+
* sentences, after the optional `lead` with `{total}` filled in.
|
|
394
|
+
*
|
|
395
|
+
* @return {string} The text.
|
|
396
|
+
* @spec openspec/changes/record-list-string-rows/specs/record-list-string-rows/spec.md#requirement-a-record-list-draws-rows-that-are-plain-strings
|
|
397
|
+
*/
|
|
398
|
+
noticeText() {
|
|
399
|
+
const parts = this.items.map((item) => item.title).filter((text) => text !== '')
|
|
400
|
+
const lead = typeof this.content?.lead === 'string' && this.content.lead !== '' ? this.withTotal(this.content.lead) : ''
|
|
401
|
+
return [lead, ...parts].filter(Boolean).join(' ')
|
|
402
|
+
},
|
|
403
|
+
|
|
337
404
|
presentation() {
|
|
338
405
|
const value = this.content?.presentation
|
|
339
406
|
return PRESENTATIONS.includes(value) ? value : 'rows'
|
|
@@ -421,7 +488,8 @@ export default {
|
|
|
421
488
|
items() {
|
|
422
489
|
const c = this.content || {}
|
|
423
490
|
const barScale = this.barScale
|
|
424
|
-
return this.rows.slice(0, this.limit).map((
|
|
491
|
+
return this.rows.slice(0, this.limit).map((raw, index) => {
|
|
492
|
+
const row = scalarRow(raw)
|
|
425
493
|
const link = this.rowLink(row)
|
|
426
494
|
return {
|
|
427
495
|
key: (row && (row.id || row['@self']?.id)) || `row-${index}`,
|
|
@@ -430,7 +498,7 @@ export default {
|
|
|
430
498
|
tag: link ? 'router-link' : 'div',
|
|
431
499
|
attrs: link ? { to: link } : {},
|
|
432
500
|
initials: this.initialsFor(row),
|
|
433
|
-
title: this.fill(c.title, row),
|
|
501
|
+
title: this.fill(c.title || (row !== raw ? '{value}' : ''), row),
|
|
434
502
|
meta: this.metaOf(row),
|
|
435
503
|
text: this.fill(c.text, row),
|
|
436
504
|
label: this.fill(c.label, row),
|
|
@@ -638,13 +706,22 @@ export default {
|
|
|
638
706
|
},
|
|
639
707
|
|
|
640
708
|
/**
|
|
641
|
-
*
|
|
709
|
+
* Fill a translated label's counts: `{total}` is the list's total, and
|
|
710
|
+
* `{count:<field>=<value>}` the loaded rows whose field reads that
|
|
711
|
+
* value ("{count:status=done} van {total} stappen klaar"). The count
|
|
712
|
+
* reads the rows the list loaded, so it is exact when `limit` covers
|
|
713
|
+
* the list.
|
|
642
714
|
*
|
|
643
715
|
* @param {string} label The label.
|
|
644
716
|
* @return {string} The text.
|
|
645
717
|
*/
|
|
646
718
|
withTotal(label) {
|
|
647
|
-
return this.translate(label)
|
|
719
|
+
return this.translate(label)
|
|
720
|
+
.replace(/\{total\}/g, String(this.total))
|
|
721
|
+
.replace(/\{count:([^=}]+)=([^}]*)\}/g, (match, field, value) => {
|
|
722
|
+
const wanted = value.trim()
|
|
723
|
+
return String(this.rows.filter((row) => String(readPath(row, field.trim())) === wanted).length)
|
|
724
|
+
})
|
|
648
725
|
},
|
|
649
726
|
|
|
650
727
|
/**
|
|
@@ -1125,6 +1202,36 @@ export default {
|
|
|
1125
1202
|
margin: 0;
|
|
1126
1203
|
}
|
|
1127
1204
|
|
|
1205
|
+
.cn-record-list__notice {
|
|
1206
|
+
display: flex;
|
|
1207
|
+
gap: 10px;
|
|
1208
|
+
align-items: flex-start;
|
|
1209
|
+
padding: 12px 14px;
|
|
1210
|
+
border-radius: var(--border-radius-large, 10px);
|
|
1211
|
+
background: var(--color-primary-element-light, var(--color-background-dark));
|
|
1212
|
+
color: var(--color-main-text);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.cn-record-list__notice--warning {
|
|
1216
|
+
background: var(--color-warning-light, rgba(255, 196, 0, 0.12));
|
|
1217
|
+
color: var(--color-warning-text, var(--color-main-text));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.cn-record-list__notice--error {
|
|
1221
|
+
background: var(--color-error-light, rgba(219, 0, 0, 0.08));
|
|
1222
|
+
color: var(--color-error-text, var(--color-main-text));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.cn-record-list__notice-icon {
|
|
1226
|
+
flex: 0 0 auto;
|
|
1227
|
+
margin-top: 1px;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.cn-record-list__notice-text {
|
|
1231
|
+
margin: 0;
|
|
1232
|
+
line-height: 1.5;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1128
1235
|
.cn-record-list__loading {
|
|
1129
1236
|
display: flex;
|
|
1130
1237
|
justify-content: center;
|
|
@@ -68,6 +68,32 @@ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
|
|
|
68
68
|
import { CnSettingsField } from '../CnSettingsField/index.js'
|
|
69
69
|
import { CnSettingsSection } from '../CnSettingsSection/index.js'
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A value by key: the key as written when the object holds it (a flat map
|
|
73
|
+
* with dotted keys such as `integration.brk.mode`), else as a dotted path.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} data The object.
|
|
76
|
+
* @param {string} key The key or dotted path.
|
|
77
|
+
* @return {unknown}
|
|
78
|
+
*/
|
|
79
|
+
function literalFirst(data, key) {
|
|
80
|
+
if (data && typeof data === 'object' && Object.hasOwn(data, key)) {
|
|
81
|
+
return data[key]
|
|
82
|
+
}
|
|
83
|
+
return readPath(data, key)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The request options for a target: an OCS endpoint (`/ocs/...`) needs the
|
|
88
|
+
* `OCS-APIRequest` header; `prefixUrl` already serves it outside index.php.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} url The target.
|
|
91
|
+
* @return {object}
|
|
92
|
+
*/
|
|
93
|
+
function ocsConfig(url) {
|
|
94
|
+
return /^\/ocs\//.test(String(url)) ? { headers: { 'OCS-APIRequest': 'true' } } : {}
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
/**
|
|
72
98
|
* Up to two initials of a name, for an image field without an image.
|
|
73
99
|
*
|
|
@@ -146,15 +172,16 @@ export default {
|
|
|
146
172
|
* The fields: `[{ key, label, type?, hint?, placeholder?, required?,
|
|
147
173
|
* pattern?, patternMessage?, min?, max?, rows?, options?, optionsUrl?,
|
|
148
174
|
* optionsPath?, optionLabel?, optionValue?, default?, readOnly?, wide?,
|
|
149
|
-
* url?, valuePath?, saveUrl?, saveMethod?, saveKey?, uploadUrl?,
|
|
175
|
+
* url?, valuePath?, saveUrl?, saveMethod?, saveKey?, saveParams?, uploadUrl?,
|
|
150
176
|
* uploadField?, uploadParams?, uploadValuePath?, initials?, initialsFrom? }]`.
|
|
151
177
|
* `type` is `text` (default), `textarea`, `number`, `switch`, `select`,
|
|
152
178
|
* `secret` or `image`. `key` names the setting; a dotted key reads a
|
|
153
179
|
* nested value and is written as written. A field with its own `url`
|
|
154
180
|
* is read there (at `valuePath`, default its key) and saved to its
|
|
155
181
|
* `saveUrl` (default that `url`) with `saveMethod` (default POST) under
|
|
156
|
-
* `saveKey` (default its key),
|
|
157
|
-
*
|
|
182
|
+
* `saveKey` (default its key), with `saveParams` merged into the body,
|
|
183
|
+
* so one section can hold the settings of several apps. A target under
|
|
184
|
+
* `/ocs/` is sent the `OCS-APIRequest` header. An `image` uploads at once to `uploadUrl`. Labels,
|
|
158
185
|
* hints, placeholders and messages go through the app's translate
|
|
159
186
|
* function.
|
|
160
187
|
*
|
|
@@ -343,7 +370,7 @@ export default {
|
|
|
343
370
|
const failures = {}
|
|
344
371
|
await Promise.all(targets.map(async (target) => {
|
|
345
372
|
try {
|
|
346
|
-
const response = await axios.get(prefixUrl(target))
|
|
373
|
+
const response = await axios.get(prefixUrl(target), ocsConfig(target))
|
|
347
374
|
answers[target] = response?.data ?? {}
|
|
348
375
|
} catch (error) {
|
|
349
376
|
failures[target] = error
|
|
@@ -396,7 +423,8 @@ export default {
|
|
|
396
423
|
|
|
397
424
|
/**
|
|
398
425
|
* A field's value in its read answer: at its `valuePath` (a field with its
|
|
399
|
-
* own `url`), else at the section's `valuesPath`, then its `key`.
|
|
426
|
+
* own `url`), else at the section's `valuesPath`, then its `key`. A key
|
|
427
|
+
* the answer holds as written wins over reading it as a dotted path.
|
|
400
428
|
*
|
|
401
429
|
* @param {object} field The field.
|
|
402
430
|
* @param {object|null} data The answer of the field's read target.
|
|
@@ -407,10 +435,10 @@ export default {
|
|
|
407
435
|
return undefined
|
|
408
436
|
}
|
|
409
437
|
if (field.url) {
|
|
410
|
-
return
|
|
438
|
+
return literalFirst(data, field.valuePath || field.key)
|
|
411
439
|
}
|
|
412
|
-
const values = (this.valuesPath ?
|
|
413
|
-
return
|
|
440
|
+
const values = (this.valuesPath ? literalFirst(data, this.valuesPath) : data) || {}
|
|
441
|
+
return literalFirst(values, field.key)
|
|
414
442
|
},
|
|
415
443
|
|
|
416
444
|
/**
|
|
@@ -494,6 +522,7 @@ export default {
|
|
|
494
522
|
}
|
|
495
523
|
const group = groups.get(id)
|
|
496
524
|
group.fields.push(field)
|
|
525
|
+
Object.assign(group.data, field.saveParams || {})
|
|
497
526
|
group.data[field.saveKey || field.key] = value
|
|
498
527
|
}
|
|
499
528
|
this.saving = true
|
|
@@ -502,7 +531,7 @@ export default {
|
|
|
502
531
|
const sent = {}
|
|
503
532
|
for (const group of groups.values()) {
|
|
504
533
|
try {
|
|
505
|
-
await axios.request({ url: prefixUrl(group.url), method: group.method, data: group.data })
|
|
534
|
+
await axios.request({ url: prefixUrl(group.url), method: group.method, data: group.data, ...ocsConfig(group.url) })
|
|
506
535
|
Object.assign(sent, group.data)
|
|
507
536
|
this.acceptSaved(group.fields)
|
|
508
537
|
} catch (error) {
|
|
@@ -69,7 +69,12 @@
|
|
|
69
69
|
|
|
70
70
|
<!-- choice -->
|
|
71
71
|
<template v-else-if="step.type === 'choice'">
|
|
72
|
-
|
|
72
|
+
<!-- The board sets the intro as a plain paragraph above the
|
|
73
|
+
cards (DqInstellen); the Nextcloud look keeps the note. -->
|
|
74
|
+
<p v-if="step.body && isBoard" class="cn-setup-step__intro" data-testid="cn-setup-step-intro">
|
|
75
|
+
{{ stepBody(step) }}
|
|
76
|
+
</p>
|
|
77
|
+
<NcNoteCard v-else-if="step.body" type="info">
|
|
73
78
|
{{ stepBody(step) }}
|
|
74
79
|
</NcNoteCard>
|
|
75
80
|
<CnChoiceCards
|
|
@@ -119,6 +124,9 @@
|
|
|
119
124
|
<NcNoteCard v-if="isChoiceDisabled(step)" type="warning">
|
|
120
125
|
{{ dependsOnHint(step) }}
|
|
121
126
|
</NcNoteCard>
|
|
127
|
+
<p v-if="stepNote(step)" class="cn-setup-step__note" data-testid="cn-setup-step-note">
|
|
128
|
+
{{ stepNote(step) }}
|
|
129
|
+
</p>
|
|
122
130
|
</template>
|
|
123
131
|
|
|
124
132
|
<!-- config-fields -->
|
|
@@ -212,6 +220,7 @@ import CnChoiceCards from '../CnChoiceCards/CnChoiceCards.vue'
|
|
|
212
220
|
import CnLeafDependencySettings from '../CnLeafDependencySettings/CnLeafDependencySettings.vue'
|
|
213
221
|
import CnWizardDialog from '../CnWizardDialog/CnWizardDialog.vue'
|
|
214
222
|
import { checkDependencies, isDependencyResolved, missingRequiredApps, readServerAppStatuses } from '../../composables/useDependencyCheck.js'
|
|
223
|
+
import { useLook } from '../../composables/useLook.js'
|
|
215
224
|
import { useSetupStatus } from '../../composables/useSetupStatus.js'
|
|
216
225
|
import { fieldsFromSchema } from '../../utils/schema.js'
|
|
217
226
|
|
|
@@ -428,12 +437,13 @@ export default {
|
|
|
428
437
|
* @return {object} `setupStatus` / `setupStatusLoading` for the option resolver.
|
|
429
438
|
*/
|
|
430
439
|
setup(props) {
|
|
440
|
+
const { isBoard } = useLook()
|
|
431
441
|
const wantsSource = (props.steps || []).some((s) => s && s.optionsSource)
|
|
432
442
|
if (!wantsSource) {
|
|
433
|
-
return {}
|
|
443
|
+
return { isBoard }
|
|
434
444
|
}
|
|
435
445
|
const { status, loading } = useSetupStatus(props.appId, { setup: { steps: props.steps } })
|
|
436
|
-
return { setupStatus: status, setupStatusLoading: loading }
|
|
446
|
+
return { isBoard, setupStatus: status, setupStatusLoading: loading }
|
|
437
447
|
},
|
|
438
448
|
|
|
439
449
|
data() {
|
|
@@ -793,6 +803,18 @@ export default {
|
|
|
793
803
|
return raw === '' ? '' : this.cnTranslate(raw)
|
|
794
804
|
},
|
|
795
805
|
|
|
806
|
+
/**
|
|
807
|
+
* The footnote under a choice step's options, from the step's `note`
|
|
808
|
+
* (or `footnote`), translated like the body.
|
|
809
|
+
*
|
|
810
|
+
* @param {object} step The step declaration.
|
|
811
|
+
* @return {string} The translated footnote, or ''.
|
|
812
|
+
*/
|
|
813
|
+
stepNote(step) {
|
|
814
|
+
const raw = (step && (step.note || step.footnote)) || ''
|
|
815
|
+
return raw === '' ? '' : this.cnTranslate(raw)
|
|
816
|
+
},
|
|
817
|
+
|
|
796
818
|
stepSlot(step) {
|
|
797
819
|
return 'step-' + step.id
|
|
798
820
|
},
|
|
@@ -1338,6 +1360,18 @@ export default {
|
|
|
1338
1360
|
gap: 12px;
|
|
1339
1361
|
}
|
|
1340
1362
|
|
|
1363
|
+
.cn-setup-step__intro {
|
|
1364
|
+
margin: 0;
|
|
1365
|
+
font-size: 15px;
|
|
1366
|
+
line-height: 1.5;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.cn-setup-step__note {
|
|
1370
|
+
margin: 0;
|
|
1371
|
+
font-size: 13px;
|
|
1372
|
+
color: var(--color-text-maxcontrast);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1341
1375
|
.cn-setup-step__nav {
|
|
1342
1376
|
display: flex;
|
|
1343
1377
|
justify-content: flex-end;
|
|
@@ -208,7 +208,7 @@ export default {
|
|
|
208
208
|
*/
|
|
209
209
|
dialogTitle: {
|
|
210
210
|
type: String,
|
|
211
|
-
default: 'Wizard',
|
|
211
|
+
default: () => t('nextcloud-vue', 'Wizard'),
|
|
212
212
|
},
|
|
213
213
|
|
|
214
214
|
/**
|
|
@@ -279,17 +279,17 @@ export default {
|
|
|
279
279
|
submitIcon: { type: [Object, Function], default: null },
|
|
280
280
|
|
|
281
281
|
/** Cancel-button label (wizard phase). */
|
|
282
|
-
cancelLabel: { type: String, default: 'Cancel' },
|
|
282
|
+
cancelLabel: { type: String, default: () => t('nextcloud-vue', 'Cancel') },
|
|
283
283
|
/** Back-button label. */
|
|
284
|
-
backLabel: { type: String, default: 'Back' },
|
|
284
|
+
backLabel: { type: String, default: () => t('nextcloud-vue', 'Back') },
|
|
285
285
|
/** Next-button label (intermediate steps). */
|
|
286
|
-
nextLabel: { type: String, default: 'Next' },
|
|
286
|
+
nextLabel: { type: String, default: () => t('nextcloud-vue', 'Next') },
|
|
287
287
|
/** Submit-button label (last step). */
|
|
288
|
-
submitLabel: { type: String, default: 'Submit' },
|
|
288
|
+
submitLabel: { type: String, default: () => t('nextcloud-vue', 'Submit') },
|
|
289
289
|
/** Close-button label (result phase). */
|
|
290
|
-
closeLabel: { type: String, default: 'Close' },
|
|
290
|
+
closeLabel: { type: String, default: () => t('nextcloud-vue', 'Close') },
|
|
291
291
|
/** Success banner default text when result.message is empty. */
|
|
292
|
-
successText: { type: String, default: 'Done.' },
|
|
292
|
+
successText: { type: String, default: () => t('nextcloud-vue', 'Done.') },
|
|
293
293
|
/**
|
|
294
294
|
* Whether the dialog can be dismissed (Cancel button, ESC, backdrop
|
|
295
295
|
* click) before reaching the result phase. `false` hides Cancel and
|
package/src/css/detail-page.css
CHANGED
|
@@ -525,3 +525,18 @@
|
|
|
525
525
|
align-items: center;
|
|
526
526
|
gap: 4px;
|
|
527
527
|
}
|
|
528
|
+
|
|
529
|
+
/* The tag after an admin-only entry in a board detail page's Actions menu
|
|
530
|
+
("beheerder", DqZaakMeer): 12px muted text at the end of the row. The menu
|
|
531
|
+
opens outside the page, so the rule keys on the attribute CnDetailPage sets
|
|
532
|
+
only in the board look. */
|
|
533
|
+
.action-button[data-cn-admin-tag]::after,
|
|
534
|
+
.action-link[data-cn-admin-tag]::after {
|
|
535
|
+
content: attr(data-cn-admin-tag);
|
|
536
|
+
margin-inline-start: auto;
|
|
537
|
+
padding-inline: 12px 4px;
|
|
538
|
+
color: var(--color-text-maxcontrast);
|
|
539
|
+
font-size: 12px;
|
|
540
|
+
font-weight: 400;
|
|
541
|
+
white-space: nowrap;
|
|
542
|
+
}
|