@conduction/nextcloud-vue 2.108.0 → 2.109.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/CnAddressSearch/CnAddressSearch.vue.js +10 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +246 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +106 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +105 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +495 -61
- package/dist/esm/utils/actionsDispatcher.js +6 -1
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/builtInModals.js +3 -1
- package/dist/esm/utils/builtInModals.js.map +1 -1
- package/dist/esm/utils/duplicateWarning.js +62 -0
- package/dist/esm/utils/duplicateWarning.js.map +1 -0
- package/dist/esm/utils/errors.js +4 -0
- package/dist/esm/utils/errors.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/openDuplicateWarning.js +39 -0
- package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
- package/dist/esm/utils/optionsFrom.js +69 -2
- package/dist/esm/utils/optionsFrom.js.map +1 -1
- package/dist/esm/utils/planTree.js +57 -0
- package/dist/esm/utils/planTree.js.map +1 -0
- package/dist/esm/utils/redactionSegments.js +53 -0
- package/dist/esm/utils/redactionSegments.js.map +1 -0
- package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
- package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
- package/dist/esm/utils/searchRefusal.js +62 -0
- package/dist/esm/utils/searchRefusal.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/nextcloud-vue.cjs.js +17409 -12832
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +495 -61
- package/l10n/en.json +106 -1
- package/l10n/nl.json +105 -1
- package/package.json +1 -1
- package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
- package/src/components/CnAddressSearch/index.js +12 -0
- package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
- package/src/components/CnAssignDialog/index.js +12 -0
- package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
- package/src/components/CnDecisionListWidget/index.js +12 -0
- package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
- package/src/components/CnDuplicateWarning/index.js +12 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
- package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
- package/src/components/CnNcDashboardList/index.js +12 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
- package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
- package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
- package/src/components/CnPlanWidget/index.js +12 -0
- package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
- package/src/components/CnRedactionEditor/index.js +13 -0
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
- package/src/components/CnRetentionWidget/index.js +12 -0
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
- package/src/components/CnWorkQueue/CnWorkQueue.vue +456 -0
- package/src/components/CnWorkQueue/index.js +12 -0
- package/src/components/index.js +9 -0
- package/src/css/index-page.css +24 -0
- package/src/index.js +12 -0
- package/src/schemas/app-manifest-v2.schema.json +119 -3
- package/src/utils/actionsDispatcher.js +6 -1
- package/src/utils/builtInModals.js +3 -1
- package/src/utils/duplicateWarning.js +59 -0
- package/src/utils/errors.js +4 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/openDuplicateWarning.js +35 -0
- package/src/utils/optionsFrom.js +68 -1
- package/src/utils/planTree.js +54 -0
- package/src/utils/redactionSegments.js +50 -0
- package/src/utils/registerNcDashboardWidgets.js +57 -0
- package/src/utils/searchRefusal.js +59 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.133.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -594,7 +594,10 @@
|
|
|
594
594
|
"responsePath": { "type": "string", "minLength": 1, "description": "Where the rows sit in the answer." },
|
|
595
595
|
"valueField": { "type": "string", "minLength": 1 },
|
|
596
596
|
"labelField": { "type": "string", "minLength": 1 },
|
|
597
|
-
"groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." }
|
|
597
|
+
"groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." },
|
|
598
|
+
"where": { "type": "object", "description": "Keep the rows whose fields equal these values; a value may be `@answer.<key>`, another field of the form (object-list-edit-in-place)." },
|
|
599
|
+
"itemsPath": { "type": "string", "minLength": 1, "description": "Read the options from the list at this path in each row (object-list-edit-in-place)." },
|
|
600
|
+
"hideWhenEmpty": { "type": "boolean", "description": "Leave the field out while the source gives no options (object-list-edit-in-place)." }
|
|
598
601
|
}
|
|
599
602
|
},
|
|
600
603
|
{
|
|
@@ -607,7 +610,10 @@
|
|
|
607
610
|
"filter": { "type": "object", "description": "Filters on the objects, as a list filter takes them." },
|
|
608
611
|
"valueField": { "type": "string", "minLength": 1 },
|
|
609
612
|
"labelField": { "type": "string", "minLength": 1 },
|
|
610
|
-
"groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." }
|
|
613
|
+
"groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." },
|
|
614
|
+
"where": { "type": "object", "description": "Keep the rows whose fields equal these values; a value may be `@answer.<key>`, another field of the form (object-list-edit-in-place)." },
|
|
615
|
+
"itemsPath": { "type": "string", "minLength": 1, "description": "Read the options from the list at this path in each row (object-list-edit-in-place)." },
|
|
616
|
+
"hideWhenEmpty": { "type": "boolean", "description": "Leave the field out while the source gives no options (object-list-edit-in-place)." }
|
|
611
617
|
}
|
|
612
618
|
}
|
|
613
619
|
]
|
|
@@ -2343,6 +2349,115 @@
|
|
|
2343
2349
|
}
|
|
2344
2350
|
}
|
|
2345
2351
|
},
|
|
2352
|
+
{
|
|
2353
|
+
"description": "decision-list built-in widget (CnDecisionListWidget): per-row sign-offs, each with an answer, its fields and a reason, sent to an app endpoint (2.133.0). Texts go through the host translate.",
|
|
2354
|
+
"if": {
|
|
2355
|
+
"properties": { "widgetKey": { "const": "decision-list" } },
|
|
2356
|
+
"required": ["widgetKey"]
|
|
2357
|
+
},
|
|
2358
|
+
"then": {
|
|
2359
|
+
"properties": {
|
|
2360
|
+
"props": {
|
|
2361
|
+
"type": "object",
|
|
2362
|
+
"properties": {
|
|
2363
|
+
"content": {
|
|
2364
|
+
"type": "object",
|
|
2365
|
+
"properties": {
|
|
2366
|
+
"source": { "type": "object", "additionalProperties": true, "description": "An endpointSource: `url`, `params?`, `responsePath?`." },
|
|
2367
|
+
"itemsPath": { "type": "string", "minLength": 1},
|
|
2368
|
+
"idField": { "type": "string", "minLength": 1},
|
|
2369
|
+
"title": { "type": "string", "description": "A template filled from a row." },
|
|
2370
|
+
"meta": { "type": "string", "description": "A template filled from a row." },
|
|
2371
|
+
"answers": { "type": "array", "items": { "type": "object", "required": ["value", "label"], "additionalProperties": true, "properties": { "value": {}, "label": { "type": "string", "minLength": 1}, "fields": { "type": "array", "items": { "type": "object", "required": ["field"], "additionalProperties": true, "properties": { "field": { "type": "string", "minLength": 1}, "type": { "type": "string", "enum": ["date", "text"] }, "label": { "type": "string" }, "required": { "type": "boolean" } }} }} }},
|
|
2372
|
+
"reason": { "description": "`{ label?, required?, field? }` (required by default, sent as `field`, default `reason`); `false` drops it.", "anyOf": [{ "type": "boolean", "const": false}, { "type": "object", "additionalProperties": false, "properties": { "label": { "type": "string" }, "required": { "type": "boolean" }, "field": { "type": "string", "minLength": 1} }}] },
|
|
2373
|
+
"submit": { "type": "object", "required": ["request"], "additionalProperties": true, "properties": { "label": { "type": "string" }, "request": { "type": "object", "required": ["url"], "additionalProperties": true, "properties": { "method": { "type": "string", "minLength": 1}, "url": { "type": "string", "minLength": 1}, "body": { "type": "object", "additionalProperties": true} }} }},
|
|
2374
|
+
"answerField": { "type": "string", "minLength": 1},
|
|
2375
|
+
"emptyText": { "type": "string" },
|
|
2376
|
+
"errorTitle": { "type": "string" },
|
|
2377
|
+
"answerLabel": { "type": "string" }
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
"description": "retention built-in widget (CnRetentionWidget): a record's stored retention, and a recompute for archivists (2.133.0). Texts go through the host translate.",
|
|
2387
|
+
"if": {
|
|
2388
|
+
"properties": { "widgetKey": { "const": "retention" } },
|
|
2389
|
+
"required": ["widgetKey"]
|
|
2390
|
+
},
|
|
2391
|
+
"then": {
|
|
2392
|
+
"properties": {
|
|
2393
|
+
"props": {
|
|
2394
|
+
"type": "object",
|
|
2395
|
+
"properties": {
|
|
2396
|
+
"content": {
|
|
2397
|
+
"type": "object",
|
|
2398
|
+
"properties": {
|
|
2399
|
+
"title": { "type": "string" },
|
|
2400
|
+
"subtitle": { "type": "string" },
|
|
2401
|
+
"statusField": { "type": "string", "minLength": 1},
|
|
2402
|
+
"archivedValues": { "type": "array", "items": { "type": "string" } },
|
|
2403
|
+
"archivistGroups": { "type": "array", "items": { "type": "string", "minLength": 1}, "description": "Groups that may recompute (default [\"archivaris\"]; admins always may)." },
|
|
2404
|
+
"recompute": { "type": "boolean", "description": "false hides the recompute control." }
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
"description": "plan built-in widget (CnPlanWidget): a record's planned steps from OpenRegister's case layer (2.133.0).",
|
|
2414
|
+
"if": {
|
|
2415
|
+
"properties": { "widgetKey": { "const": "plan" } },
|
|
2416
|
+
"required": ["widgetKey"]
|
|
2417
|
+
},
|
|
2418
|
+
"then": {
|
|
2419
|
+
"properties": {
|
|
2420
|
+
"props": {
|
|
2421
|
+
"type": "object",
|
|
2422
|
+
"properties": {
|
|
2423
|
+
"content": {
|
|
2424
|
+
"type": "object",
|
|
2425
|
+
"properties": {
|
|
2426
|
+
"emptyText": { "type": "string", "description": "Through the host translate." }
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
"description": "work-queue built-in widget (CnWorkQueue): one queue of everything waiting on the reader, in groups that can be hidden until tomorrow (2.133.0).",
|
|
2436
|
+
"if": {
|
|
2437
|
+
"properties": { "widgetKey": { "const": "work-queue" } },
|
|
2438
|
+
"required": ["widgetKey"]
|
|
2439
|
+
},
|
|
2440
|
+
"then": {
|
|
2441
|
+
"properties": {
|
|
2442
|
+
"props": {
|
|
2443
|
+
"type": "object",
|
|
2444
|
+
"properties": {
|
|
2445
|
+
"content": {
|
|
2446
|
+
"type": "object",
|
|
2447
|
+
"properties": {
|
|
2448
|
+
"source": { "type": "object", "additionalProperties": true, "description": "An endpointSource answering `{ groups, hiddenGroups?, unavailable? }`." },
|
|
2449
|
+
"hide": { "type": "string", "minLength": 1, "description": "A url template with `{group}` that hides a group until tomorrow." },
|
|
2450
|
+
"show": { "type": "string", "minLength": 1, "description": "A url template with `{group}` that shows a hidden group again." },
|
|
2451
|
+
"itemHref": { "type": "string", "minLength": 1, "description": "A template filled from an item, for items without `route` or `href`." },
|
|
2452
|
+
"waitingParties": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
2453
|
+
"emptyText": { "type": "string" }
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2346
2461
|
{
|
|
2347
2462
|
"description": "audit-trail built-in widget (CnAuditTrailWidget, Wave 1): the object change-log card. All props are optional — register/schema/objectId normally arrive via the detail-page object-context merge; explicit props win.",
|
|
2348
2463
|
"if": {
|
|
@@ -5287,6 +5402,7 @@
|
|
|
5287
5402
|
}
|
|
5288
5403
|
},
|
|
5289
5404
|
"search": { "type": "boolean", "default": true, "description": "type='index': `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page. Wins over `inlineSearch` (2.127.0)." },
|
|
5405
|
+
"searchRefusalHint": { "type": "string", "description": "type='index': what to do after a refused search, through the app's translate; the default explains AND, OR, NOT and quotes (2.133.0)." },
|
|
5290
5406
|
"searchPlacement": { "type": "string", "enum": ["", "toolbar"], "default": "", "description": "type='index', board look: where the search field sits. `toolbar` puts it on row 1 before the Filter button and the view switch instead of opening row 2 (DqTeamwachtrij, 2.127.0)." },
|
|
5291
5407
|
"sortSelectOptions": { "type": "array", "description": "type='index': the orders the index sidebar's Sort section offers in the list, card and map views, `{ value, label, order?, default?, source? }`. An option with a `source` takes its rows from an app endpoint, in that endpoint's order, for an order the server cannot sort by; the page's search, sidebar filters and pager run over those rows in the browser, and it is chosen by its value. `default: true` opens the page on it unless the address carries an order; a column header click in the table view leaves it. When the endpoint fails the list falls back to the option's `order` and shows `failedNote` (2.131.0).", "items": { "type": "object", "required": ["value", "label"], "properties": { "value": { "type": "string", "minLength": 1 }, "label": { "type": "string", "minLength": 1 }, "order": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object" }, { "type": "array" } ] }, "default": { "type": "boolean" }, "source": { "type": "object", "required": ["url"], "additionalProperties": false, "properties": { "url": { "type": "string", "minLength": 1, "description": "An app path, resolved with generateUrl and fetched with the session." }, "params": { "type": "object", "description": "Query parameters sent with the request." }, "rowsPath": { "type": "string", "minLength": 1, "description": "Where the entries sit in the answer; `results` by default, a bare array as is." }, "rowPath": { "type": "string", "minLength": 1, "description": "Where each entry carries its row; entries without one are left out." }, "carry": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "Entry fields copied onto each row, such as a tier a card's head pill reads." }, "idPath": { "type": "string", "minLength": 1, "description": "The row's identity for `annotate`; `id` by default." }, "annotate": { "type": "boolean", "description": "Copy the carried fields onto the rows of every other order too, matched on idPath." }, "searchFields": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "The fields the search box matches; every top-level text by default." }, "failedNote": { "type": "string", "minLength": 1, "description": "Shown above the list when the endpoint failed and the option's order stands in, through the app's translate." } } } } } },
|
|
5292
5408
|
"defaultOrder": { "description": "type='index', self-fetch: the order the page opens on when neither the address nor sortKeys gives one: `\"-createdAt\"`, `\"priority:desc,title\"`, an `_order` map or a `[{ key, order }]` list (2.127.0).", "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] } }, { "type": "array", "items": { "type": "object", "required": ["key"], "properties": { "key": { "type": "string", "minLength": 1 }, "order": { "type": "string", "enum": ["asc", "desc"] } } } } ] },
|
|
@@ -1021,7 +1021,12 @@ export function dispatchAction(action, context = {}) {
|
|
|
1021
1021
|
if (op === 'delete') {
|
|
1022
1022
|
return store.deleteObject(type, id)
|
|
1023
1023
|
}
|
|
1024
|
-
// patch: the
|
|
1024
|
+
// patch: only the action's values, as a PATCH, so every other key
|
|
1025
|
+
// keeps its stored value. A store without patchObject writes the
|
|
1026
|
+
// row's object merged with the values.
|
|
1027
|
+
if (typeof store.patchObject === 'function') {
|
|
1028
|
+
return store.patchObject(type, id, { ...(action.values || {}) })
|
|
1029
|
+
}
|
|
1025
1030
|
return store.saveObject(type, { ...row, ...(action.values || {}), id })
|
|
1026
1031
|
}
|
|
1027
1032
|
|
|
@@ -13,13 +13,15 @@ import { defineAsyncComponent } from 'vue'
|
|
|
13
13
|
/**
|
|
14
14
|
* The built-in modals, as registry entries (`{ kind: 'modal', component }`):
|
|
15
15
|
* `CnActionDialog`, the generic summary, checklist, reason and confirm dialog,
|
|
16
|
-
*
|
|
16
|
+
* and `CnAssignDialog`, hand a record to another user or team; each takes its
|
|
17
|
+
* `config` in the action's `props`. Loaded on first open.
|
|
17
18
|
*
|
|
18
19
|
* @type {Readonly<Record<string, {kind: string, component: object}>>}
|
|
19
20
|
* @spec openspec/changes/action-dialog/specs/action-dialog/spec.md#requirement-a-manifest-configures-an-action-dialog
|
|
20
21
|
*/
|
|
21
22
|
export const BUILT_IN_MODALS = Object.freeze({
|
|
22
23
|
CnActionDialog: { kind: 'modal', component: defineAsyncComponent(() => import('../components/CnActionDialog/CnActionDialog.vue')) },
|
|
24
|
+
CnAssignDialog: { kind: 'modal', component: defineAsyncComponent(() => import('../components/CnAssignDialog/CnAssignDialog.vue')) },
|
|
23
25
|
})
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* What a duplicate warning offers, decided in one place, and the candidate an
|
|
6
|
+
* app sends to OpenRegister's duplicate check. None of this enforces
|
|
7
|
+
* anything: the server refuses a duplicate on the write. These helpers make
|
|
8
|
+
* sure the user is told before pressing Create rather than after.
|
|
9
|
+
*
|
|
10
|
+
* @module utils/duplicateWarning
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What the duplicate warning shows and which buttons it offers.
|
|
15
|
+
*
|
|
16
|
+
* A policy other than `block` reads as `warn`, as on the server. `checked:
|
|
17
|
+
* false` means the check never answered: nothing is shown, because the write
|
|
18
|
+
* still refuses a real duplicate.
|
|
19
|
+
*
|
|
20
|
+
* @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
|
|
21
|
+
* @param {object} state The state to decide from.
|
|
22
|
+
* @param {Array<object>} [state.matches] The scored matches.
|
|
23
|
+
* @param {boolean} [state.checked] Whether the check ran (default true).
|
|
24
|
+
* @param {string} [state.policy] `warn` or `block`.
|
|
25
|
+
* @param {boolean} [state.mayOverride] Whether this account may file over a block.
|
|
26
|
+
* @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}} What to draw.
|
|
27
|
+
*/
|
|
28
|
+
export function duplicateAffordances(state) {
|
|
29
|
+
const source = state || {}
|
|
30
|
+
const matches = Array.isArray(source.matches) ? source.matches : []
|
|
31
|
+
const show = source.checked !== false && matches.length > 0
|
|
32
|
+
const blocked = show && source.policy === 'block'
|
|
33
|
+
const mayOverride = source.mayOverride === true
|
|
34
|
+
return {
|
|
35
|
+
show,
|
|
36
|
+
blocked,
|
|
37
|
+
mayContinue: show && (!blocked || mayOverride),
|
|
38
|
+
needsReason: blocked && mayOverride,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The candidate to send to the duplicate check, without blank fields.
|
|
44
|
+
*
|
|
45
|
+
* OpenRegister scores two empty strings as a match, and a form sends `''`
|
|
46
|
+
* for every field nobody filled in, so blanks would match almost everything.
|
|
47
|
+
*
|
|
48
|
+
* @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
|
|
49
|
+
* @param {object} payload The form's values.
|
|
50
|
+
* @return {object} The candidate.
|
|
51
|
+
*/
|
|
52
|
+
export function candidateWithoutBlanks(payload) {
|
|
53
|
+
return Object.fromEntries(Object.entries(payload || {}).filter(([, value]) => {
|
|
54
|
+
if (value === null || value === undefined || value === '') {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
return !Array.isArray(value) || value.length > 0
|
|
58
|
+
}))
|
|
59
|
+
}
|
package/src/utils/errors.js
CHANGED
|
@@ -9,6 +9,7 @@ import { translateValidationMessage } from './validationMessages.js'
|
|
|
9
9
|
* @property {object|null} details Validation errors or additional details
|
|
10
10
|
* @property {boolean} isValidation Whether this is a validation error (400/422)
|
|
11
11
|
* @property {object|null} fields Per-field validation errors
|
|
12
|
+
* @property {number|null} [position] The 1-based character a refused search term stopped at (400 only)
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -62,6 +63,7 @@ export async function parseResponseError(response, type) {
|
|
|
62
63
|
const status = response.status
|
|
63
64
|
let details = null
|
|
64
65
|
let fields = null
|
|
66
|
+
let position = null
|
|
65
67
|
let message
|
|
66
68
|
|
|
67
69
|
try {
|
|
@@ -74,6 +76,7 @@ export async function parseResponseError(response, type) {
|
|
|
74
76
|
} else if (body && typeof body === 'object') {
|
|
75
77
|
details = body.errors || body.error || body.message || null
|
|
76
78
|
fields = body.validationErrors || body.errors || null
|
|
79
|
+
position = Number.isInteger(body.position) && body.position > 0 ? body.position : null
|
|
77
80
|
}
|
|
78
81
|
} catch {
|
|
79
82
|
// Response body is not JSON
|
|
@@ -88,6 +91,7 @@ export async function parseResponseError(response, type) {
|
|
|
88
91
|
details,
|
|
89
92
|
isValidation: true,
|
|
90
93
|
fields,
|
|
94
|
+
position,
|
|
91
95
|
toString() {
|
|
92
96
|
return this.message
|
|
93
97
|
},
|
|
@@ -59,6 +59,7 @@ export const BUILT_IN_WIDGET_KEYS = Object.freeze([
|
|
|
59
59
|
'form-renderer',
|
|
60
60
|
'map-viewer',
|
|
61
61
|
'notice',
|
|
62
|
+
'decision-list',
|
|
62
63
|
'object-geo',
|
|
63
64
|
'card-grid',
|
|
64
65
|
'nav-card-grid',
|
|
@@ -134,10 +135,12 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
134
135
|
'object-table',
|
|
135
136
|
'party-actions',
|
|
136
137
|
'people',
|
|
138
|
+
'plan',
|
|
137
139
|
'quicklinks',
|
|
138
140
|
'radio-cards',
|
|
139
141
|
'record-list',
|
|
140
142
|
'related',
|
|
143
|
+
'retention',
|
|
141
144
|
'saved-view',
|
|
142
145
|
'saved-views',
|
|
143
146
|
'sections',
|
|
@@ -159,6 +162,7 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
159
162
|
'video',
|
|
160
163
|
'week-strip',
|
|
161
164
|
'working-calendar',
|
|
165
|
+
'work-queue',
|
|
162
166
|
'workspace-filter',
|
|
163
167
|
])
|
|
164
168
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Open CnDuplicateWarning from code (a create override, a store action) and
|
|
6
|
+
* wait for the user's answer.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/openDuplicateWarning
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createApp } from 'vue'
|
|
12
|
+
import CnDuplicateWarning from '../components/CnDuplicateWarning/CnDuplicateWarning.vue'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mount the warning on the page and resolve with the decision: `{ reason,
|
|
16
|
+
* over }` when the user files anyway, null when they cancel.
|
|
17
|
+
*
|
|
18
|
+
* @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
|
|
19
|
+
* @param {object} props CnDuplicateWarning's props (`matches`, `records`, `policy`, `mayOverride`, ...).
|
|
20
|
+
* @return {Promise<{reason: string, over: Array<string>}|null>} The decision.
|
|
21
|
+
*/
|
|
22
|
+
export function openDuplicateWarning(props) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const host = document.createElement('div')
|
|
25
|
+
document.body.appendChild(host)
|
|
26
|
+
let app = null
|
|
27
|
+
const finish = (decision) => {
|
|
28
|
+
app.unmount()
|
|
29
|
+
host.remove()
|
|
30
|
+
resolve(decision)
|
|
31
|
+
}
|
|
32
|
+
app = createApp(CnDuplicateWarning, { ...(props || {}), onClose: () => finish(null), onFile: (decision) => finish(decision) })
|
|
33
|
+
app.mount(host)
|
|
34
|
+
})
|
|
35
|
+
}
|
package/src/utils/optionsFrom.js
CHANGED
|
@@ -113,7 +113,7 @@ export async function loadOptionsFrom(from, ctx = {}) {
|
|
|
113
113
|
return []
|
|
114
114
|
}
|
|
115
115
|
const data = await fetchEndpointSource(source, ctx, { force: true })
|
|
116
|
-
const rows = Array.isArray(data) ? data : []
|
|
116
|
+
const rows = narrowRows(Array.isArray(data) ? data : [], from)
|
|
117
117
|
const seen = new Set()
|
|
118
118
|
const out = []
|
|
119
119
|
for (const row of rows) {
|
|
@@ -131,6 +131,73 @@ export async function loadOptionsFrom(from, ctx = {}) {
|
|
|
131
131
|
return out
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The rows a source answers, narrowed: `where` keeps the rows whose fields
|
|
136
|
+
* equal its values (`{ type: 'string' }`), and `itemsPath` replaces each
|
|
137
|
+
* remaining row by the list at that path (the formats of one property type).
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<unknown>} rows The rows.
|
|
140
|
+
* @param {{where?: object, itemsPath?: string}} from The `optionsFrom` block.
|
|
141
|
+
* @return {Array<unknown>} The rows the options are read from.
|
|
142
|
+
*/
|
|
143
|
+
function narrowRows(rows, from) {
|
|
144
|
+
let out = rows
|
|
145
|
+
if (isPlainObject(from.where)) {
|
|
146
|
+
out = out.filter((row) => row && typeof row === 'object' && Object.entries(from.where).every(([key, value]) => String(readPath(row, key)) === String(value)))
|
|
147
|
+
}
|
|
148
|
+
if (typeof from.itemsPath === 'string' && from.itemsPath !== '') {
|
|
149
|
+
out = out.flatMap((row) => {
|
|
150
|
+
const items = readPath(row, from.itemsPath)
|
|
151
|
+
return Array.isArray(items) ? items : []
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
return out
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* An `optionsFrom` block with every `@answer.<key>` string replaced by that
|
|
159
|
+
* form value, so a source can depend on another field (`where: { type:
|
|
160
|
+
* '@answer.propertyType' }`). A value the form does not hold becomes ''.
|
|
161
|
+
*
|
|
162
|
+
* @param {unknown} value The block, or a part of it.
|
|
163
|
+
* @param {object} formData The form values.
|
|
164
|
+
* @return {unknown} The block with the form values filled in.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveFormTokens(value, formData) {
|
|
167
|
+
if (typeof value === 'string' && value.startsWith('@answer.')) {
|
|
168
|
+
const held = (formData || {})[value.slice('@answer.'.length)]
|
|
169
|
+
return held === undefined || held === null ? '' : held
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
return value.map((v) => resolveFormTokens(v, formData))
|
|
173
|
+
}
|
|
174
|
+
if (isPlainObject(value)) {
|
|
175
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveFormTokens(v, formData)]))
|
|
176
|
+
}
|
|
177
|
+
return value
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The form fields an `optionsFrom` block reads through `@answer.<key>`.
|
|
182
|
+
*
|
|
183
|
+
* @param {unknown} from The block.
|
|
184
|
+
* @return {Array<string>} The keys.
|
|
185
|
+
*/
|
|
186
|
+
export function formTokenKeys(from) {
|
|
187
|
+
const keys = new Set()
|
|
188
|
+
const walk = (v) => {
|
|
189
|
+
if (typeof v === 'string' && v.startsWith('@answer.')) {
|
|
190
|
+
keys.add(v.slice('@answer.'.length))
|
|
191
|
+
} else if (Array.isArray(v)) {
|
|
192
|
+
v.forEach(walk)
|
|
193
|
+
} else if (isPlainObject(v)) {
|
|
194
|
+
Object.values(v).forEach(walk)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
walk(from)
|
|
198
|
+
return [...keys]
|
|
199
|
+
}
|
|
200
|
+
|
|
134
201
|
/**
|
|
135
202
|
* The options whose label holds the query, ignoring case. An empty query
|
|
136
203
|
* keeps them all.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The plan rows of OpenRegister's case layer as a tree, and the moves a
|
|
6
|
+
* user may ask for on one item. The moves are advisory: OpenRegister decides.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/planTree
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Plan item states after which nothing more can happen. */
|
|
12
|
+
export const TERMINAL_STATES = ['completed', 'terminated', 'disabled']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The plan rows as a tree: stages with their items, each level by position.
|
|
16
|
+
*
|
|
17
|
+
* @param {Array<object>} items The plan rows (`id`, `parentItemId`, `position`).
|
|
18
|
+
* @return {Array<object>} The roots, each with `children`.
|
|
19
|
+
*/
|
|
20
|
+
export function planTree(items) {
|
|
21
|
+
const rows = Array.isArray(items) ? items : []
|
|
22
|
+
const byId = new Map(rows.map((row) => [row.id, { ...row, children: [] }]))
|
|
23
|
+
const roots = []
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const node = byId.get(row.id)
|
|
26
|
+
const parent = row.parentItemId === null || row.parentItemId === undefined ? undefined : byId.get(row.parentItemId)
|
|
27
|
+
if (parent === undefined) {
|
|
28
|
+
roots.push(node)
|
|
29
|
+
} else {
|
|
30
|
+
parent.children.push(node)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const byPosition = (a, b) => (a.position ?? 0) - (b.position ?? 0)
|
|
34
|
+
for (const node of byId.values()) {
|
|
35
|
+
node.children.sort(byPosition)
|
|
36
|
+
}
|
|
37
|
+
return roots.sort(byPosition)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The target states a user may ask for. Advisory only: OpenRegister decides.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} item The plan item.
|
|
44
|
+
* @return {string[]} The targets.
|
|
45
|
+
*/
|
|
46
|
+
export function offeredTransitions(item) {
|
|
47
|
+
if (!item || TERMINAL_STATES.includes(item.state)) {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
if (item.type === 'milestone') {
|
|
51
|
+
return item.state === 'available' ? ['completed', 'terminated'] : []
|
|
52
|
+
}
|
|
53
|
+
return item.state === 'active' ? ['completed', 'terminated'] : ['terminated']
|
|
54
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* A document text cut into plain and marked segments, for the redaction
|
|
6
|
+
* editor to draw the passages in place.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/redactionSegments
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The text as segments: plain ones and marked ones carrying the passage's
|
|
13
|
+
* verdict (`open` before it has one). Where passages overlap the longest wins,
|
|
14
|
+
* and every occurrence of a passage is marked.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} text The document text.
|
|
17
|
+
* @param {Array<{value: string, verdict: (string|null)}>} passages The passages.
|
|
18
|
+
* @return {Array<{text: string, verdict: (string|null)}>} The segments.
|
|
19
|
+
*/
|
|
20
|
+
export function redactionSegments(text, passages) {
|
|
21
|
+
const source = String(text || '')
|
|
22
|
+
const ranges = []
|
|
23
|
+
const ordered = (Array.isArray(passages) ? passages : [])
|
|
24
|
+
.filter((passage) => passage && String(passage.value || '') !== '')
|
|
25
|
+
.sort((left, right) => right.value.length - left.value.length)
|
|
26
|
+
for (const passage of ordered) {
|
|
27
|
+
let from = source.indexOf(passage.value)
|
|
28
|
+
while (from !== -1) {
|
|
29
|
+
const to = from + passage.value.length
|
|
30
|
+
if (ranges.every((range) => to <= range.from || from >= range.to)) {
|
|
31
|
+
ranges.push({ from, to, verdict: passage.verdict || 'open' })
|
|
32
|
+
}
|
|
33
|
+
from = source.indexOf(passage.value, to)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
ranges.sort((left, right) => left.from - right.from)
|
|
37
|
+
const segments = []
|
|
38
|
+
let at = 0
|
|
39
|
+
for (const range of ranges) {
|
|
40
|
+
if (range.from > at) {
|
|
41
|
+
segments.push({ text: source.slice(at, range.from), verdict: null })
|
|
42
|
+
}
|
|
43
|
+
segments.push({ text: source.slice(range.from, range.to), verdict: range.verdict })
|
|
44
|
+
at = range.to
|
|
45
|
+
}
|
|
46
|
+
if (at < source.length) {
|
|
47
|
+
segments.push({ text: source.slice(at), verdict: null })
|
|
48
|
+
}
|
|
49
|
+
return segments
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Register an app's Nextcloud Dashboard widgets from configuration.
|
|
6
|
+
*
|
|
7
|
+
* @module utils/registerNcDashboardWidgets
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { translate } from '@nextcloud/l10n'
|
|
11
|
+
import { createApp } from 'vue'
|
|
12
|
+
import CnNcDashboardList from '../components/CnNcDashboardList/CnNcDashboardList.vue'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register each widget with `OCA.Dashboard.register(id, …)`. A widget mounts
|
|
16
|
+
* CnNcDashboardList with its `config`, or its own `component` with `props`
|
|
17
|
+
* and the widget's `title`, as its own Vue app with the app's translate provided as `cnTranslate`. The
|
|
18
|
+
* Dashboard host draws the widget's title, so the component must not.
|
|
19
|
+
*
|
|
20
|
+
* The widget ids are the ones the app's PHP `IWidget::getId()` returns; the
|
|
21
|
+
* server side stays the app's.
|
|
22
|
+
*
|
|
23
|
+
* @spec openspec/changes/nc-dashboard-widgets/specs/nc-dashboard-widgets/spec.md#requirement-dashboard-widgets-are-configuration
|
|
24
|
+
* @param {Array<{id: string, config?: object, component?: object, props?: object}>} widgets The widgets.
|
|
25
|
+
* @param {object} options Options.
|
|
26
|
+
* @param {string} options.appId The app id, for its translations.
|
|
27
|
+
* @param {(app: object) => void} [options.setup] Called on each widget's Vue app before mounting (to `use` a pinia, say).
|
|
28
|
+
* @param {object} [options.dashboard] The `OCA.Dashboard` registry (default `window.OCA.Dashboard`).
|
|
29
|
+
* @return {string[]} The ids registered; none when the Dashboard is absent.
|
|
30
|
+
*/
|
|
31
|
+
export function registerNcDashboardWidgets(widgets, options = {}) {
|
|
32
|
+
const dashboard = options.dashboard || (typeof window !== 'undefined' && window.OCA && window.OCA.Dashboard) || null
|
|
33
|
+
if (!dashboard || typeof dashboard.register !== 'function' || !Array.isArray(widgets)) {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
const appId = options.appId || 'nextcloud-vue'
|
|
37
|
+
const cnTranslate = (key, vars) => translate(appId, key, vars)
|
|
38
|
+
const registered = []
|
|
39
|
+
for (const widget of widgets) {
|
|
40
|
+
if (!widget || typeof widget.id !== 'string' || widget.id === '') {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
dashboard.register(widget.id, (el, context = {}) => {
|
|
44
|
+
const title = context.widget && context.widget.title ? context.widget.title : ''
|
|
45
|
+
const app = widget.component
|
|
46
|
+
? createApp(widget.component, { ...(widget.props || {}), title })
|
|
47
|
+
: createApp(CnNcDashboardList, { config: widget.config || {} })
|
|
48
|
+
app.provide('cnTranslate', cnTranslate)
|
|
49
|
+
if (typeof options.setup === 'function') {
|
|
50
|
+
options.setup(app)
|
|
51
|
+
}
|
|
52
|
+
app.mount(el)
|
|
53
|
+
})
|
|
54
|
+
registered.push(widget.id)
|
|
55
|
+
}
|
|
56
|
+
return registered
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* A refused search, said where it was typed. OpenRegister refuses a `_search`
|
|
6
|
+
* term it cannot parse (an unclosed bracket or quote) with `400 { error,
|
|
7
|
+
* position, term }` rather than running it as a literal, which would return
|
|
8
|
+
* nothing and read as an honest empty result.
|
|
9
|
+
*
|
|
10
|
+
* @module utils/searchRefusal
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** `<reason> at position <n>.`, the shape OpenRegister builds the message in. */
|
|
14
|
+
const POSITION_IN_MESSAGE = /\bat position (\d+)\.?\s*$/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The 1-based character a refusal points at, or null.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} error The ApiError.
|
|
20
|
+
* @return {number|null}
|
|
21
|
+
*/
|
|
22
|
+
function positionOf(error) {
|
|
23
|
+
const carried = error.position ?? (error.details && typeof error.details === 'object' ? error.details.position : undefined)
|
|
24
|
+
if (Number.isInteger(carried) && carried > 0) {
|
|
25
|
+
return carried
|
|
26
|
+
}
|
|
27
|
+
const match = POSITION_IN_MESSAGE.exec(String(error.message || ''))
|
|
28
|
+
const parsed = match ? Number(match[1]) : 0
|
|
29
|
+
return parsed > 0 ? parsed : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A search refusal, or null when this failure is not one: not a 400, no term
|
|
34
|
+
* typed, or no position to point at (then it is a plain failed fetch).
|
|
35
|
+
*
|
|
36
|
+
* @spec openspec/changes/index-search-refusal/specs/index-search-refusal/spec.md#requirement-a-refused-search-is-said-where-it-was-typed
|
|
37
|
+
* @param {object|null} error The ApiError the store recorded.
|
|
38
|
+
* @param {string} term The term the user typed.
|
|
39
|
+
* @return {{message: string, position: number, term: string, before: string, at: string, after: string}|null}
|
|
40
|
+
*/
|
|
41
|
+
export function readSearchRefusal(error, term) {
|
|
42
|
+
const typed = typeof term === 'string' ? term : ''
|
|
43
|
+
if (!error || typed.trim() === '' || Number(error.status) !== 400) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
const position = positionOf(error)
|
|
47
|
+
if (position === null) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
const index = Math.min(position - 1, typed.length)
|
|
51
|
+
return {
|
|
52
|
+
message: String(error.message || ''),
|
|
53
|
+
position,
|
|
54
|
+
term: typed,
|
|
55
|
+
before: typed.slice(0, index),
|
|
56
|
+
at: typed.slice(index, index + 1),
|
|
57
|
+
after: typed.slice(index + 1),
|
|
58
|
+
}
|
|
59
|
+
}
|