@conduction/nextcloud-vue 2.102.0 → 2.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +7 -7
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +4 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/pageViews.js +8 -1
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +586 -416
- package/dist/esm/utils/nextcloudDirectory.js +18 -1
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/esm/utils/sortOrder.js +61 -0
- package/dist/esm/utils/sortOrder.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +13 -2
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +12270 -10936
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +586 -416
- package/l10n/en.json +4 -0
- package/l10n/nl.json +4 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +1 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
- package/src/components/CnAppRoot/CnAppRoot.md +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
- package/src/components/CnConversationThread/CnConversationThread.md +17 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
- package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
- package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +49 -0
- package/src/components/CnDetailPage/CnDetailPage.md +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
- package/src/components/CnFormDialog/CnFormDialog.md +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
- package/src/components/CnIndexPage/CnIndexPage.md +14 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
- package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
- package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
- package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
- package/src/components/CnSearchSelect/index.js +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +7 -7
- package/src/css/dialog.css +5 -3
- package/src/css/index-page.css +19 -0
- package/src/css/look-board-index.css +48 -0
- package/src/index.js +1 -0
- package/src/mixins/pageViews.js +8 -1
- package/src/schemas/app-manifest-v2.schema.json +75 -6
- package/src/utils/nextcloudDirectory.js +17 -0
- package/src/utils/sortOrder.js +58 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +13 -2
package/l10n/en.json
CHANGED
package/l10n/nl.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.104.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": {
|
|
@@ -153,6 +153,7 @@ Controlling the inline action button count — `inlineActionCount` sets how many
|
|
|
153
153
|
| `mapLabel` | `String` | `''` | Label for the map view-toggle option (defaults to "Map"). Only shown when `showMap` |
|
|
154
154
|
| `mapIcon` | `String` | `''` | MDI icon name for the map option (defaults to the built-in map-marker icon). Resolved via `CnIcon` |
|
|
155
155
|
| `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set |
|
|
156
|
+
| `searchPlacement` | `String` | `''` | Board look only: `'toolbar'` draws the search field on row 1, before Filter and the view switch, instead of at the start of row 2 |
|
|
156
157
|
| `searchValue` | `String` | `''` | Current value of the inline search field (controlled) |
|
|
157
158
|
| `searchPlaceholder` | `String` | `''` | Placeholder / accessible label for the inline search field |
|
|
158
159
|
| `showCountWithSearch` | `Boolean` | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls. No effect without `showSearch` |
|
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
@description Under the board look: saved views as chips and the Save view button, in row 1 after the quick-filter chips.
|
|
19
19
|
-->
|
|
20
20
|
<slot name="actions-end" />
|
|
21
|
+
<!-- `searchPlacement: "toolbar"`: the search field on row 1, before
|
|
22
|
+
Filter and the view switch, instead of opening row 2. -->
|
|
23
|
+
<div v-if="showSearch && searchInToolbar"
|
|
24
|
+
class="cn-actions-bar__search cn-actions-bar__search--toolbar"
|
|
25
|
+
data-testid="cn-actions-bar-search-toolbar">
|
|
26
|
+
<Magnify :size="18" class="cn-actions-bar__search-icon" />
|
|
27
|
+
<input
|
|
28
|
+
type="search"
|
|
29
|
+
class="cn-actions-bar__search-input"
|
|
30
|
+
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search…')"
|
|
31
|
+
:value="searchValue"
|
|
32
|
+
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
|
|
33
|
+
@input="onSearchInput">
|
|
34
|
+
</div>
|
|
21
35
|
<!--
|
|
22
36
|
Filter and the view switch are one unit: when row 1 is full they
|
|
23
37
|
wrap together to the start of the next line (the DqZaken board),
|
|
@@ -373,7 +387,7 @@
|
|
|
373
387
|
|
|
374
388
|
<!-- Board look, row 2: the search field, then the active filters. -->
|
|
375
389
|
<div v-if="isBoardLayout" class="cn-actions-bar__row cn-actions-bar__row--search" data-testid="cn-actions-bar-row-search">
|
|
376
|
-
<div v-if="showSearch" class="cn-actions-bar__search">
|
|
390
|
+
<div v-if="showSearch && !searchInToolbar" class="cn-actions-bar__search">
|
|
377
391
|
<Magnify :size="18" class="cn-actions-bar__search-icon" />
|
|
378
392
|
<input
|
|
379
393
|
type="search"
|
|
@@ -383,7 +397,7 @@
|
|
|
383
397
|
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
|
|
384
398
|
@input="onSearchInput">
|
|
385
399
|
</div>
|
|
386
|
-
<span v-else-if="showCount && hasTotal" class="cn-actions-bar__count">
|
|
400
|
+
<span v-else-if="showCount && hasTotal && !searchInToolbar" class="cn-actions-bar__count">
|
|
387
401
|
{{ countText }}
|
|
388
402
|
</span>
|
|
389
403
|
<slot name="after-search" />
|
|
@@ -943,6 +957,19 @@ export default {
|
|
|
943
957
|
default: false,
|
|
944
958
|
},
|
|
945
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Where the board look draws the search field: `''` (the default) at
|
|
962
|
+
* the start of row 2, `'toolbar'` on row 1 before Filter and the view
|
|
963
|
+
* switch (DqTeamwachtrij). No effect outside the board look.
|
|
964
|
+
*
|
|
965
|
+
* @type {''|'toolbar'}
|
|
966
|
+
*/
|
|
967
|
+
searchPlacement: {
|
|
968
|
+
type: String,
|
|
969
|
+
default: '',
|
|
970
|
+
validator: (v) => ['', 'toolbar'].includes(v),
|
|
971
|
+
},
|
|
972
|
+
|
|
946
973
|
/** Current value of the inline search field (controlled) */
|
|
947
974
|
searchValue: {
|
|
948
975
|
type: String,
|
|
@@ -1101,6 +1128,16 @@ export default {
|
|
|
1101
1128
|
],
|
|
1102
1129
|
|
|
1103
1130
|
computed: {
|
|
1131
|
+
/**
|
|
1132
|
+
* Whether the search field sits on the board's row 1.
|
|
1133
|
+
*
|
|
1134
|
+
* @spec openspec/changes/index-search-on-toolbar/specs/index-page/spec.md#requirement-the-search-field-can-sit-on-the-toolbar-row
|
|
1135
|
+
* @return {boolean}
|
|
1136
|
+
*/
|
|
1137
|
+
searchInToolbar() {
|
|
1138
|
+
return this.isBoardLayout && this.searchPlacement === 'toolbar'
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1104
1141
|
/**
|
|
1105
1142
|
* Whether the board toolbar is drawn: the `layout` prop, else the look
|
|
1106
1143
|
* the app provides.
|
|
@@ -67,7 +67,7 @@ With loading state, custom components, translate, permissions, and custom page t
|
|
|
67
67
|
|------|------|----------|---------|-------------|
|
|
68
68
|
| `manifest` | Object | Yes | — | Reactive manifest object from `useAppManifest`. Provides `manifest.dependencies`, `manifest.menu`, and is propagated via provide/inject |
|
|
69
69
|
| `appId` | String | Yes | — | Nextcloud app id. Forwarded to `NcContent` and `CnDependencyMissing` |
|
|
70
|
-
| `appDisplayName` | String | No | `''` | Human-readable name shown in the Nextcloud top bar. When set it overrides the technical `appId` so a virtual app shows its own name (e.g. "Pet Store") instead of the host app id. |
|
|
70
|
+
| `appDisplayName` | String | No | `''` | Human-readable name shown in the Nextcloud top bar. When set it overrides the technical `appId` so a virtual app shows its own name (e.g. "Pet Store") instead of the host app id. The user settings dialog, the setup wizard and the credentials section show it through the app's `translate`; without it they show the name Nextcloud's app menu gives (localised), else the id with a capital, never the bare id. The top bar's own name, which also keys the stored list-pane width (`pane-list-size-<name>`), stays untranslated so a reader's pane width survives a language switch. |
|
|
71
71
|
| `persistManifestDelta` | Function | No | `null` | In-app editing (ADR-041) persistence hook, called with the minimal manifest delta on save. When Buildiq is reachable for the user, CnAppRoot provides a shared `cnManifestEditor` + `cnOpenBuildAvailable` (the inject key deliberately keeps its old spelling — see the rename note in CHANGELOG); the page action rows (`CnDashboardPage`/`CnDetailPage`/`CnIndexPage`) then render an icon-only `CnBuildiqEditButton` inline with their normal buttons, and the body grid becomes drag/resizable in edit mode. Omit to keep edits in-memory only. |
|
|
72
72
|
| `dataSources` | Object \| null | No | `null` | App registers/schemas for the in-app pages editor (ADR-041). Lets the Edit-pages modal offer Register / Schema / Columns dropdowns for `index`/`detail` pages instead of free-text slug inputs, so a created page actually renders a table. Shape: `{ registers: [{ value, label, schemas: [{ value, label, columns: string[] }] }] }`. Provided to descendants as `cnDataSources`; when omitted the editor falls back to free-text register/schema fields. **A snapshot** — captured once, so it cannot show anything created after boot. Prefer `dataSourcesLoader`. |
|
|
73
73
|
| `dataSourcesLoader` | Function \| null | No | `null` | Async loader for the same data, re-invoked every time a pages-editor modal opens: `async () => ({ registers: [...] })`. Because `provide()` runs once, the static `dataSources` snapshot can never change — so a register or schema created after the app booted (in another tab, the OpenRegister UI, or via the API) would never appear in the dropdowns. A loader fixes that with no page reload, and moves the fetch off the app-boot path onto the far rarer editor-open path. When both props are given, `dataSources` seeds the initial list and the loader's result replaces it on the first refresh. Provided to descendants as the `cnDataSourcesState` holder plus a `cnRefreshDataSources()` action. |
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
<div class="cn-app-root__setup">
|
|
181
181
|
<CnSetupWizard
|
|
182
182
|
:appId="appId"
|
|
183
|
-
:appName="
|
|
183
|
+
:appName="shownAppName"
|
|
184
184
|
:steps="manifest.setup.steps"
|
|
185
185
|
:cancellable="false"
|
|
186
186
|
:completedStepIds="setupCompletedStepIds"
|
|
@@ -441,7 +441,7 @@
|
|
|
441
441
|
<div v-if="setupWizardOpen" class="cn-app-root__setup-optional">
|
|
442
442
|
<CnSetupWizard
|
|
443
443
|
:appId="appId"
|
|
444
|
-
:appName="
|
|
444
|
+
:appName="shownAppName"
|
|
445
445
|
:steps="manifest.setup.steps"
|
|
446
446
|
:cancellable="true"
|
|
447
447
|
:completedStepIds="setupCompletedStepIds"
|
|
@@ -580,7 +580,7 @@
|
|
|
580
580
|
<CnCredentials
|
|
581
581
|
scope="personal"
|
|
582
582
|
:appId="appId"
|
|
583
|
-
:appName="
|
|
583
|
+
:appName="shownAppName"
|
|
584
584
|
:appCredentials="(manifest && manifest.credentials) || []" />
|
|
585
585
|
</NcAppSettingsSection>
|
|
586
586
|
<!--
|
|
@@ -747,6 +747,7 @@ import {
|
|
|
747
747
|
import { RegistryKindError } from '../../errors/RegistryKindError.js'
|
|
748
748
|
import { useObjectStore } from '../../store/index.js'
|
|
749
749
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
750
|
+
import { appShownName } from '../../utils/appShownName.js'
|
|
750
751
|
import { BUILT_IN_FORMATTERS } from '../../utils/builtInFormatters.js'
|
|
751
752
|
import { addDiagnosticsListener, clearReportedDiagnostics, reportDiagnostic } from '../../utils/diagnostics.js'
|
|
752
753
|
import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
|
|
@@ -3134,6 +3135,22 @@ export default {
|
|
|
3134
3135
|
return this.userSettingsTitle || this.libraryCopy('User settings')
|
|
3135
3136
|
},
|
|
3136
3137
|
|
|
3138
|
+
/**
|
|
3139
|
+
* The app's name as the reader reads it: `appDisplayName` or the
|
|
3140
|
+
* manifest's `name` through the app's translate, else the name
|
|
3141
|
+
* Nextcloud's app menu shows (localised), else the id with a capital.
|
|
3142
|
+
* Used for the user settings dialog, the setup wizard and the
|
|
3143
|
+
* credentials section; never for NcContent's `appName`, which also
|
|
3144
|
+
* keys the stored list-pane width (`pane-list-size-<appName>`).
|
|
3145
|
+
*
|
|
3146
|
+
* @spec openspec/changes/app-shown-name-in-chrome/specs/app-root/spec.md#requirement-the-app-chrome-shows-the-apps-localised-name
|
|
3147
|
+
* @return {string}
|
|
3148
|
+
*/
|
|
3149
|
+
shownAppName() {
|
|
3150
|
+
const given = this.appDisplayName || (this.manifest && this.manifest.name) || ''
|
|
3151
|
+
return appShownName(this.appId, given ? this.translate(given) : '')
|
|
3152
|
+
},
|
|
3153
|
+
|
|
3137
3154
|
/**
|
|
3138
3155
|
* The props of the user-settings dialog: `NcAppSettingsDialog`'s, or
|
|
3139
3156
|
* under the board look CnUserSettingsDialog's (eyebrow, subtitle and
|
|
@@ -3149,7 +3166,7 @@ export default {
|
|
|
3149
3166
|
const props = {
|
|
3150
3167
|
open: this.userSettingsOpen,
|
|
3151
3168
|
title: this.resolvedUserSettingsTitle,
|
|
3152
|
-
appName: this.
|
|
3169
|
+
appName: this.shownAppName,
|
|
3153
3170
|
}
|
|
3154
3171
|
if (this.userSettingsEyebrow) {
|
|
3155
3172
|
props.eyebrow = this.translate(this.userSettingsEyebrow)
|
|
@@ -23,6 +23,23 @@ Read-only, for a closed record:
|
|
|
23
23
|
]" />
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Channel chips and a header action. `channel-filter` draws All plus a chip per channel with its count, `channels` lists labels that always get a chip, and the `header-actions` slot puts buttons beside the title:
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<CnConversationThread
|
|
30
|
+
title="Contact"
|
|
31
|
+
channel-filter
|
|
32
|
+
:channels="['E-mail', 'Letter']"
|
|
33
|
+
:messages="[
|
|
34
|
+
{ id: 1, author: 'Sanne de Vries', side: 'them', channel: 'E-mail', text: 'Could I see the documents?' },
|
|
35
|
+
{ id: 2, side: 'us', channel: 'E-mail', text: 'They are on their way.' },
|
|
36
|
+
]">
|
|
37
|
+
<template #header-actions>
|
|
38
|
+
<NcButton>Log contact</NcButton>
|
|
39
|
+
</template>
|
|
40
|
+
</CnConversationThread>
|
|
41
|
+
```
|
|
42
|
+
|
|
26
43
|
No messages yet:
|
|
27
44
|
|
|
28
45
|
```vue
|
|
@@ -9,13 +9,27 @@
|
|
|
9
9
|
data-testid="cn-conversation-thread"
|
|
10
10
|
:aria-labelledby="title ? titleId : null"
|
|
11
11
|
:aria-label="title ? null : fallbackLabel">
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
<div v-if="title || $slots['header-actions']" class="cn-conversation-thread__header">
|
|
13
|
+
<component
|
|
14
|
+
:is="titleTag"
|
|
15
|
+
v-if="title"
|
|
16
|
+
:id="titleId"
|
|
17
|
+
class="cn-conversation-thread__title">
|
|
18
|
+
{{ title }}
|
|
19
|
+
</component>
|
|
20
|
+
<div v-if="$slots['header-actions']" class="cn-conversation-thread__header-actions">
|
|
21
|
+
<!-- @slot header-actions Buttons beside the title, for example "Log contact". -->
|
|
22
|
+
<slot name="header-actions" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<CnQuickFilterBar
|
|
27
|
+
v-if="channelChips.length > 0"
|
|
28
|
+
inline
|
|
29
|
+
class="cn-conversation-thread__channels"
|
|
30
|
+
:tabs="channelChips"
|
|
31
|
+
:activeIndex="activeChannelIndex"
|
|
32
|
+
@update:activeIndex="onChannelChip" />
|
|
19
33
|
|
|
20
34
|
<p v-if="loading && rows.length === 0"
|
|
21
35
|
class="cn-conversation-thread__empty"
|
|
@@ -27,9 +41,13 @@
|
|
|
27
41
|
{{ emptyText }}
|
|
28
42
|
</p>
|
|
29
43
|
|
|
44
|
+
<p v-else-if="shownRows.length === 0" class="cn-conversation-thread__empty" data-testid="cn-conversation-channel-empty">
|
|
45
|
+
{{ emptyText }}
|
|
46
|
+
</p>
|
|
47
|
+
|
|
30
48
|
<ol v-else class="cn-conversation-thread__messages">
|
|
31
49
|
<li
|
|
32
|
-
v-for="row in
|
|
50
|
+
v-for="row in shownRows"
|
|
33
51
|
:key="row.key"
|
|
34
52
|
class="cn-conversation-thread__message"
|
|
35
53
|
:class="`cn-conversation-thread__message--${row.side}`"
|
|
@@ -93,6 +111,7 @@
|
|
|
93
111
|
import axios from '@nextcloud/axios'
|
|
94
112
|
import { translate as t } from '@nextcloud/l10n'
|
|
95
113
|
import { NcButton } from '@nextcloud/vue'
|
|
114
|
+
import CnQuickFilterBar from '../CnQuickFilterBar/CnQuickFilterBar.vue'
|
|
96
115
|
import { prefixUrl } from '../../utils/headers.js'
|
|
97
116
|
import { userLocale } from '../../utils/userLocale.js'
|
|
98
117
|
|
|
@@ -126,6 +145,7 @@ export default {
|
|
|
126
145
|
name: 'CnConversationThread',
|
|
127
146
|
|
|
128
147
|
components: {
|
|
148
|
+
CnQuickFilterBar,
|
|
129
149
|
NcButton,
|
|
130
150
|
},
|
|
131
151
|
|
|
@@ -204,6 +224,24 @@ export default {
|
|
|
204
224
|
default: false,
|
|
205
225
|
},
|
|
206
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Draw channel chips above the messages: All, then one per channel
|
|
229
|
+
* with its count. Choosing one shows only that channel's messages.
|
|
230
|
+
*/
|
|
231
|
+
channelFilter: {
|
|
232
|
+
type: Boolean,
|
|
233
|
+
default: false,
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Channel labels that always get a chip, in this order, also when no
|
|
238
|
+
* message uses them yet. Channels the messages use follow.
|
|
239
|
+
*/
|
|
240
|
+
channels: {
|
|
241
|
+
type: Array,
|
|
242
|
+
default: () => [],
|
|
243
|
+
},
|
|
244
|
+
|
|
207
245
|
/** Shown when there are no messages yet. */
|
|
208
246
|
emptyText: {
|
|
209
247
|
type: String,
|
|
@@ -224,6 +262,8 @@ export default {
|
|
|
224
262
|
conversationThreadUid += 1
|
|
225
263
|
return {
|
|
226
264
|
draft: '',
|
|
265
|
+
/** The channel the chips narrow the thread to; '' is All. */
|
|
266
|
+
activeChannel: '',
|
|
227
267
|
sending: false,
|
|
228
268
|
errorMessage: '',
|
|
229
269
|
titleId: `cn-conversation-title-${conversationThreadUid}`,
|
|
@@ -262,6 +302,47 @@ export default {
|
|
|
262
302
|
})
|
|
263
303
|
},
|
|
264
304
|
|
|
305
|
+
/**
|
|
306
|
+
* The channel labels with a chip: the declared ones, then the ones
|
|
307
|
+
* the messages use.
|
|
308
|
+
*
|
|
309
|
+
* @return {Array<string>} The labels, each once.
|
|
310
|
+
*/
|
|
311
|
+
channelLabels() {
|
|
312
|
+
const declared = (Array.isArray(this.channels) ? this.channels : []).filter((label) => typeof label === 'string' && label !== '')
|
|
313
|
+
return [...new Set([...declared, ...this.rows.map((row) => row.channel).filter(Boolean)])]
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The chips: All, then each channel, each with its count. Empty
|
|
318
|
+
* without `channelFilter` or without channels.
|
|
319
|
+
*
|
|
320
|
+
* @return {Array<{label: string, count: number}>} The chips.
|
|
321
|
+
*/
|
|
322
|
+
channelChips() {
|
|
323
|
+
if (!this.channelFilter || this.channelLabels.length === 0) {
|
|
324
|
+
return []
|
|
325
|
+
}
|
|
326
|
+
return [
|
|
327
|
+
{ label: t('nextcloud-vue', 'All'), count: this.rows.length },
|
|
328
|
+
...this.channelLabels.map((label) => ({ label, count: this.rows.filter((row) => row.channel === label).length })),
|
|
329
|
+
]
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/** @return {number} The pressed chip: 0 for All. */
|
|
333
|
+
activeChannelIndex() {
|
|
334
|
+
const index = this.channelLabels.indexOf(this.activeChannel)
|
|
335
|
+
return index < 0 ? 0 : index + 1
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/** @return {Array<object>} The rows on the chosen channel, or all of them. */
|
|
339
|
+
shownRows() {
|
|
340
|
+
if (this.channelChips.length === 0 || this.activeChannelIndex === 0) {
|
|
341
|
+
return this.rows
|
|
342
|
+
}
|
|
343
|
+
return this.rows.filter((row) => row.channel === this.activeChannel)
|
|
344
|
+
},
|
|
345
|
+
|
|
265
346
|
/**
|
|
266
347
|
* Whether there is something to send and nothing in flight.
|
|
267
348
|
*
|
|
@@ -289,6 +370,16 @@ export default {
|
|
|
289
370
|
},
|
|
290
371
|
|
|
291
372
|
methods: {
|
|
373
|
+
/**
|
|
374
|
+
* Narrow the thread to the chip's channel; the All chip (0) clears it.
|
|
375
|
+
*
|
|
376
|
+
* @param {number|null} index The chip index.
|
|
377
|
+
* @return {void}
|
|
378
|
+
*/
|
|
379
|
+
onChannelChip(index) {
|
|
380
|
+
this.activeChannel = index > 0 ? (this.channelLabels[index - 1] || '') : ''
|
|
381
|
+
},
|
|
382
|
+
|
|
292
383
|
/**
|
|
293
384
|
* Up to two initials of a name, for the avatar.
|
|
294
385
|
*
|
|
@@ -370,6 +461,20 @@ export default {
|
|
|
370
461
|
gap: calc(3.5 * var(--default-grid-baseline));
|
|
371
462
|
}
|
|
372
463
|
|
|
464
|
+
.cn-conversation-thread__header {
|
|
465
|
+
align-items: flex-start;
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-wrap: wrap;
|
|
468
|
+
gap: calc(3 * var(--default-grid-baseline));
|
|
469
|
+
justify-content: space-between;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.cn-conversation-thread__header-actions {
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-wrap: wrap;
|
|
475
|
+
gap: calc(2 * var(--default-grid-baseline));
|
|
476
|
+
}
|
|
477
|
+
|
|
373
478
|
.cn-conversation-thread__title {
|
|
374
479
|
font-size: 1.1em;
|
|
375
480
|
font-weight: 700;
|
|
@@ -15,12 +15,27 @@
|
|
|
15
15
|
:endpoint="replyAction ? '' : endpoint"
|
|
16
16
|
:loading="loading"
|
|
17
17
|
:emptyText="content.emptyText ? tr(content.emptyText) : undefined"
|
|
18
|
+
:channelFilter="content.channelChips === true"
|
|
19
|
+
:channels="channelLabels"
|
|
18
20
|
@send="onSend"
|
|
19
|
-
@sent="onSent"
|
|
21
|
+
@sent="onSent">
|
|
22
|
+
<template v-if="headerAction" #header-actions>
|
|
23
|
+
<NcButton
|
|
24
|
+
data-testid="cn-conversation-header-action"
|
|
25
|
+
@click="onHeaderAction">
|
|
26
|
+
<template v-if="headerAction.icon" #icon>
|
|
27
|
+
<CnIcon :name="headerAction.icon" :size="20" />
|
|
28
|
+
</template>
|
|
29
|
+
{{ tr(headerAction.label) }}
|
|
30
|
+
</NcButton>
|
|
31
|
+
</template>
|
|
32
|
+
</CnConversationThread>
|
|
20
33
|
</div>
|
|
21
34
|
</template>
|
|
22
35
|
|
|
23
36
|
<script>
|
|
37
|
+
import { NcButton } from '@nextcloud/vue'
|
|
38
|
+
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
24
39
|
import CnConversationThread from './CnConversationThread.vue'
|
|
25
40
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
26
41
|
import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'
|
|
@@ -45,12 +60,20 @@ import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveF
|
|
|
45
60
|
* `replyAction`, an action descriptor the app decides the meaning of: it is
|
|
46
61
|
* dispatched with `args: [{ text, objectId }]` and `props.text`. After a send
|
|
47
62
|
* the source is read again.
|
|
63
|
+
*
|
|
64
|
+
* `channelChips: true` draws a chip per channel above the thread (every
|
|
65
|
+
* `channelMap` label, in map order, then any other channel a message uses).
|
|
66
|
+
* `headerAction` is a button beside the title (`{ label, icon?, ...action }`,
|
|
67
|
+
* DqZaakContact's "Contact vastleggen"), dispatched like `replyAction` with
|
|
68
|
+
* `args: [{ objectId }]`; after it the source is read again.
|
|
48
69
|
*/
|
|
49
70
|
export default {
|
|
50
71
|
name: 'CnConversationWidget',
|
|
51
72
|
|
|
52
73
|
components: {
|
|
53
74
|
CnConversationThread,
|
|
75
|
+
CnIcon,
|
|
76
|
+
NcButton,
|
|
54
77
|
},
|
|
55
78
|
|
|
56
79
|
inject: {
|
|
@@ -102,6 +125,18 @@ export default {
|
|
|
102
125
|
return source && typeof source === 'object' && source.register && source.schema ? source : null
|
|
103
126
|
},
|
|
104
127
|
|
|
128
|
+
/** @return {object|null} The header action, or null without a label. */
|
|
129
|
+
headerAction() {
|
|
130
|
+
const action = this.content?.headerAction
|
|
131
|
+
return action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '' ? action : null
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** @return {Array<string>} The `channelMap` labels, translated, each once. */
|
|
135
|
+
channelLabels() {
|
|
136
|
+
const map = this.content?.channelMap && typeof this.content.channelMap === 'object' ? this.content.channelMap : {}
|
|
137
|
+
return [...new Set(Object.values(map).filter((label) => typeof label === 'string' && label !== '').map((label) => this.tr(label)))]
|
|
138
|
+
},
|
|
139
|
+
|
|
105
140
|
/** @return {object|null} The reply action, or null. */
|
|
106
141
|
replyAction() {
|
|
107
142
|
const action = this.content?.replyAction
|
|
@@ -254,6 +289,30 @@ export default {
|
|
|
254
289
|
this.onSent({ text })
|
|
255
290
|
},
|
|
256
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Run `headerAction` with the record id, then read the source again.
|
|
294
|
+
*
|
|
295
|
+
* @return {Promise<void>}
|
|
296
|
+
*/
|
|
297
|
+
async onHeaderAction() {
|
|
298
|
+
if (!this.headerAction) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const { label, icon, ...rest } = resolveDeepTokens(this.headerAction, this.tokenCtx)
|
|
302
|
+
const action = { ...rest, args: [{ objectId: this.objectId }] }
|
|
303
|
+
try {
|
|
304
|
+
const outcome = await (typeof this.cnDispatchAction === 'function'
|
|
305
|
+
? this.cnDispatchAction(action)
|
|
306
|
+
: dispatchAction(action, { router: this.$router || null }))
|
|
307
|
+
if (outcome === false) {
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
await this.loadSource()
|
|
314
|
+
},
|
|
315
|
+
|
|
257
316
|
/**
|
|
258
317
|
* Show an accepted reply at once, without waiting for the record.
|
|
259
318
|
*
|
|
@@ -126,12 +126,15 @@ predicate's field value into their text wherever it says `{value}` (e.g.
|
|
|
126
126
|
| `gridMargin` | Number | `12` | Grid margin (gutter) in pixels between widgets |
|
|
127
127
|
| `emptyLabel` | String | `'No widgets configured'` | Text shown in the empty state when `layout` is empty |
|
|
128
128
|
| `unavailableLabel` | String | `'Widget not available'` | Text shown for unknown or unavailable widgets |
|
|
129
|
-
| `userLayout` | Boolean | `false` | Keep a per-user arrangement of this page. Off by default: a page without it makes **no** layout request and renders exactly as before. |
|
|
129
|
+
| `userLayout` | Boolean | `false` | **Deprecated** (layout belongs to Buildiq; removed after this release). Keep a per-user arrangement of this page. Off by default: a page without it makes **no** layout request and renders exactly as before. |
|
|
130
130
|
| `appId` | String | `''` | The Nextcloud app the user layout is stored under. Required for `userLayout` to do anything. |
|
|
131
131
|
| `userLayoutStore` | Object | `null` | A store exposing `loadDashboardLayout` / `saveDashboardLayout` / `resetDashboardLayout`, usually one carrying [`dashboardLayoutsPlugin`](../../../docs/store/plugins/dashboard-layouts.md). Left unset, the page reads and writes user preferences directly. |
|
|
132
132
|
|
|
133
133
|
## Per-user layout
|
|
134
134
|
|
|
135
|
+
> **Deprecated: layout belongs to Buildiq.** Arranging a page is Buildiq's job (Ruben, 18 Oct 2026): its **Indeling aanpassen** edits the layout everybody sees. `userLayout` still works for this release and will be removed after it. While Buildiq edits the page (`cnEditingBody`), the grid shows the manifest layout, and a drag or resize goes into the manifest Buildiq saves, also on a page with `userLayout`. A user arrangement made outside an in-page edit session is saved at once, because no **Done** follows to save it.
|
|
136
|
+
|
|
137
|
+
|
|
135
138
|
With `userLayout: true` and an `appId`, this page loads the current user's arrangement before the first grid render and merges it over the manifest layout. The rules are [`mergeUserLayout`](../../../docs/utilities/merge-user-layout.md)'s: **the manifest decides which widgets exist, the user decides where they sit.**
|
|
136
139
|
|
|
137
140
|
```vue
|
|
@@ -1839,6 +1839,9 @@ export default {
|
|
|
1839
1839
|
* and renders exactly as it did before this existed, which is the
|
|
1840
1840
|
* property that makes shipping it safe on every dashboard at once.
|
|
1841
1841
|
*
|
|
1842
|
+
* Deprecated: layout belongs to Buildiq. Kept working for one release.
|
|
1843
|
+
*
|
|
1844
|
+
* @deprecated
|
|
1842
1845
|
* @type {boolean}
|
|
1843
1846
|
*/
|
|
1844
1847
|
userLayout: {
|
|
@@ -2216,9 +2219,25 @@ export default {
|
|
|
2216
2219
|
* @return {boolean}
|
|
2217
2220
|
*/
|
|
2218
2221
|
gridEditable() {
|
|
2222
|
+
return this.isEditing || this.editingBody
|
|
2223
|
+
},
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Whether Buildiq is editing the page (the injected `cnEditingBody`
|
|
2227
|
+
* ref, or a plain boolean). Buildiq is the layout editor: while it
|
|
2228
|
+
* edits, the grid shows and changes the manifest layout, also on a
|
|
2229
|
+
* page with `userLayout`.
|
|
2230
|
+
*
|
|
2231
|
+
* @return {boolean}
|
|
2232
|
+
*/
|
|
2233
|
+
editingBody() {
|
|
2219
2234
|
const e = this.cnEditingBody
|
|
2220
|
-
|
|
2221
|
-
|
|
2235
|
+
return Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e)
|
|
2236
|
+
},
|
|
2237
|
+
|
|
2238
|
+
/** @return {boolean} Whether the grid shows this user's arrangement. */
|
|
2239
|
+
showsUserLayout() {
|
|
2240
|
+
return this.userLayout && !this.editingBody && Array.isArray(this.userLayoutItems)
|
|
2222
2241
|
},
|
|
2223
2242
|
|
|
2224
2243
|
/**
|
|
@@ -2279,9 +2298,7 @@ export default {
|
|
|
2279
2298
|
* @return {Array<object>} The layout.
|
|
2280
2299
|
*/
|
|
2281
2300
|
renderedLayout() {
|
|
2282
|
-
const base =
|
|
2283
|
-
? this.userLayoutItems
|
|
2284
|
-
: this.layout
|
|
2301
|
+
const base = this.showsUserLayout ? this.userLayoutItems : this.layout
|
|
2285
2302
|
// The KPI row is not part of the grid.
|
|
2286
2303
|
if (Array.isArray(this.kpiRow) && this.kpiRow.length > 0 && Array.isArray(base)) {
|
|
2287
2304
|
return base.filter((item) => !this.kpiRow.includes(item.widgetId))
|
|
@@ -3577,7 +3594,8 @@ export default {
|
|
|
3577
3594
|
// write below is for the admin editor, which edits the page for
|
|
3578
3595
|
// everybody; doing it for a user rearranging their own dashboard
|
|
3579
3596
|
// would rewrite the page for everyone who opens it after them.
|
|
3580
|
-
|
|
3597
|
+
// While Buildiq edits, the drag is the admin's and goes below.
|
|
3598
|
+
if (this.showsUserLayout) {
|
|
3581
3599
|
this.userLayoutDirty = true
|
|
3582
3600
|
this.userLayoutItems = this.userLayoutItems.map((item) => {
|
|
3583
3601
|
const u = (Array.isArray(updated) ? updated : []).find((candidate) => String(candidate.id) === String(item.id)
|
|
@@ -3586,6 +3604,11 @@ export default {
|
|
|
3586
3604
|
return (u ? { ...item, ...this.geometryFrom(u) } : item)
|
|
3587
3605
|
})
|
|
3588
3606
|
this.$emit('layout-change', this.userLayoutItems)
|
|
3607
|
+
// Outside an in-page edit session no Done will come to save
|
|
3608
|
+
// the arrangement, so it is saved now.
|
|
3609
|
+
if (!this.isEditing) {
|
|
3610
|
+
this.saveUserLayout()
|
|
3611
|
+
}
|
|
3589
3612
|
|
|
3590
3613
|
return
|
|
3591
3614
|
}
|
|
@@ -229,6 +229,7 @@ features are folded in here as opt-in props (bare-table usage is unchanged):
|
|
|
229
229
|
- `borderless` — drop the container's card chrome so the table sits flush inside
|
|
230
230
|
a parent card (e.g. a `CnWidgetWrapper` dashboard slot).
|
|
231
231
|
- `limit` — show only the first N rows; with `viewAllRoute` a "View all" footer appears.
|
|
232
|
+
- `pageSize` — rows per page: one page of the rows the table holds, with the board's pager under them ("4 of 7", the page buttons, Next) once there is more than one page (DqCatalogus). 0 (the default) shows every row, or the first `limit`.
|
|
232
233
|
- `viewAllRoute` / `viewAllLabel` — the footer control's route and label. It renders
|
|
233
234
|
as a real `<a :href>` when the router resolves the route, and as a
|
|
234
235
|
`<button type="button">` (emitting `view-all`) otherwise, so it always has a
|