@conduction/nextcloud-vue 2.79.0 → 2.80.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/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -8
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +133 -96
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
- 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/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/index.js +30 -0
- package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/index.js +29 -0
- package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +305 -72
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +31 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/index.js +31 -0
- package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/index.js +35 -0
- package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +122 -7
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +140 -9
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection.js +21 -0
- package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
- package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
- package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
- package/dist/esm/integrations/offline/index.js +2 -1
- package/dist/esm/integrations/offline/index.js.map +1 -1
- package/dist/esm/integrations/offline/offlineDb.js +6 -1
- package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
- package/dist/esm/integrations/offline/syncReplayService.js +8 -2
- package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
- package/dist/esm/l10n/en.json.js +12 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +16 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1000 -59
- package/dist/esm/utils/detailActionModel.js +60 -3
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +114 -0
- package/dist/esm/utils/fillTemplate.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/queryPresets.js +96 -0
- package/dist/esm/utils/queryPresets.js.map +1 -0
- package/dist/esm/utils/resolveFilterTokens.js +11 -0
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +21 -0
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +3 -3
- package/dist/esm/utils/sentinelTokens.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/esm/utils/widgetDispatch.js +7 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +12368 -7717
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1000 -59
- package/l10n/en.json +12 -1
- package/l10n/nl.json +16 -2
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
- package/src/components/CnActionsBar/CnActionsBar.md +4 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +42 -10
- package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
- package/src/components/CnCellRenderer/CnCellRenderer.vue +188 -1
- package/src/components/CnDataTable/CnDataTable.vue +1 -0
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
- package/src/components/CnFactsWidget/index.js +29 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
- package/src/components/CnFieldListWidget/index.js +28 -0
- package/src/components/CnFormDialog/CnFormDialog.md +9 -3
- package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
- package/src/components/CnFormPage/CnFormPage.vue +18 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +254 -6
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
- package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
- package/src/components/CnRecordListWidget/index.js +30 -0
- package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
- package/src/components/CnSectionsWidget/index.js +34 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnTabsWidget/CnTabsWidget.vue +184 -8
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
- package/src/components/index.js +4 -0
- package/src/composables/__tests__/useFormDraft.spec.js +69 -0
- package/src/composables/useFormDraft.js +139 -8
- package/src/css/form-field.css +14 -0
- package/src/css/index-page.css +71 -0
- package/src/css/kpi-card.css +32 -0
- package/src/css/look-board-detail.css +77 -0
- package/src/css/look-board-index.css +51 -0
- package/src/css/look-board.css +37 -2
- package/src/css/table.css +19 -0
- package/src/index.js +4 -0
- package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
- package/src/integrations/builtin/field-inspection.js +21 -0
- package/src/integrations/offline/checklistSubmission.js +137 -0
- package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
- package/src/integrations/offline/index.js +3 -0
- package/src/integrations/offline/offlineDb.js +6 -1
- package/src/integrations/offline/syncReplayService.js +8 -2
- package/src/schemas/app-manifest-v2.schema.json +104 -5
- package/src/utils/detailActionModel.js +59 -2
- package/src/utils/fillTemplate.js +110 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/queryPresets.js +93 -0
- package/src/utils/resolveFilterTokens.js +11 -0
- package/src/utils/schema.js +21 -0
- package/src/utils/sentinelTokens.js +3 -3
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +7 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnFieldListWidget — self-registers the `field-list` widget type (a list
|
|
6
|
+
* stored inside one field of the current object, shown as rows and edited
|
|
7
|
+
* with one PATCH of that field) into the shared dashboardWidgetRegistry at
|
|
8
|
+
* module load. Renderer only: declared in the manifest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnFieldListWidget from './CnFieldListWidget.vue'
|
|
12
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('field-list', {
|
|
15
|
+
renderer: CnFieldListWidget,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
field: '',
|
|
19
|
+
row: {},
|
|
20
|
+
readOnly: false,
|
|
21
|
+
},
|
|
22
|
+
displayName: 'Field list',
|
|
23
|
+
icon: 'FormatListBulleted',
|
|
24
|
+
surfaces: ['detail-page'],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export { CnFieldListWidget }
|
|
28
|
+
export default CnFieldListWidget
|
|
@@ -113,7 +113,7 @@ export default {
|
|
|
113
113
|
| `allowDraft` | `false` | Offer a **Save draft** button that stores the record with a draft marker instead of validating it. Inert unless the schema declares `draftField`. |
|
|
114
114
|
| `draftField` | `'isDraft'` | The boolean property that marks a record as a draft. |
|
|
115
115
|
| `draftAppId` | `''` | The app the draft belongs to, for the storage key. |
|
|
116
|
-
| `draftUserId` | `''` | Who is typing, for the storage key.
|
|
116
|
+
| `draftUserId` | `''` | Who is typing, for the storage key. Empty: the signed-in Nextcloud user, and `anonymous` only when nobody is signed in. |
|
|
117
117
|
|
|
118
118
|
#### What the draft does and does not do
|
|
119
119
|
|
|
@@ -122,9 +122,15 @@ Restore and Discard; the values go in only when somebody presses Restore. A form
|
|
|
122
122
|
that filled itself would be indistinguishable from one the server prefilled, and
|
|
123
123
|
somebody would submit last week's answers without knowing they were there.
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
The user is part of the storage key. A shared browser profile at a service
|
|
126
126
|
desk is the ordinary case in a municipality, and a draft keyed without the user
|
|
127
|
-
hands the next person at the counter what the last one typed.
|
|
127
|
+
hands the next person at the counter what the last one typed. Without
|
|
128
|
+
`draftUserId` the dialog uses the signed-in Nextcloud user. A draft stored
|
|
129
|
+
under `anonymous` is removed, never offered, once somebody is signed in.
|
|
130
|
+
|
|
131
|
+
A draft is written only after a person changes a field, and only while that
|
|
132
|
+
field differs from the value the form opened with. Opening a dialog, or the
|
|
133
|
+
form filling in values by itself, writes nothing and shows no Saving or Saved.
|
|
128
134
|
|
|
129
135
|
A draft is dropped rather than offered when it is more than seven days old, when
|
|
130
136
|
the record has been saved by somebody else since it was written, or when the
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
:error="!!errors[field.key]"
|
|
187
187
|
:type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
|
|
188
188
|
:disabled="field.readOnly"
|
|
189
|
-
:placeholder="field
|
|
189
|
+
:placeholder="fieldPlaceholder(field)"
|
|
190
190
|
@update:modelValue="value => updateField(field.key, value)" />
|
|
191
191
|
<CnFieldHelper
|
|
192
192
|
:id="helpId(field)"
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
:error="!!errors[field.key]"
|
|
209
209
|
type="number"
|
|
210
210
|
:disabled="field.readOnly"
|
|
211
|
-
:placeholder="field
|
|
211
|
+
:placeholder="fieldPlaceholder(field)"
|
|
212
212
|
@update:modelValue="value => updateField(field.key, value !== '' ? Number(value) : null)" />
|
|
213
213
|
<CnFieldHelper
|
|
214
214
|
:id="helpId(field)"
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
v-bind="fieldAria(field)"
|
|
231
231
|
:value="formData[field.key] || ''"
|
|
232
232
|
:disabled="field.readOnly"
|
|
233
|
-
:placeholder="field
|
|
233
|
+
:placeholder="fieldPlaceholder(field)"
|
|
234
234
|
rows="4"
|
|
235
235
|
@input="updateField(field.key, $event.target.value)" />
|
|
236
236
|
<CnFieldHelper
|
|
@@ -652,7 +652,7 @@
|
|
|
652
652
|
:modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
|
|
653
653
|
:error="!!errors[field.key]"
|
|
654
654
|
:disabled="field.readOnly"
|
|
655
|
-
:placeholder="field
|
|
655
|
+
:placeholder="fieldPlaceholder(field)"
|
|
656
656
|
@update:modelValue="value => updateField(field.key, value)" />
|
|
657
657
|
<CnFieldHelper
|
|
658
658
|
:id="helpId(field)"
|
|
@@ -787,7 +787,7 @@ import CnPropertySourceField from '../CnPropertySourceField/CnPropertySourceFiel
|
|
|
787
787
|
import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
|
|
788
788
|
import CnSubObjectsField from '../CnSubObjectsField/CnSubObjectsField.vue'
|
|
789
789
|
import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js'
|
|
790
|
-
import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
|
|
790
|
+
import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
|
|
791
791
|
import { heldFromFailures, splitHeldFiles, uploadHeldFiles } from '../../composables/useHeldFileUpload.js'
|
|
792
792
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
793
793
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
|
|
@@ -1172,16 +1172,16 @@ export default {
|
|
|
1172
1172
|
* 🔴 THE USER IS PART OF THE DRAFT KEY. A shared browser profile at a
|
|
1173
1173
|
* service desk is the ordinary case in a municipality, and a draft
|
|
1174
1174
|
* keyed without the user hands the next person at the counter what the
|
|
1175
|
-
* last one typed. A host that passes nothing gets
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1175
|
+
* last one typed. A host that passes nothing gets the signed-in
|
|
1176
|
+
* Nextcloud user (`@nextcloud/auth`), and `anonymous` only when
|
|
1177
|
+
* nobody is signed in.
|
|
1178
1178
|
*/
|
|
1179
1179
|
draftAppId: {
|
|
1180
1180
|
type: String,
|
|
1181
1181
|
default: '',
|
|
1182
1182
|
},
|
|
1183
1183
|
|
|
1184
|
-
/** Who is typing, for the draft key. See `draftAppId`. */
|
|
1184
|
+
/** Who is typing, for the draft key. Empty: the signed-in user. See `draftAppId`. */
|
|
1185
1185
|
draftUserId: {
|
|
1186
1186
|
type: String,
|
|
1187
1187
|
default: '',
|
|
@@ -1442,10 +1442,20 @@ export default {
|
|
|
1442
1442
|
appId: this.draftAppId,
|
|
1443
1443
|
schema: (this.schema?.slug || this.schema?.title || ''),
|
|
1444
1444
|
objectId: (this.item?.id || this.item?.uuid || ''),
|
|
1445
|
-
userId: this.
|
|
1445
|
+
userId: this.draftUserOwner,
|
|
1446
1446
|
})
|
|
1447
1447
|
},
|
|
1448
1448
|
|
|
1449
|
+
/**
|
|
1450
|
+
* Who the draft belongs to: `draftUserId`, else the signed-in user.
|
|
1451
|
+
*
|
|
1452
|
+
* @return {string} The user id, or '' when nobody is signed in.
|
|
1453
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
|
|
1454
|
+
*/
|
|
1455
|
+
draftUserOwner() {
|
|
1456
|
+
return resolveDraftUserId(this.draftUserId)
|
|
1457
|
+
},
|
|
1458
|
+
|
|
1449
1459
|
/**
|
|
1450
1460
|
* Whether "Save draft" may be offered.
|
|
1451
1461
|
*
|
|
@@ -1757,17 +1767,6 @@ export default {
|
|
|
1757
1767
|
formData: {
|
|
1758
1768
|
deep: true,
|
|
1759
1769
|
handler(values) {
|
|
1760
|
-
// 🔴 THE FIRST CHANGE IS THE COMPONENT SEEDING THE FORM, NOT
|
|
1761
|
-
// SOMEBODY TYPING. Writing a draft for it stores the empty
|
|
1762
|
-
// form over a real one, and the indicator announces "Saving
|
|
1763
|
-
// draft" about work nobody did — an instrument reporting
|
|
1764
|
-
// activity that did not happen, which is the whole class of
|
|
1765
|
-
// defect this library keeps paying down.
|
|
1766
|
-
if (this.draftSeeded !== true) {
|
|
1767
|
-
this.draftSeeded = true
|
|
1768
|
-
return
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
1770
|
// Nothing is written while a draft is being offered: the user
|
|
1772
1771
|
// has not chosen yet, and overwriting the stored draft with the
|
|
1773
1772
|
// empty form they are looking at would destroy the thing they
|
|
@@ -1776,6 +1775,19 @@ export default {
|
|
|
1776
1775
|
return
|
|
1777
1776
|
}
|
|
1778
1777
|
|
|
1778
|
+
// 🔴 ONLY A PERSON'S CHANGE IS WORTH A DRAFT. The form fills
|
|
1779
|
+
// in values by itself after it opens (an input reporting its
|
|
1780
|
+
// value back, normalised dates, defaults, the organisation),
|
|
1781
|
+
// and each of those used to write a draft and say "Saved just
|
|
1782
|
+
// now" on a dialog nobody had touched. A field counts once a
|
|
1783
|
+
// person edited it AND it differs from what the form opened
|
|
1784
|
+
// with, so undoing the edit drops the draft again.
|
|
1785
|
+
const edited = Object.keys(this.touchedFields || {}).filter((key) => this.touchedFields[key])
|
|
1786
|
+
if (!hasDraftChanges(values, this.draftBaseline, edited)) {
|
|
1787
|
+
this.settleUnchangedDraft(this.formDraftKey)
|
|
1788
|
+
return
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1779
1791
|
this.scheduleDraftWrite(this.formDraftKey, values)
|
|
1780
1792
|
},
|
|
1781
1793
|
},
|
|
@@ -1940,6 +1952,12 @@ export default {
|
|
|
1940
1952
|
// Silently refilling a form is how somebody submits last week's answers
|
|
1941
1953
|
// without noticing they were there, and a form that fills itself is
|
|
1942
1954
|
// indistinguishable from one the server prefilled.
|
|
1955
|
+
// A draft stored under `anonymous` was written by a page that did not
|
|
1956
|
+
// know who was typing. Once somebody is signed in it is removed, never
|
|
1957
|
+
// offered: it may be another person's.
|
|
1958
|
+
if (this.formDraftKey && this.draftUserOwner) {
|
|
1959
|
+
clearAnonymousDrafts()
|
|
1960
|
+
}
|
|
1943
1961
|
if (this.formDraftKey) {
|
|
1944
1962
|
this.draftOffer = readDraft(this.formDraftKey, {
|
|
1945
1963
|
objectUpdated: (this.item?.updated || this.item?.['@self']?.updated || ''),
|
|
@@ -2298,6 +2316,10 @@ export default {
|
|
|
2298
2316
|
this.runAssignments(null)
|
|
2299
2317
|
this.loadProfileDefaults()
|
|
2300
2318
|
}
|
|
2319
|
+
// What the form opened with. A draft is written only once a field
|
|
2320
|
+
// a person edited differs from this (see the `formData` watcher).
|
|
2321
|
+
// Not reactive: nothing renders from it.
|
|
2322
|
+
this.draftBaseline = JSON.parse(JSON.stringify(this.formData || {}))
|
|
2301
2323
|
},
|
|
2302
2324
|
|
|
2303
2325
|
/**
|
|
@@ -3085,6 +3107,27 @@ export default {
|
|
|
3085
3107
|
return this.showsOuterLabel(field) ? '' : this.errors[field.key]
|
|
3086
3108
|
},
|
|
3087
3109
|
|
|
3110
|
+
/**
|
|
3111
|
+
* The placeholder of a text, number or textarea control.
|
|
3112
|
+
*
|
|
3113
|
+
* The field's description already shows as the help line under the
|
|
3114
|
+
* control, so the placeholder stays empty rather than repeat it word
|
|
3115
|
+
* for word. The help line is the one kept: it is still there after
|
|
3116
|
+
* somebody typed (a placeholder disappears with the first keystroke),
|
|
3117
|
+
* it is read out through `aria-describedby`, and in the Nextcloud look
|
|
3118
|
+
* a placeholder is hidden until the field has focus anyway. While an
|
|
3119
|
+
* error takes the help line's place, the description returns as the
|
|
3120
|
+
* placeholder so it is not lost.
|
|
3121
|
+
*
|
|
3122
|
+
* @param {object} field The field definition.
|
|
3123
|
+
* @return {string} The placeholder, or '' when the help line says it.
|
|
3124
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-field-description-shows-once
|
|
3125
|
+
*/
|
|
3126
|
+
fieldPlaceholder(field) {
|
|
3127
|
+
const description = field.description || ''
|
|
3128
|
+
return this.helperError(field) ? description : ''
|
|
3129
|
+
},
|
|
3130
|
+
|
|
3088
3131
|
/**
|
|
3089
3132
|
* The accessibility attributes of a text control. In both looks an invalid
|
|
3090
3133
|
* control is `aria-invalid` and `aria-describedby` lists the error before
|
|
@@ -4567,6 +4610,11 @@ export default {
|
|
|
4567
4610
|
}
|
|
4568
4611
|
|
|
4569
4612
|
this.formData = { ...this.formData, ...this.draftOffer.values }
|
|
4613
|
+
// What the person typed last time is their edit now: it counts for
|
|
4614
|
+
// the next draft write and survives the record arriving late.
|
|
4615
|
+
for (const key of Object.keys(this.draftOffer.values)) {
|
|
4616
|
+
this.touchedFields[key] = true
|
|
4617
|
+
}
|
|
4570
4618
|
this.draftOffer = null
|
|
4571
4619
|
},
|
|
4572
4620
|
|
|
@@ -397,7 +397,7 @@ import CnFormErrorSummary from '../CnFormErrorSummary/CnFormErrorSummary.vue'
|
|
|
397
397
|
import CnFormField from '../CnFormField/CnFormField.vue'
|
|
398
398
|
import CnStepper from '../CnStepper/CnStepper.vue'
|
|
399
399
|
import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
|
|
400
|
-
import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
|
|
400
|
+
import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
|
|
401
401
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
402
402
|
import { cnSelectAria } from '../../directives/cnSelectAria.js'
|
|
403
403
|
import { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'
|
|
@@ -529,7 +529,7 @@ export default {
|
|
|
529
529
|
default: '',
|
|
530
530
|
},
|
|
531
531
|
|
|
532
|
-
/** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. */
|
|
532
|
+
/** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. Empty: the signed-in Nextcloud user. */
|
|
533
533
|
draftUserId: {
|
|
534
534
|
type: String,
|
|
535
535
|
default: '',
|
|
@@ -949,7 +949,17 @@ export default {
|
|
|
949
949
|
if (this.recoverDraft !== true) {
|
|
950
950
|
return ''
|
|
951
951
|
}
|
|
952
|
-
return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.
|
|
952
|
+
return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserOwner })
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Who the draft belongs to: `draftUserId`, else the signed-in user.
|
|
957
|
+
*
|
|
958
|
+
* @return {string} The user id, or '' when nobody is signed in.
|
|
959
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
|
|
960
|
+
*/
|
|
961
|
+
draftUserOwner() {
|
|
962
|
+
return resolveDraftUserId(this.draftUserId)
|
|
953
963
|
},
|
|
954
964
|
|
|
955
965
|
/**
|
|
@@ -997,6 +1007,11 @@ export default {
|
|
|
997
1007
|
this.calcTimers = {}
|
|
998
1008
|
// Offered, never applied: a form that fills itself is indistinguishable
|
|
999
1009
|
// from one the server prefilled.
|
|
1010
|
+
// A draft under `anonymous` may be somebody else's: removed once a
|
|
1011
|
+
// signed-in user is known, never offered.
|
|
1012
|
+
if (this.formDraftKey && this.draftUserOwner) {
|
|
1013
|
+
clearAnonymousDrafts()
|
|
1014
|
+
}
|
|
1000
1015
|
if (this.formDraftKey) {
|
|
1001
1016
|
this.draftOffer = readDraft(this.formDraftKey)
|
|
1002
1017
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cn-index-page"
|
|
3
|
-
:class="lookClass"
|
|
3
|
+
:class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel }]"
|
|
4
4
|
data-testid="cn-index-page"
|
|
5
5
|
@keydown="onListKeydown">
|
|
6
6
|
<!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
|
|
@@ -76,6 +76,20 @@
|
|
|
76
76
|
<slot name="below-header" />
|
|
77
77
|
</div>
|
|
78
78
|
|
|
79
|
+
<!-- KPI tiles above the list (`statRow`) and cards beside it
|
|
80
|
+
(`sidePanel`), as DqTeamwachtrij and PqContracten
|
|
81
|
+
(screens-index-stat-row). -->
|
|
82
|
+
<CnIndexPageWidgets
|
|
83
|
+
v-if="statRow && statRow.length > 0"
|
|
84
|
+
class="cn-index-page__stat-row"
|
|
85
|
+
variant="stat-row"
|
|
86
|
+
:widgets="statRow" />
|
|
87
|
+
<CnIndexPageWidgets
|
|
88
|
+
v-if="hasSidePanel"
|
|
89
|
+
class="cn-index-page__side-panel"
|
|
90
|
+
variant="side-panel"
|
|
91
|
+
:widgets="sidePanel" />
|
|
92
|
+
|
|
79
93
|
<!-- Actions bar -->
|
|
80
94
|
<CnActionsBar
|
|
81
95
|
:layout="isBoardLook ? 'board' : 'nextcloud'"
|
|
@@ -97,6 +111,7 @@
|
|
|
97
111
|
:viewMode="currentViewMode"
|
|
98
112
|
:showViewToggle="showViewToggle"
|
|
99
113
|
:availableViewModes="effectiveToggleModes"
|
|
114
|
+
:disabledViewModes="viewSwitchDisabledModes"
|
|
100
115
|
:cardsLabel="cardsLabel"
|
|
101
116
|
:tableLabel="tableLabel"
|
|
102
117
|
:listLabel="listLabel"
|
|
@@ -959,6 +974,7 @@ import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
|
|
|
959
974
|
import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
960
975
|
import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
|
|
961
976
|
import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
|
|
977
|
+
import CnIndexPageWidgets from './CnIndexPageWidgets.vue'
|
|
962
978
|
import { useContextMenu } from '../../composables/index.js'
|
|
963
979
|
import { useLook } from '../../composables/useLook.js'
|
|
964
980
|
import { copyKindsOf } from '../../composables/useObjectCopy.js'
|
|
@@ -1243,6 +1259,7 @@ export default {
|
|
|
1243
1259
|
name: 'CnIndexPage',
|
|
1244
1260
|
|
|
1245
1261
|
components: {
|
|
1262
|
+
CnIndexPageWidgets,
|
|
1246
1263
|
CnBuildiqEditButton,
|
|
1247
1264
|
CnFollowToggle,
|
|
1248
1265
|
NcLoadingIcon,
|
|
@@ -1587,7 +1604,10 @@ export default {
|
|
|
1587
1604
|
* `$route.params` just like the `filter` prop. The first tab with
|
|
1588
1605
|
* `default:true` (else index 0) is active on mount; changing tabs
|
|
1589
1606
|
* re-fetches at page 1. Omit (or `null`) → no tab strip, behaviour
|
|
1590
|
-
* unchanged.
|
|
1607
|
+
* unchanged. Under the board look a tab with `activeLabel` ("Pipeline:
|
|
1608
|
+
* Sales") adds a removable chip with that text to the Active line and
|
|
1609
|
+
* counts on the Filter badge while it is selected; removing the chip
|
|
1610
|
+
* goes back to the default tab.
|
|
1591
1611
|
*/
|
|
1592
1612
|
quickFilters: {
|
|
1593
1613
|
type: Array,
|
|
@@ -1958,6 +1978,22 @@ export default {
|
|
|
1958
1978
|
default: null,
|
|
1959
1979
|
},
|
|
1960
1980
|
|
|
1981
|
+
/**
|
|
1982
|
+
* Board look: the segments the view switch draws, e.g. `['table',
|
|
1983
|
+
* 'cards', 'board', 'map']` as the boards do. A listed mode the page
|
|
1984
|
+
* can open is an ordinary segment; one it cannot (no board columns, no
|
|
1985
|
+
* map configured) is drawn disabled, named "<mode>: not available on
|
|
1986
|
+
* this list". Fed from `pages[].config.viewSwitch`. Ignored without the
|
|
1987
|
+
* board look, and absent the switch is as before.
|
|
1988
|
+
*
|
|
1989
|
+
* @type {Array<'table' | 'cards' | 'board' | 'map'>}
|
|
1990
|
+
* @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
|
|
1991
|
+
*/
|
|
1992
|
+
viewSwitch: {
|
|
1993
|
+
type: Array,
|
|
1994
|
+
default: null,
|
|
1995
|
+
},
|
|
1996
|
+
|
|
1961
1997
|
/**
|
|
1962
1998
|
* The board's configuration, mirroring the manifest `config.board`
|
|
1963
1999
|
* block: `{ statusField, cardFields, swimlaneField }`. The board is
|
|
@@ -2644,6 +2680,35 @@ export default {
|
|
|
2644
2680
|
default: null,
|
|
2645
2681
|
},
|
|
2646
2682
|
|
|
2683
|
+
/**
|
|
2684
|
+
* KPI tiles above the list (manifest `config.statRow`): dashboard
|
|
2685
|
+
* widget definitions `{ id, type, title?, content }`, usually `stat`,
|
|
2686
|
+
* drawn in an auto-fit row (each at least 200px) in the listed order.
|
|
2687
|
+
* Types resolve like dashboard widgets: the app registry, then the
|
|
2688
|
+
* dashboard catalog. Empty by default.
|
|
2689
|
+
*
|
|
2690
|
+
* @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-stat-row-above-the-list
|
|
2691
|
+
* @type {Array<object>}
|
|
2692
|
+
*/
|
|
2693
|
+
statRow: {
|
|
2694
|
+
type: Array,
|
|
2695
|
+
default: () => [],
|
|
2696
|
+
},
|
|
2697
|
+
|
|
2698
|
+
/**
|
|
2699
|
+
* Cards beside the list (manifest `config.sidePanel`): dashboard
|
|
2700
|
+
* widget definitions `{ id, type, title?, content, headerLink? }`
|
|
2701
|
+
* drawn in a 300px column right of the toolbar and the list. Empty
|
|
2702
|
+
* by default; below 1024px the column moves above the list.
|
|
2703
|
+
*
|
|
2704
|
+
* @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-side-panel-beside-the-list
|
|
2705
|
+
* @type {Array<object>}
|
|
2706
|
+
*/
|
|
2707
|
+
sidePanel: {
|
|
2708
|
+
type: Array,
|
|
2709
|
+
default: () => [],
|
|
2710
|
+
},
|
|
2711
|
+
|
|
2647
2712
|
/**
|
|
2648
2713
|
* Where a row carries the actions the server says this caller may run
|
|
2649
2714
|
* on it: a dotted path, read off the rows the list already fetched, so
|
|
@@ -3323,6 +3388,16 @@ export default {
|
|
|
3323
3388
|
},
|
|
3324
3389
|
|
|
3325
3390
|
computed: {
|
|
3391
|
+
/**
|
|
3392
|
+
* Whether the page draws cards beside its list (`sidePanel`).
|
|
3393
|
+
*
|
|
3394
|
+
* @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-side-panel-beside-the-list
|
|
3395
|
+
* @return {boolean}
|
|
3396
|
+
*/
|
|
3397
|
+
hasSidePanel() {
|
|
3398
|
+
return Array.isArray(this.sidePanel) && this.sidePanel.length > 0
|
|
3399
|
+
},
|
|
3400
|
+
|
|
3326
3401
|
/**
|
|
3327
3402
|
* The empty-state copy run through the host translate function
|
|
3328
3403
|
* (the injected `cnTranslate`, identity by default). Only this
|
|
@@ -3695,6 +3770,21 @@ export default {
|
|
|
3695
3770
|
return Object.keys(this.mapConfig || {}).length > 0
|
|
3696
3771
|
},
|
|
3697
3772
|
|
|
3773
|
+
/**
|
|
3774
|
+
* Board look: the modes `viewSwitch` lists that this page cannot open,
|
|
3775
|
+
* drawn as disabled segments.
|
|
3776
|
+
*
|
|
3777
|
+
* @return {Array<string>} The disabled modes.
|
|
3778
|
+
* @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
|
|
3779
|
+
*/
|
|
3780
|
+
viewSwitchDisabledModes() {
|
|
3781
|
+
if (!this.isBoardLook || !Array.isArray(this.viewSwitch)) {
|
|
3782
|
+
return []
|
|
3783
|
+
}
|
|
3784
|
+
const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
|
|
3785
|
+
return this.viewSwitch.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
|
|
3786
|
+
},
|
|
3787
|
+
|
|
3698
3788
|
/**
|
|
3699
3789
|
* The base view-toggle segments (cards/table/list) passed to CnActionsBar.
|
|
3700
3790
|
* Beta's explicit `viewModes` whitelist wins when set; otherwise the
|
|
@@ -5264,11 +5354,11 @@ export default {
|
|
|
5264
5354
|
return []
|
|
5265
5355
|
}
|
|
5266
5356
|
const props = this.effectiveSchema?.properties || {}
|
|
5267
|
-
const chips = []
|
|
5357
|
+
const chips = [...this.quickFilterActiveChips]
|
|
5268
5358
|
for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
|
|
5269
5359
|
const values = (Array.isArray(raw) ? raw : [raw])
|
|
5270
5360
|
.filter((value) => value !== null && value !== undefined && value !== '')
|
|
5271
|
-
.map((value) =>
|
|
5361
|
+
.map((value) => this.activeFilterValueLabel(key, value))
|
|
5272
5362
|
if (values.length === 0) {
|
|
5273
5363
|
continue
|
|
5274
5364
|
}
|
|
@@ -5278,6 +5368,87 @@ export default {
|
|
|
5278
5368
|
return chips
|
|
5279
5369
|
},
|
|
5280
5370
|
|
|
5371
|
+
/**
|
|
5372
|
+
* The Active line's chips for the selected quick filters: one per
|
|
5373
|
+
* selected tab that declares an `activeLabel` ("Pipeline: Sales"), run
|
|
5374
|
+
* through the app's translate. A tab without one (an "All" tab) adds no
|
|
5375
|
+
* chip. The chip carries the tab's index so removing it can deselect it.
|
|
5376
|
+
*
|
|
5377
|
+
* @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-a-quick-filter-with-an-active-label-joins-the-active-line
|
|
5378
|
+
* @return {Array<{key: string, label: string, quickFilterIndex: number}>}
|
|
5379
|
+
*/
|
|
5380
|
+
quickFilterActiveChips() {
|
|
5381
|
+
const tabs = this.tabStripEntries || []
|
|
5382
|
+
if (tabs.length === 0) {
|
|
5383
|
+
return []
|
|
5384
|
+
}
|
|
5385
|
+
const indices = this.quickFilterMultiple
|
|
5386
|
+
? (this.selectedQuickFilterIndices || [])
|
|
5387
|
+
: [this.activeQuickFilterIndex]
|
|
5388
|
+
return indices
|
|
5389
|
+
.filter((index) => typeof index === 'number' && tabs[index] && typeof tabs[index].activeLabel === 'string' && tabs[index].activeLabel !== '')
|
|
5390
|
+
.map((index) => ({ key: `quick-filter:${index}`, label: this.cnTranslate(tabs[index].activeLabel), quickFilterIndex: index }))
|
|
5391
|
+
},
|
|
5392
|
+
|
|
5393
|
+
/**
|
|
5394
|
+
* Filter keys over a reference property that the Active line must name,
|
|
5395
|
+
* and that no reference column resolves already: one spec each, with the
|
|
5396
|
+
* referenced register and schema off the page schema's `$ref`.
|
|
5397
|
+
*
|
|
5398
|
+
* @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-an-active-chip-names-the-value-not-its-id
|
|
5399
|
+
* @return {Array<{key: string, labelField: string, register: string, schema: string}>}
|
|
5400
|
+
*/
|
|
5401
|
+
activeFilterRefSpecs() {
|
|
5402
|
+
if (!this.isBoardLook || !this.effectiveSchema) {
|
|
5403
|
+
return []
|
|
5404
|
+
}
|
|
5405
|
+
const keys = Object.keys(this.effectiveActiveFilters || {})
|
|
5406
|
+
if (keys.length === 0) {
|
|
5407
|
+
return []
|
|
5408
|
+
}
|
|
5409
|
+
const covered = new Set(this.refLabelSpecs.map((spec) => spec.key))
|
|
5410
|
+
const fields = fieldsFromSchema(this.effectiveSchema, { includeReadOnly: true })
|
|
5411
|
+
const specs = []
|
|
5412
|
+
for (const key of keys) {
|
|
5413
|
+
if (covered.has(key)) {
|
|
5414
|
+
continue
|
|
5415
|
+
}
|
|
5416
|
+
const field = fields.find((f) => f.key === key)
|
|
5417
|
+
const ref = field && field.reference
|
|
5418
|
+
if (!ref || ref.schema === undefined || ref.schema === null) {
|
|
5419
|
+
continue
|
|
5420
|
+
}
|
|
5421
|
+
specs.push({
|
|
5422
|
+
key,
|
|
5423
|
+
labelField: '',
|
|
5424
|
+
register: ref.register || (typeof this.register === 'string' ? this.register : ''),
|
|
5425
|
+
schema: String(ref.schema),
|
|
5426
|
+
})
|
|
5427
|
+
}
|
|
5428
|
+
return specs
|
|
5429
|
+
},
|
|
5430
|
+
|
|
5431
|
+
/**
|
|
5432
|
+
* The ids each active reference filter holds, per filter key, for the
|
|
5433
|
+
* label lookup of the Active line.
|
|
5434
|
+
*
|
|
5435
|
+
* @return {{[key: string]: string[]}} Distinct ids per filter key.
|
|
5436
|
+
*/
|
|
5437
|
+
activeFilterRefIds() {
|
|
5438
|
+
const out = {}
|
|
5439
|
+
const keys = new Set([...this.refLabelSpecs, ...this.activeFilterRefSpecs].map((spec) => spec.key))
|
|
5440
|
+
for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
|
|
5441
|
+
if (!keys.has(key)) {
|
|
5442
|
+
continue
|
|
5443
|
+
}
|
|
5444
|
+
out[key] = (Array.isArray(raw) ? raw : [raw])
|
|
5445
|
+
.filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')
|
|
5446
|
+
.map((id) => String(id))
|
|
5447
|
+
.sort()
|
|
5448
|
+
}
|
|
5449
|
+
return out
|
|
5450
|
+
},
|
|
5451
|
+
|
|
5281
5452
|
/**
|
|
5282
5453
|
* The plural the board bulk band names: "With the selected cases".
|
|
5283
5454
|
* The schema's `titlePlural` (or `plural`), else the lower-cased
|
|
@@ -5558,6 +5729,14 @@ export default {
|
|
|
5558
5729
|
},
|
|
5559
5730
|
},
|
|
5560
5731
|
|
|
5732
|
+
// The Active line names a referenced value, not its id.
|
|
5733
|
+
activeFilterRefIds: {
|
|
5734
|
+
deep: true,
|
|
5735
|
+
handler() {
|
|
5736
|
+
this.loadRefLabels()
|
|
5737
|
+
},
|
|
5738
|
+
},
|
|
5739
|
+
|
|
5561
5740
|
countRequestKey: {
|
|
5562
5741
|
immediate: true,
|
|
5563
5742
|
/** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views */
|
|
@@ -5826,7 +6005,7 @@ export default {
|
|
|
5826
6005
|
* @spec openspec/changes/index-ref-column-labels/tasks.md#task-2
|
|
5827
6006
|
*/
|
|
5828
6007
|
async loadRefLabels() {
|
|
5829
|
-
const specs = this.refLabelSpecs
|
|
6008
|
+
const specs = [...this.refLabelSpecs, ...this.activeFilterRefSpecs]
|
|
5830
6009
|
if (specs.length === 0) {
|
|
5831
6010
|
return
|
|
5832
6011
|
}
|
|
@@ -5841,7 +6020,8 @@ export default {
|
|
|
5841
6020
|
}
|
|
5842
6021
|
await Promise.all(specs.map(async (spec) => {
|
|
5843
6022
|
const known = this.refLabels[spec.key] || {}
|
|
5844
|
-
const
|
|
6023
|
+
const wanted = new Set([...(this.refLabelIds[spec.key] || []), ...(this.activeFilterRefIds[spec.key] || [])])
|
|
6024
|
+
const ids = [...wanted].filter((id) => !Object.hasOwn(known, id))
|
|
5845
6025
|
if (ids.length === 0) {
|
|
5846
6026
|
return
|
|
5847
6027
|
}
|
|
@@ -6525,6 +6705,11 @@ export default {
|
|
|
6525
6705
|
* @return {void}
|
|
6526
6706
|
*/
|
|
6527
6707
|
onClearFilters() {
|
|
6708
|
+
// "Clear all" on the Active line also undoes the quick filters it
|
|
6709
|
+
// listed (a tab without an `activeLabel` is not a filter there).
|
|
6710
|
+
for (const chip of this.quickFilterActiveChips) {
|
|
6711
|
+
this.deselectQuickFilterChip(chip.quickFilterIndex)
|
|
6712
|
+
}
|
|
6528
6713
|
// Clearing the view brings the person's own columns back.
|
|
6529
6714
|
this.appliedViewColumns = null
|
|
6530
6715
|
this.appliedSavedViewId = ''
|
|
@@ -6615,11 +6800,74 @@ export default {
|
|
|
6615
6800
|
* @spec openspec/changes/screens-index-list-parity/specs/index-list-board-look/spec.md#requirement-the-toolbar-sits-on-the-ground-in-two-rows
|
|
6616
6801
|
*/
|
|
6617
6802
|
onRemoveActiveFilter(chip) {
|
|
6803
|
+
if (chip && typeof chip.quickFilterIndex === 'number') {
|
|
6804
|
+
this.deselectQuickFilterChip(chip.quickFilterIndex)
|
|
6805
|
+
return
|
|
6806
|
+
}
|
|
6618
6807
|
if (chip && typeof chip.key === 'string') {
|
|
6619
6808
|
this.onFilterEvent({ key: chip.key, values: [] })
|
|
6620
6809
|
}
|
|
6621
6810
|
},
|
|
6622
6811
|
|
|
6812
|
+
/**
|
|
6813
|
+
* Undo a quick filter from the Active line. With several tabs selectable
|
|
6814
|
+
* the tab leaves the selection; with one, the page goes back to its
|
|
6815
|
+
* default tab when that tab names no active filter itself, and else to no
|
|
6816
|
+
* tab at all, so the chip does not come straight back.
|
|
6817
|
+
*
|
|
6818
|
+
* @param {number} index The tab index the chip stands for.
|
|
6819
|
+
* @return {void}
|
|
6820
|
+
* @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-a-quick-filter-with-an-active-label-joins-the-active-line
|
|
6821
|
+
*/
|
|
6822
|
+
deselectQuickFilterChip(index) {
|
|
6823
|
+
if (this.quickFilterMultiple) {
|
|
6824
|
+
this.onQuickFilterMultiChange((this.selectedQuickFilterIndices || []).filter((i) => i !== index))
|
|
6825
|
+
return
|
|
6826
|
+
}
|
|
6827
|
+
if (this.activeQuickFilterIndex !== index) {
|
|
6828
|
+
return
|
|
6829
|
+
}
|
|
6830
|
+
const tabs = this.tabStripEntries || []
|
|
6831
|
+
let fallback = tabs.findIndex((tab) => tab && tab.default === true)
|
|
6832
|
+
if (fallback < 0 && tabs.length > 0) {
|
|
6833
|
+
fallback = 0
|
|
6834
|
+
}
|
|
6835
|
+
const fallbackNamed = fallback >= 0 && typeof tabs[fallback].activeLabel === 'string' && tabs[fallback].activeLabel !== ''
|
|
6836
|
+
this.onQuickFilterChange(fallback >= 0 && fallback !== index && !fallbackNamed ? fallback : null)
|
|
6837
|
+
},
|
|
6838
|
+
|
|
6839
|
+
/**
|
|
6840
|
+
* The text an Active chip shows for one filter value: the referenced
|
|
6841
|
+
* object's label, the facet bucket's label, the schema's `oneOf` title,
|
|
6842
|
+
* else the value itself.
|
|
6843
|
+
*
|
|
6844
|
+
* @param {string} key The filter key.
|
|
6845
|
+
* @param {unknown} value One value of that filter.
|
|
6846
|
+
* @return {string} The label.
|
|
6847
|
+
* @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-an-active-chip-names-the-value-not-its-id
|
|
6848
|
+
*/
|
|
6849
|
+
activeFilterValueLabel(key, value) {
|
|
6850
|
+
const id = String(value)
|
|
6851
|
+
const refs = this.refLabels[key]
|
|
6852
|
+
if (refs && typeof refs[id] === 'string' && refs[id] !== '') {
|
|
6853
|
+
return refs[id]
|
|
6854
|
+
}
|
|
6855
|
+
const facet = this.effectiveFacetData && this.effectiveFacetData[key]
|
|
6856
|
+
const bucket = facet && Array.isArray(facet.values)
|
|
6857
|
+
? facet.values.find((fv) => fv && typeof fv === 'object' && String(fv.value) === id)
|
|
6858
|
+
: null
|
|
6859
|
+
if (bucket && typeof bucket.label === 'string' && bucket.label !== '') {
|
|
6860
|
+
return this.cnTranslate(bucket.label)
|
|
6861
|
+
}
|
|
6862
|
+
const prop = this.effectiveSchema?.properties?.[key]
|
|
6863
|
+
const options = prop && Array.isArray(prop.oneOf) ? prop.oneOf : []
|
|
6864
|
+
const option = options.find((o) => o && String(o.const) === id && typeof o.title === 'string')
|
|
6865
|
+
if (option) {
|
|
6866
|
+
return this.cnTranslate(option.title)
|
|
6867
|
+
}
|
|
6868
|
+
return id
|
|
6869
|
+
},
|
|
6870
|
+
|
|
6623
6871
|
/**
|
|
6624
6872
|
* A header filter was applied or cleared. One column can own several
|
|
6625
6873
|
* query parameters (a range is `key[gte]` and `key[lte]`), so they are
|