@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
|
@@ -80,10 +80,64 @@
|
|
|
80
80
|
</span>
|
|
81
81
|
</template>
|
|
82
82
|
|
|
83
|
+
<!-- A stack (`widgetIds`): every widget is a titled card; the
|
|
84
|
+
first joins the open tab, the next ones stand below it. -->
|
|
85
|
+
<div
|
|
86
|
+
v-if="entry.widget && entry.stack.length"
|
|
87
|
+
class="cn-tabs-widget__stack"
|
|
88
|
+
data-testid="cn-tabs-widget-stack">
|
|
89
|
+
<div class="cn-tabs-widget__stack-first">
|
|
90
|
+
<CnDetailWidgetHost
|
|
91
|
+
:widget="entry.widget"
|
|
92
|
+
chrome="card"
|
|
93
|
+
:showCardTitle="true"
|
|
94
|
+
:showActions="false"
|
|
95
|
+
:availableWidgets="availableWidgets"
|
|
96
|
+
:objectId="objectId"
|
|
97
|
+
:object="objectData"
|
|
98
|
+
:objectType="objectType"
|
|
99
|
+
:schemaObject="schemaObject"
|
|
100
|
+
:register="register"
|
|
101
|
+
:schema="schema"
|
|
102
|
+
:store="store"
|
|
103
|
+
:surface="surface"
|
|
104
|
+
:integrationContext="integrationContext"
|
|
105
|
+
:cnRegistry="cnRegistry"
|
|
106
|
+
@geoSaved="onGeoSaved"
|
|
107
|
+
@openIntegration="onOpenIntegration"
|
|
108
|
+
@selectObject="onSelectObject" />
|
|
109
|
+
</div>
|
|
110
|
+
<div
|
|
111
|
+
v-for="child in entry.stack"
|
|
112
|
+
:key="`${entry.key}-${child.id}`"
|
|
113
|
+
class="cn-tabs-widget__stack-item"
|
|
114
|
+
:data-testid="`cn-tabs-widget-stack-${child.id}`">
|
|
115
|
+
<CnDetailWidgetHost
|
|
116
|
+
:widget="child"
|
|
117
|
+
chrome="card"
|
|
118
|
+
:showCardTitle="true"
|
|
119
|
+
:showActions="false"
|
|
120
|
+
:availableWidgets="availableWidgets"
|
|
121
|
+
:objectId="objectId"
|
|
122
|
+
:object="objectData"
|
|
123
|
+
:objectType="objectType"
|
|
124
|
+
:schemaObject="schemaObject"
|
|
125
|
+
:register="register"
|
|
126
|
+
:schema="schema"
|
|
127
|
+
:store="store"
|
|
128
|
+
:surface="surface"
|
|
129
|
+
:integrationContext="integrationContext"
|
|
130
|
+
:cnRegistry="cnRegistry"
|
|
131
|
+
@geoSaved="onGeoSaved"
|
|
132
|
+
@openIntegration="onOpenIntegration"
|
|
133
|
+
@selectObject="onSelectObject" />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
83
136
|
<CnDetailWidgetHost
|
|
84
|
-
v-if="entry.widget"
|
|
137
|
+
v-else-if="entry.widget"
|
|
85
138
|
:widget="entry.widget"
|
|
86
139
|
chrome="bare"
|
|
140
|
+
:availableWidgets="availableWidgets"
|
|
87
141
|
:objectId="objectId"
|
|
88
142
|
:object="objectData"
|
|
89
143
|
:objectType="objectType"
|
|
@@ -114,6 +168,7 @@ import CnTab from '../CnTabs/CnTab.vue'
|
|
|
114
168
|
import CnTabs from '../CnTabs/CnTabs.vue'
|
|
115
169
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
116
170
|
import { resolveTabCount } from '../../utils/detailActionModel.js'
|
|
171
|
+
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
117
172
|
import { isActivityWidget } from '../../utils/headerMeta.js'
|
|
118
173
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
119
174
|
import { evaluateVisibleWhen, evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
|
|
@@ -268,7 +323,11 @@ export default {
|
|
|
268
323
|
* `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.
|
|
269
324
|
* `label` and `icon` fall back to the referenced widget's own title and
|
|
270
325
|
* icon. `count` is a number shown after the label; `countField` reads
|
|
271
|
-
* it off the record instead (a list counts its items). `
|
|
326
|
+
* it off the record instead (a list counts its items). `countFrom`
|
|
327
|
+
* counts a list on the server: `"widget"` counts the child widget's
|
|
328
|
+
* own list (`content.register`, `schema`, `filter`, tokens such as
|
|
329
|
+
* `@objectId` resolved), an object `{ register, schema, filter? }`
|
|
330
|
+
* names the list; `count` and `countField` win over it. `overflow: true`
|
|
272
331
|
* lists the tab under "More".
|
|
273
332
|
*
|
|
274
333
|
* `maxVisibleTabs` caps the strip: later tabs go under "More".
|
|
@@ -276,7 +335,7 @@ export default {
|
|
|
276
335
|
* `moreLabel` names that menu. `variant: "segmented"` draws the strip
|
|
277
336
|
* as a pill switch (CnTabs' segmented variant); `line` is the default.
|
|
278
337
|
*
|
|
279
|
-
* @type {{ tabs?: Array<{widgetId
|
|
338
|
+
* @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}), overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
|
|
280
339
|
*/
|
|
281
340
|
content: {
|
|
282
341
|
type: Object,
|
|
@@ -396,6 +455,9 @@ export default {
|
|
|
396
455
|
sourceVisibility: {},
|
|
397
456
|
// Bumped per evaluation round, so a slow answer from an earlier round is dropped.
|
|
398
457
|
visibilitySeq: 0,
|
|
458
|
+
// Totals counted for tabs with `countFrom`, by position in `content.tabs`.
|
|
459
|
+
widgetCounts: {},
|
|
460
|
+
countSeq: 0,
|
|
399
461
|
// Tab id named by the route hash that has not appeared yet (still pending).
|
|
400
462
|
hashTargetId: '',
|
|
401
463
|
// Items published by the panels, keyed by widget id. Keyed rather
|
|
@@ -438,7 +500,7 @@ export default {
|
|
|
438
500
|
// whatever its place in the manifest. `index` stays the position in
|
|
439
501
|
// `content.tabs`, which the source-mode conditions are keyed on.
|
|
440
502
|
const isActivityTab = (tab) => {
|
|
441
|
-
const id =
|
|
503
|
+
const id = this.tabWidgetIds(tab)[0]
|
|
442
504
|
return isActivityWidget(this.availableWidgets.find((w) => w && w.id === id))
|
|
443
505
|
}
|
|
444
506
|
const ordered = authored.map((tab, index) => ({ tab, index }))
|
|
@@ -460,9 +522,19 @@ export default {
|
|
|
460
522
|
if (visibility === 'hidden') {
|
|
461
523
|
return
|
|
462
524
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
525
|
+
// A stack (`widgetIds`) resolves every id; one that names no
|
|
526
|
+
// widget is skipped. The first that resolves is the panel's.
|
|
527
|
+
const ids = this.tabWidgetIds(tab)
|
|
528
|
+
const widgets = ids.length > 1
|
|
529
|
+
? ids.map((id) => this.availableWidgets.find((w) => w && w.id === id) || null).filter(Boolean)
|
|
530
|
+
: []
|
|
531
|
+
const widget = ids.length > 1
|
|
532
|
+
? (widgets[0] || null)
|
|
533
|
+
: (this.availableWidgets.find((w) => w && w.id === ids[0]) || null)
|
|
534
|
+
const widgetId = widget ? widget.id : ids[0]
|
|
535
|
+
const declared = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)
|
|
536
|
+
const counted = this.widgetCounts[index]
|
|
537
|
+
const count = declared === null && typeof counted === 'number' ? counted : declared
|
|
466
538
|
// The board strip has no "More": every tab is in the strip.
|
|
467
539
|
let overflow = !this.isBoard && ((tab && tab.overflow === true) || (hideEmpty && count === 0))
|
|
468
540
|
if (!overflow && !this.isBoard) {
|
|
@@ -479,6 +551,7 @@ export default {
|
|
|
479
551
|
label: this.tabLabel(tab, widget, widgetId),
|
|
480
552
|
icon: (tab && tab.icon) || widget?.icon || '',
|
|
481
553
|
widget,
|
|
554
|
+
stack: widgets.slice(1),
|
|
482
555
|
count,
|
|
483
556
|
overflow,
|
|
484
557
|
pending: visibility === 'pending',
|
|
@@ -497,6 +570,17 @@ export default {
|
|
|
497
570
|
return t('nextcloud-vue', 'Loading …')
|
|
498
571
|
},
|
|
499
572
|
|
|
573
|
+
/**
|
|
574
|
+
* The lists the tabs count (`countFrom`), serialised, to count again
|
|
575
|
+
* when they change.
|
|
576
|
+
*
|
|
577
|
+
* @return {string} The key.
|
|
578
|
+
*/
|
|
579
|
+
countSourcesKey() {
|
|
580
|
+
const tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []
|
|
581
|
+
return JSON.stringify(tabs.map((tab) => this.countSourceOf(tab)))
|
|
582
|
+
},
|
|
583
|
+
|
|
500
584
|
/** @return {string} The source-mode conditions, serialised, to re-evaluate when they change. */
|
|
501
585
|
sourceConditionsKey() {
|
|
502
586
|
const tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []
|
|
@@ -609,6 +693,12 @@ export default {
|
|
|
609
693
|
objectId() {
|
|
610
694
|
this.sourceVisibility = {}
|
|
611
695
|
this.refreshSourceVisibility()
|
|
696
|
+
this.widgetCounts = {}
|
|
697
|
+
this.refreshWidgetCounts()
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
countSourcesKey() {
|
|
701
|
+
this.refreshWidgetCounts()
|
|
612
702
|
},
|
|
613
703
|
|
|
614
704
|
sourceConditionsKey() {
|
|
@@ -643,10 +733,14 @@ export default {
|
|
|
643
733
|
}
|
|
644
734
|
this.activeKey = this.resolvedTabs[this.activeIndex]?.key || ''
|
|
645
735
|
this.refreshSourceVisibility()
|
|
736
|
+
this.refreshWidgetCounts()
|
|
646
737
|
},
|
|
647
738
|
|
|
648
739
|
mounted() {
|
|
649
|
-
this._onRefresh = () =>
|
|
740
|
+
this._onRefresh = () => {
|
|
741
|
+
this.refreshSourceVisibility()
|
|
742
|
+
this.refreshWidgetCounts()
|
|
743
|
+
}
|
|
650
744
|
subscribe('cn:page:refresh', this._onRefresh)
|
|
651
745
|
subscribe('cn:widget:refresh', this._onRefresh)
|
|
652
746
|
},
|
|
@@ -657,6 +751,27 @@ export default {
|
|
|
657
751
|
},
|
|
658
752
|
|
|
659
753
|
methods: {
|
|
754
|
+
/**
|
|
755
|
+
* The widget ids a tab names: `widgetIds` (a stack) when it holds at
|
|
756
|
+
* least one id, else `widgetId`, or the bare string.
|
|
757
|
+
*
|
|
758
|
+
* @param {object|string} tab The `content.tabs[]` entry.
|
|
759
|
+
* @return {string[]} The ids, at least one entry (possibly undefined).
|
|
760
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-tab-can-hold-a-stack-of-widgets
|
|
761
|
+
*/
|
|
762
|
+
tabWidgetIds(tab) {
|
|
763
|
+
if (typeof tab === 'string') {
|
|
764
|
+
return [tab]
|
|
765
|
+
}
|
|
766
|
+
if (tab && Array.isArray(tab.widgetIds)) {
|
|
767
|
+
const ids = tab.widgetIds.filter((id) => typeof id === 'string' && id !== '')
|
|
768
|
+
if (ids.length) {
|
|
769
|
+
return ids
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return [tab?.widgetId]
|
|
773
|
+
},
|
|
774
|
+
|
|
660
775
|
/**
|
|
661
776
|
* The label a tab shows: the authored `label`, else (board look) History
|
|
662
777
|
* for the activity widget, else the child widget's title, else its id.
|
|
@@ -733,6 +848,60 @@ export default {
|
|
|
733
848
|
}))
|
|
734
849
|
},
|
|
735
850
|
|
|
851
|
+
/**
|
|
852
|
+
* The list a tab counts (`countFrom`): `"widget"` counts the child
|
|
853
|
+
* widget's own list (its `content.register`, `content.schema` and
|
|
854
|
+
* `content.filter`); an object `{ register, schema, filter? }` names
|
|
855
|
+
* the list itself. Null for a tab that does not ask, or a child with
|
|
856
|
+
* no list.
|
|
857
|
+
*
|
|
858
|
+
* @param {object|string} tab The `content.tabs[]` entry.
|
|
859
|
+
* @return {{register: string, schema: string, filter: object}|null} The list.
|
|
860
|
+
* @spec openspec/changes/screens-tab-counts-parity/specs/tab-counts/spec.md#requirement-a-tab-can-count-its-list
|
|
861
|
+
*/
|
|
862
|
+
countSourceOf(tab) {
|
|
863
|
+
const from = tab && typeof tab === 'object' ? tab.countFrom : null
|
|
864
|
+
let source = null
|
|
865
|
+
if (from === 'widget') {
|
|
866
|
+
// A stack counts its first widget that has a list.
|
|
867
|
+
const lists = this.tabWidgetIds(tab)
|
|
868
|
+
.map((id) => this.availableWidgets.find((w) => w && w.id === id))
|
|
869
|
+
.filter((w) => w && w.content && typeof w.content === 'object' && w.content.register && w.content.schema)
|
|
870
|
+
source = lists.length ? lists[0].content : null
|
|
871
|
+
} else if (from && typeof from === 'object') {
|
|
872
|
+
source = from
|
|
873
|
+
}
|
|
874
|
+
if (!source || typeof source.register !== 'string' || source.register === '' || typeof source.schema !== 'string' || source.schema === '') {
|
|
875
|
+
return null
|
|
876
|
+
}
|
|
877
|
+
const filter = source.filter && typeof source.filter === 'object' && !Array.isArray(source.filter) ? source.filter : {}
|
|
878
|
+
return { register: source.register, schema: source.schema, filter }
|
|
879
|
+
},
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Count the list behind every tab with `countFrom`: once on mount,
|
|
883
|
+
* when the record changes and after a refresh. A tab keeps its last
|
|
884
|
+
* total while the new one is on its way; a failed count shows none.
|
|
885
|
+
*
|
|
886
|
+
* @return {Promise<void>}
|
|
887
|
+
* @spec openspec/changes/screens-tab-counts-parity/specs/tab-counts/spec.md#requirement-a-tab-can-count-its-list
|
|
888
|
+
*/
|
|
889
|
+
async refreshWidgetCounts() {
|
|
890
|
+
const tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []
|
|
891
|
+
const seq = ++this.countSeq
|
|
892
|
+
const ctx = { objectId: this.objectId, object: this.objectData || undefined }
|
|
893
|
+
await Promise.all(tabs.map(async (tab, index) => {
|
|
894
|
+
const source = this.countSourceOf(tab)
|
|
895
|
+
if (!source) {
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
const total = await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null)
|
|
899
|
+
if (seq === this.countSeq) {
|
|
900
|
+
this.widgetCounts = { ...this.widgetCounts, [index]: total }
|
|
901
|
+
}
|
|
902
|
+
}))
|
|
903
|
+
},
|
|
904
|
+
|
|
736
905
|
/** @return {string} The route hash without its `#`, or ''. */
|
|
737
906
|
currentHash() {
|
|
738
907
|
const fromRoute = this.$route && typeof this.$route.hash === 'string' ? this.$route.hash : ''
|
|
@@ -823,6 +992,13 @@ export default {
|
|
|
823
992
|
</script>
|
|
824
993
|
|
|
825
994
|
<style scoped>
|
|
995
|
+
/* A stack (`widgetIds`): the panel's widget, then cards 20px apart. */
|
|
996
|
+
.cn-tabs-widget__stack {
|
|
997
|
+
display: flex;
|
|
998
|
+
flex-direction: column;
|
|
999
|
+
gap: 20px;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
826
1002
|
/* The panel is the card; the strip is not inside it. There is no title bar
|
|
827
1003
|
above the tabs either: the open tab names the panel, so a title row would
|
|
828
1004
|
say the same thing twice and cost a row of height on a card that is mostly
|
|
@@ -72,6 +72,10 @@ import '../CnDeltaWidget/index.js'
|
|
|
72
72
|
import '../CnGaugeWidget/index.js'
|
|
73
73
|
import '../CnFlowRunsWidget/index.js'
|
|
74
74
|
import '../CnTasksWidget/index.js'
|
|
75
|
+
import '../CnSectionsWidget/index.js'
|
|
76
|
+
import '../CnFactsWidget/index.js'
|
|
77
|
+
import '../CnFieldListWidget/index.js'
|
|
78
|
+
import '../CnRecordListWidget/index.js'
|
|
75
79
|
import '../CnNotepadWidget/index.js'
|
|
76
80
|
import '../CnObjectDataWidget/dashboardRegistration.js'
|
|
77
81
|
import '../CnObjectGeoWidget/dashboardRegistration.js'
|
|
@@ -4,12 +4,43 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div
|
|
7
|
+
<div
|
|
8
|
+
class="cn-workspace-filter-widget"
|
|
9
|
+
:class="{ 'cn-workspace-filter-widget--start': isStart }"
|
|
10
|
+
data-testid="cn-workspace-filter-widget">
|
|
11
|
+
<!-- The start card (`layout: "start"`): a title and one line of
|
|
12
|
+
guidance beside the control, as the "Contact starten" card. -->
|
|
13
|
+
<div v-if="isStart && (title || description)" class="cn-workspace-filter-widget__text">
|
|
14
|
+
<h3 v-if="title" class="cn-workspace-filter-widget__title">
|
|
15
|
+
{{ title }}
|
|
16
|
+
</h3>
|
|
17
|
+
<p v-if="description" class="cn-workspace-filter-widget__description">
|
|
18
|
+
{{ description }}
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
8
22
|
<p v-if="label" class="cn-workspace-filter-widget__label">
|
|
9
23
|
{{ label }}
|
|
10
24
|
</p>
|
|
11
25
|
|
|
12
|
-
|
|
26
|
+
<!-- An object picker (`objectSource`): searches OpenRegister on the
|
|
27
|
+
server, so it reaches every object, not a fetched page. -->
|
|
28
|
+
<CnResourceSelect
|
|
29
|
+
v-if="objectSource"
|
|
30
|
+
class="cn-workspace-filter-widget__control"
|
|
31
|
+
:register="objectSource.register"
|
|
32
|
+
:schema="objectSource.schema"
|
|
33
|
+
:labelField="objectSource.labelField || 'title'"
|
|
34
|
+
:modelValue="selected === undefined || selected === null ? '' : String(selected)"
|
|
35
|
+
:placeholder="tr(objectSource.placeholder || '')"
|
|
36
|
+
:ariaLabelCombobox="tr(objectSource.ariaLabel || '') || title || label || writeKey"
|
|
37
|
+
:preload="objectSource.preload !== false"
|
|
38
|
+
:clearable="Boolean(content.allLabel)"
|
|
39
|
+
:allowCreate="false"
|
|
40
|
+
data-testid="cn-workspace-filter-object"
|
|
41
|
+
@update:modelValue="applySelection($event || '')" />
|
|
42
|
+
|
|
43
|
+
<div v-else-if="loading" class="cn-workspace-filter-widget__status">
|
|
13
44
|
{{ loadingLabel }}
|
|
14
45
|
</div>
|
|
15
46
|
|
|
@@ -47,12 +78,23 @@
|
|
|
47
78
|
</NcCheckboxRadioSwitch>
|
|
48
79
|
</li>
|
|
49
80
|
</ul>
|
|
81
|
+
|
|
82
|
+
<component
|
|
83
|
+
:is="startLink.to ? 'router-link' : 'a'"
|
|
84
|
+
v-if="startLink"
|
|
85
|
+
:to="startLink.to || undefined"
|
|
86
|
+
:href="startLink.href || undefined"
|
|
87
|
+
class="cn-workspace-filter-widget__link"
|
|
88
|
+
data-testid="cn-workspace-filter-link">
|
|
89
|
+
{{ startLink.label }}
|
|
90
|
+
</component>
|
|
50
91
|
</div>
|
|
51
92
|
</template>
|
|
52
93
|
|
|
53
94
|
<script>
|
|
54
95
|
import { translate as t } from '@nextcloud/l10n'
|
|
55
96
|
import { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'
|
|
97
|
+
import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
|
|
56
98
|
import { fetchEndpointSource } from '../../composables/useEndpointSource.js'
|
|
57
99
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
58
100
|
|
|
@@ -94,7 +136,7 @@ import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
|
94
136
|
export default {
|
|
95
137
|
name: 'CnWorkspaceFilterWidget',
|
|
96
138
|
|
|
97
|
-
components: { NcSelect, NcCheckboxRadioSwitch },
|
|
139
|
+
components: { CnResourceSelect, NcSelect, NcCheckboxRadioSwitch },
|
|
98
140
|
|
|
99
141
|
inject: {
|
|
100
142
|
/**
|
|
@@ -104,13 +146,23 @@ export default {
|
|
|
104
146
|
* Null on pages without one (the widget then just emits `@change`).
|
|
105
147
|
*/
|
|
106
148
|
cnWorkspaceContext: { default: null },
|
|
149
|
+
/** Host translate function, for the start card's texts. */
|
|
150
|
+
cnTranslate: { default: () => (key) => key },
|
|
107
151
|
},
|
|
108
152
|
|
|
109
153
|
props: {
|
|
110
154
|
/**
|
|
111
155
|
* Persisted configuration blob.
|
|
112
156
|
*
|
|
113
|
-
*
|
|
157
|
+
* Start card keys (abstract-start-card): `layout: "start"` draws
|
|
158
|
+
* `title` and `description` beside the control, and `link`
|
|
159
|
+
* (`{ label, route? | href? }`) after it. `objectSource`
|
|
160
|
+
* (`{ register, schema, labelField?, placeholder?, ariaLabel?, preload? }`)
|
|
161
|
+
* makes the control an OpenRegister object picker that searches on the
|
|
162
|
+
* server and writes the picked object's id; without a selection and
|
|
163
|
+
* without `allLabel` it selects the first object.
|
|
164
|
+
*
|
|
165
|
+
* @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}
|
|
114
166
|
*/
|
|
115
167
|
content: {
|
|
116
168
|
type: Object,
|
|
@@ -137,6 +189,50 @@ export default {
|
|
|
137
189
|
return this.content.label || ''
|
|
138
190
|
},
|
|
139
191
|
|
|
192
|
+
/** Whether the widget draws the start card. */
|
|
193
|
+
isStart() {
|
|
194
|
+
return this.content.layout === 'start'
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** The start card's title, translated. */
|
|
198
|
+
title() {
|
|
199
|
+
return this.tr(this.content.title || '')
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/** The start card's guidance line, translated. */
|
|
203
|
+
description() {
|
|
204
|
+
return this.tr(this.content.description || '')
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The link after the control, or null.
|
|
209
|
+
*
|
|
210
|
+
* @return {{label: string, to?: object, href?: string}|null}
|
|
211
|
+
*/
|
|
212
|
+
startLink() {
|
|
213
|
+
const link = this.content.link
|
|
214
|
+
if (!link || typeof link !== 'object' || !link.label) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
if (link.route && typeof link.route === 'object' && link.route.name) {
|
|
218
|
+
return { label: this.tr(link.label), to: link.route }
|
|
219
|
+
}
|
|
220
|
+
if (typeof link.href === 'string' && link.href !== '') {
|
|
221
|
+
return { label: this.tr(link.label), href: link.href }
|
|
222
|
+
}
|
|
223
|
+
return null
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The object picker declaration, or null.
|
|
228
|
+
*
|
|
229
|
+
* @return {{register: string, schema: string, labelField?: string}|null}
|
|
230
|
+
*/
|
|
231
|
+
objectSource() {
|
|
232
|
+
const src = this.content.objectSource
|
|
233
|
+
return src && typeof src === 'object' && src.register && src.schema ? src : null
|
|
234
|
+
},
|
|
235
|
+
|
|
140
236
|
/** Choice style: 'select' or 'radio' (default). */
|
|
141
237
|
style() {
|
|
142
238
|
return this.content.style === 'select' ? 'select' : 'radio'
|
|
@@ -263,6 +359,10 @@ export default {
|
|
|
263
359
|
* @return {Promise<void>}
|
|
264
360
|
*/
|
|
265
361
|
async loadOptions() {
|
|
362
|
+
if (this.objectSource) {
|
|
363
|
+
await this.seedFirstObject()
|
|
364
|
+
return
|
|
365
|
+
}
|
|
266
366
|
if (this.content.endpointSource && this.content.endpointSource.url) {
|
|
267
367
|
await this.loadFromEndpoint()
|
|
268
368
|
} else if (this.content.source && this.content.source.schema) {
|
|
@@ -281,6 +381,51 @@ export default {
|
|
|
281
381
|
}
|
|
282
382
|
},
|
|
283
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Translate a manifest string through the host app.
|
|
386
|
+
*
|
|
387
|
+
* @param {string} text The text.
|
|
388
|
+
* @return {string}
|
|
389
|
+
*/
|
|
390
|
+
tr(text) {
|
|
391
|
+
if (typeof text !== 'string' || text === '') {
|
|
392
|
+
return ''
|
|
393
|
+
}
|
|
394
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The object picker's default: with nothing selected, no `default` and
|
|
399
|
+
* no `allLabel`, select the first object the source lists, so the page
|
|
400
|
+
* always has one in focus. Fail-safe: a failed read selects nothing.
|
|
401
|
+
*
|
|
402
|
+
* @return {Promise<void>}
|
|
403
|
+
*/
|
|
404
|
+
async seedFirstObject() {
|
|
405
|
+
if ((this.selected !== undefined && this.selected !== '') || this.content.allLabel || this.content.default !== undefined) {
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
const src = this.objectSource
|
|
409
|
+
try {
|
|
410
|
+
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
411
|
+
import('@nextcloud/axios'),
|
|
412
|
+
import('@nextcloud/router'),
|
|
413
|
+
])
|
|
414
|
+
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: src.register, schema: src.schema })
|
|
415
|
+
const res = await axios.get(url, { params: { _limit: 1 } })
|
|
416
|
+
const data = res && res.data
|
|
417
|
+
const rows = Array.isArray(data) ? data : ((data && data.results) || [])
|
|
418
|
+
const first = rows[0]
|
|
419
|
+
const id = first && (first.id || (first['@self'] && first['@self'].id))
|
|
420
|
+
if (id && (this.selected === undefined || this.selected === '') && this.readWorkspace() === undefined) {
|
|
421
|
+
this.selected = String(id)
|
|
422
|
+
this.writeWorkspace(String(id))
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
// Nothing selected: the siblings wait for a pick.
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
|
|
284
429
|
/**
|
|
285
430
|
* Fetch options from an `endpointSource` (array of options at
|
|
286
431
|
* responsePath). Fail-safe: a failed fetch leaves an empty list.
|
|
@@ -465,6 +610,45 @@ export default {
|
|
|
465
610
|
gap: 2px;
|
|
466
611
|
}
|
|
467
612
|
|
|
613
|
+
/* The start card: text left, control (and link) right, as PqDashboard's "Contact starten". */
|
|
614
|
+
.cn-workspace-filter-widget--start {
|
|
615
|
+
flex-direction: row;
|
|
616
|
+
flex-wrap: wrap;
|
|
617
|
+
align-items: center;
|
|
618
|
+
gap: 16px 24px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.cn-workspace-filter-widget__text {
|
|
622
|
+
flex: 1 1 320px;
|
|
623
|
+
min-width: 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.cn-workspace-filter-widget__title {
|
|
627
|
+
margin: 0 0 4px;
|
|
628
|
+
font-size: 17px;
|
|
629
|
+
font-weight: 700;
|
|
630
|
+
color: var(--color-main-text);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.cn-workspace-filter-widget__description {
|
|
634
|
+
margin: 0;
|
|
635
|
+
max-width: 420px;
|
|
636
|
+
color: var(--color-text-maxcontrast);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.cn-workspace-filter-widget--start .cn-workspace-filter-widget__control,
|
|
640
|
+
.cn-workspace-filter-widget--start > .v-select {
|
|
641
|
+
flex: 0 1 480px;
|
|
642
|
+
min-width: 240px;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.cn-workspace-filter-widget__link {
|
|
646
|
+
font-weight: 600;
|
|
647
|
+
color: var(--color-primary-element);
|
|
648
|
+
text-decoration: underline;
|
|
649
|
+
white-space: nowrap;
|
|
650
|
+
}
|
|
651
|
+
|
|
468
652
|
.cn-workspace-filter-widget__count {
|
|
469
653
|
margin-inline-start: 6px;
|
|
470
654
|
padding: 0 6px;
|
package/src/components/index.js
CHANGED
|
@@ -216,6 +216,10 @@ export { CnFlowStepOutcomes } from './CnFlowStepOutcomes/index.js'
|
|
|
216
216
|
export { CnFlowRunsWidgetForm } from './CnFlowRunsWidgetForm/index.js'
|
|
217
217
|
export { CnTasksWidget } from './CnTasksWidget/index.js'
|
|
218
218
|
export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
|
|
219
|
+
export { CnSectionsWidget } from './CnSectionsWidget/index.js'
|
|
220
|
+
export { CnFactsWidget } from './CnFactsWidget/index.js'
|
|
221
|
+
export { CnFieldListWidget } from './CnFieldListWidget/index.js'
|
|
222
|
+
export { CnRecordListWidget } from './CnRecordListWidget/index.js'
|
|
219
223
|
export { CnFilePreview } from './CnFilePreview/index.js'
|
|
220
224
|
export { CnTaskFormDialog } from './CnTaskFormDialog/index.js'
|
|
221
225
|
export { CnChildRecordsField } from './CnChildRecordsField/index.js'
|
|
@@ -17,11 +17,21 @@
|
|
|
17
17
|
* one, and it would be this library's fault in somebody else's app.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
jest.mock('@nextcloud/auth', () => ({
|
|
21
|
+
...jest.requireActual('@nextcloud/auth'),
|
|
22
|
+
getCurrentUser: jest.fn(() => null),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
import { getCurrentUser } from '@nextcloud/auth'
|
|
20
26
|
import {
|
|
27
|
+
clearAnonymousDrafts,
|
|
21
28
|
clearDraft,
|
|
22
29
|
DRAFT_MAX_AGE_MS,
|
|
23
30
|
draftKey,
|
|
31
|
+
hasDraftChanges,
|
|
24
32
|
readDraft,
|
|
33
|
+
resolveDraftUserId,
|
|
34
|
+
sameDraftValue,
|
|
25
35
|
writeDraft,
|
|
26
36
|
} from '../useFormDraft.js'
|
|
27
37
|
|
|
@@ -170,3 +180,62 @@ describe('when storage is not available', () => {
|
|
|
170
180
|
expect(() => clearDraft('cn-form-draft:a:s:new:u')).not.toThrow()
|
|
171
181
|
})
|
|
172
182
|
})
|
|
183
|
+
|
|
184
|
+
// @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
|
|
185
|
+
describe('resolveDraftUserId', () => {
|
|
186
|
+
afterEach(() => getCurrentUser.mockReturnValue(null))
|
|
187
|
+
|
|
188
|
+
it('prefers the id the host passed', () => {
|
|
189
|
+
getCurrentUser.mockReturnValue({ uid: 'cluade' })
|
|
190
|
+
expect(resolveDraftUserId('anne')).toBe('anne')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('falls back to the signed-in user', () => {
|
|
194
|
+
getCurrentUser.mockReturnValue({ uid: 'cluade' })
|
|
195
|
+
expect(resolveDraftUserId('')).toBe('cluade')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('is empty when nobody is signed in, and when the lookup throws', () => {
|
|
199
|
+
expect(resolveDraftUserId()).toBe('')
|
|
200
|
+
getCurrentUser.mockImplementationOnce(() => {
|
|
201
|
+
throw new Error('no head')
|
|
202
|
+
})
|
|
203
|
+
expect(resolveDraftUserId()).toBe('')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('clearAnonymousDrafts', () => {
|
|
208
|
+
beforeEach(() => window.localStorage.clear())
|
|
209
|
+
|
|
210
|
+
it('removes only drafts stored under anonymous', () => {
|
|
211
|
+
const anon = draftKey({ appId: 'pipelinq', schema: 'crmTask', objectId: 't1' })
|
|
212
|
+
const anne = draftKey({ appId: 'pipelinq', schema: 'crmTask', objectId: 't1', userId: 'anne' })
|
|
213
|
+
writeDraft(anon, { title: 'x' })
|
|
214
|
+
writeDraft(anne, { title: 'y' })
|
|
215
|
+
window.localStorage.setItem('unrelated:anonymous', 'keep')
|
|
216
|
+
|
|
217
|
+
expect(clearAnonymousDrafts()).toBe(1)
|
|
218
|
+
expect(window.localStorage.getItem(anon)).toBeNull()
|
|
219
|
+
expect(window.localStorage.getItem(anne)).not.toBeNull()
|
|
220
|
+
expect(window.localStorage.getItem('unrelated:anonymous')).toBe('keep')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// @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-is-written-only-after-a-person-changes-a-field
|
|
225
|
+
describe('hasDraftChanges', () => {
|
|
226
|
+
it('treats empty values as equal', () => {
|
|
227
|
+
expect(sameDraftValue(null, '')).toBe(true)
|
|
228
|
+
expect(sameDraftValue(undefined, [])).toBe(true)
|
|
229
|
+
expect(sameDraftValue('a', 'b')).toBe(false)
|
|
230
|
+
expect(sameDraftValue({ a: 1 }, { a: 1 })).toBe(true)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('counts only fields a person edited', () => {
|
|
234
|
+
const baseline = { title: 'Call back', notes: '' }
|
|
235
|
+
// The form changed `notes` by itself; nobody edited it.
|
|
236
|
+
expect(hasDraftChanges({ title: 'Call back', notes: 'filled in' }, baseline, [])).toBe(false)
|
|
237
|
+
// Edited, but back to what it was.
|
|
238
|
+
expect(hasDraftChanges({ title: 'Call back' }, baseline, ['title'])).toBe(false)
|
|
239
|
+
expect(hasDraftChanges({ title: 'Call back tomorrow' }, baseline, ['title'])).toBe(true)
|
|
240
|
+
})
|
|
241
|
+
})
|