@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
|
@@ -57,6 +57,27 @@ const DEFAULT_FIELD_INSPECTION_CONFIG = {
|
|
|
57
57
|
// Schema holding conflict records. Empty keeps a conflict on the device,
|
|
58
58
|
// and the queue row says so rather than pretending it was filed.
|
|
59
59
|
conflictSchema: '',
|
|
60
|
+
// How a template's items are read (normaliseChecklistTemplate). Empty
|
|
61
|
+
// `sectionsField` reads a flat `items[]`; set it when the items sit inside
|
|
62
|
+
// ordered sections, each with its own `items[]`.
|
|
63
|
+
sectionsField: '',
|
|
64
|
+
itemKeyField: 'questionId',
|
|
65
|
+
itemTextField: 'text',
|
|
66
|
+
itemTypeField: 'type',
|
|
67
|
+
// The app's own item type words onto the leaf's (`yes_no`,
|
|
68
|
+
// `photo_required`; anything else renders as free text).
|
|
69
|
+
itemTypeMap: {},
|
|
70
|
+
// An item whose `photoRequiredField` equals `photoRequiredValue` needs a
|
|
71
|
+
// photo as well as an answer. Empty switches the gate off.
|
|
72
|
+
photoRequiredField: '',
|
|
73
|
+
photoRequiredValue: '',
|
|
74
|
+
// Where a finished run goes (checklistSubmission). Empty queues an object
|
|
75
|
+
// create on `resultSchema`. Set, it is a path under the Nextcloud root with
|
|
76
|
+
// `{field}` placeholders from the planned item, and the run replays as one
|
|
77
|
+
// POST there, naming the template and the planned item under these keys.
|
|
78
|
+
resultEndpoint: '',
|
|
79
|
+
resultTemplateParam: 'templateId',
|
|
80
|
+
resultPlannedItemParam: 'plannedItem',
|
|
60
81
|
};
|
|
61
82
|
|
|
62
83
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-inspection.js","sources":["../../../../src/integrations/builtin/field-inspection.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Built-in `field-inspection` integration registration (offline field\n * data-collection leaf).\n *\n * Surfaces the generic offline data-collection core (IndexedDB cache, mutation\n * queue, replay-on-reconnect, sync-state indicator — see\n * `src/integrations/offline/`) as a registry leaf with a sidebar tab and a\n * surface-aware widget. The checklist fields and the \"today's planned items\"\n * query are NOT hardcoded: they come from the integration's `offlineConfig`,\n * which a consuming app supplies when it registers an override of this id (or\n * relies on the per-object `integrationContext` for register/schema).\n *\n * Default registration uses the canonical `fieldInspection` / `checklistResult`\n * schema names; a consuming app (e.g. procest) registers the SAME id first with\n * its own `offlineConfig` to point the leaf at its schemas — AD-13 first-wins\n * collision policy keeps the consumer's config.\n *\n * @module integrations/builtin/field-inspection\n */\n\nimport { translate as t } from '@nextcloud/l10n'\nimport CnFieldInspectionCard from './field-inspection/CnFieldInspectionCard.vue'\nimport CnFieldInspectionTab from './field-inspection/CnFieldInspectionTab.vue'\n\n/**\n * Default offline configuration for the field-inspection leaf.\n *\n * A consuming app overrides any of these by registering the `field-inspection`\n * id with its own `offlineConfig` before `registerBuiltinIntegrations()` runs.\n *\n * @type {object}\n */\nexport const DEFAULT_FIELD_INSPECTION_CONFIG = {\n\t// Schema holding the planned items (the inspections to do today).\n\tplannedSchema: 'fieldInspection',\n\t// Schema holding the checklist templates referenced by planned items.\n\treferenceSchema: 'inspectionChecklist',\n\t// Property on a planned item that references its checklist template.\n\ttemplateRefField: 'checklistTemplateRef',\n\t// Schema the completed checklist result is written back to (the queued create).\n\tresultSchema: 'checklistResult',\n\t// Planning filter: property holding the assignee + the scheduled-date field.\n\tassigneeField: 'inspectorRef',\n\tdateField: 'scheduledAt',\n\t// Property on a planned item used as its display title.\n\ttitleField: 'caseRef',\n\t// Register the leaf writes its own bookkeeping to. Empty falls back to the\n\t// register each queued operation already names.\n\tregister: '',\n\t// Schema holding queue records, for a consuming app that wants the queue\n\t// itself readable server-side. Empty writes nothing.\n\tqueueSchema: '',\n\t// Schema holding conflict records. Empty keeps a conflict on the device,\n\t// and the queue row says so rather than pretending it was filed.\n\tconflictSchema: '',\n}\n\n/**\n * `field-inspection` integration descriptor.\n *\n * @type {object}\n */\nexport const fieldInspectionIntegration = {\n\tid: 'field-inspection',\n\tlabel: t('nextcloud-vue', 'Field inspections'),\n\tappName: t('nextcloud-vue', 'Field inspections'),\n\ticon: 'ClipboardCheckOutline',\n\torder: 68,\n\tgroup: 'workflow',\n\treferenceType: 'field-inspection',\n\taccentColor: '#21468B',\n\tofflineConfig: DEFAULT_FIELD_INSPECTION_CONFIG,\n\ttab: CnFieldInspectionTab,\n\twidget: CnFieldInspectionCard,\n\tdefaultSize: { w: 4, h: 4 },\n}\n\nexport default fieldInspectionIntegration\n"],"names":["t","CnFieldInspectionTab","CnFieldInspectionCard"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,+BAA+B,GAAG;AAC/C;AACA,CAAC,aAAa,EAAE,iBAAiB;AACjC;AACA,CAAC,eAAe,EAAE,qBAAqB;AACvC;AACA,CAAC,gBAAgB,EAAE,sBAAsB;AACzC;AACA,CAAC,YAAY,EAAE,iBAAiB;AAChC;AACA,CAAC,aAAa,EAAE,cAAc;AAC9B,CAAC,SAAS,EAAE,aAAa;AACzB;AACA,CAAC,UAAU,EAAE,SAAS;AACtB;AACA;AACA,CAAC,QAAQ,EAAE,EAAE;AACb;AACA;AACA,CAAC,WAAW,EAAE,EAAE;AAChB;AACA;AACA,CAAC,cAAc,EAAE,EAAE;AACnB,EAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,0BAA0B,GAAG;AAC1C,CAAC,EAAE,EAAE,kBAAkB;AACvB,CAAC,KAAK,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AAC/C,CAAC,OAAO,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AACjD,CAAC,IAAI,EAAE,uBAAuB;AAC9B,CAAC,KAAK,EAAE,EAAE;AACV,CAAC,KAAK,EAAE,UAAU;AAClB,CAAC,aAAa,EAAE,kBAAkB;AAClC,CAAC,WAAW,EAAE,SAAS;AACvB,CAAC,aAAa,EAAE,+BAA+B;AAC/C,CAAC,GAAG,EAAEC,UAAoB;AAC1B,CAAC,MAAM,EAAEC,YAAqB;AAC9B,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAC5B;;;;"}
|
|
1
|
+
{"version":3,"file":"field-inspection.js","sources":["../../../../src/integrations/builtin/field-inspection.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Built-in `field-inspection` integration registration (offline field\n * data-collection leaf).\n *\n * Surfaces the generic offline data-collection core (IndexedDB cache, mutation\n * queue, replay-on-reconnect, sync-state indicator — see\n * `src/integrations/offline/`) as a registry leaf with a sidebar tab and a\n * surface-aware widget. The checklist fields and the \"today's planned items\"\n * query are NOT hardcoded: they come from the integration's `offlineConfig`,\n * which a consuming app supplies when it registers an override of this id (or\n * relies on the per-object `integrationContext` for register/schema).\n *\n * Default registration uses the canonical `fieldInspection` / `checklistResult`\n * schema names; a consuming app (e.g. procest) registers the SAME id first with\n * its own `offlineConfig` to point the leaf at its schemas — AD-13 first-wins\n * collision policy keeps the consumer's config.\n *\n * @module integrations/builtin/field-inspection\n */\n\nimport { translate as t } from '@nextcloud/l10n'\nimport CnFieldInspectionCard from './field-inspection/CnFieldInspectionCard.vue'\nimport CnFieldInspectionTab from './field-inspection/CnFieldInspectionTab.vue'\n\n/**\n * Default offline configuration for the field-inspection leaf.\n *\n * A consuming app overrides any of these by registering the `field-inspection`\n * id with its own `offlineConfig` before `registerBuiltinIntegrations()` runs.\n *\n * @type {object}\n */\nexport const DEFAULT_FIELD_INSPECTION_CONFIG = {\n\t// Schema holding the planned items (the inspections to do today).\n\tplannedSchema: 'fieldInspection',\n\t// Schema holding the checklist templates referenced by planned items.\n\treferenceSchema: 'inspectionChecklist',\n\t// Property on a planned item that references its checklist template.\n\ttemplateRefField: 'checklistTemplateRef',\n\t// Schema the completed checklist result is written back to (the queued create).\n\tresultSchema: 'checklistResult',\n\t// Planning filter: property holding the assignee + the scheduled-date field.\n\tassigneeField: 'inspectorRef',\n\tdateField: 'scheduledAt',\n\t// Property on a planned item used as its display title.\n\ttitleField: 'caseRef',\n\t// Register the leaf writes its own bookkeeping to. Empty falls back to the\n\t// register each queued operation already names.\n\tregister: '',\n\t// Schema holding queue records, for a consuming app that wants the queue\n\t// itself readable server-side. Empty writes nothing.\n\tqueueSchema: '',\n\t// Schema holding conflict records. Empty keeps a conflict on the device,\n\t// and the queue row says so rather than pretending it was filed.\n\tconflictSchema: '',\n\t// How a template's items are read (normaliseChecklistTemplate). Empty\n\t// `sectionsField` reads a flat `items[]`; set it when the items sit inside\n\t// ordered sections, each with its own `items[]`.\n\tsectionsField: '',\n\titemKeyField: 'questionId',\n\titemTextField: 'text',\n\titemTypeField: 'type',\n\t// The app's own item type words onto the leaf's (`yes_no`,\n\t// `photo_required`; anything else renders as free text).\n\titemTypeMap: {},\n\t// An item whose `photoRequiredField` equals `photoRequiredValue` needs a\n\t// photo as well as an answer. Empty switches the gate off.\n\tphotoRequiredField: '',\n\tphotoRequiredValue: '',\n\t// Where a finished run goes (checklistSubmission). Empty queues an object\n\t// create on `resultSchema`. Set, it is a path under the Nextcloud root with\n\t// `{field}` placeholders from the planned item, and the run replays as one\n\t// POST there, naming the template and the planned item under these keys.\n\tresultEndpoint: '',\n\tresultTemplateParam: 'templateId',\n\tresultPlannedItemParam: 'plannedItem',\n}\n\n/**\n * `field-inspection` integration descriptor.\n *\n * @type {object}\n */\nexport const fieldInspectionIntegration = {\n\tid: 'field-inspection',\n\tlabel: t('nextcloud-vue', 'Field inspections'),\n\tappName: t('nextcloud-vue', 'Field inspections'),\n\ticon: 'ClipboardCheckOutline',\n\torder: 68,\n\tgroup: 'workflow',\n\treferenceType: 'field-inspection',\n\taccentColor: '#21468B',\n\tofflineConfig: DEFAULT_FIELD_INSPECTION_CONFIG,\n\ttab: CnFieldInspectionTab,\n\twidget: CnFieldInspectionCard,\n\tdefaultSize: { w: 4, h: 4 },\n}\n\nexport default fieldInspectionIntegration\n"],"names":["t","CnFieldInspectionTab","CnFieldInspectionCard"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,+BAA+B,GAAG;AAC/C;AACA,CAAC,aAAa,EAAE,iBAAiB;AACjC;AACA,CAAC,eAAe,EAAE,qBAAqB;AACvC;AACA,CAAC,gBAAgB,EAAE,sBAAsB;AACzC;AACA,CAAC,YAAY,EAAE,iBAAiB;AAChC;AACA,CAAC,aAAa,EAAE,cAAc;AAC9B,CAAC,SAAS,EAAE,aAAa;AACzB;AACA,CAAC,UAAU,EAAE,SAAS;AACtB;AACA;AACA,CAAC,QAAQ,EAAE,EAAE;AACb;AACA;AACA,CAAC,WAAW,EAAE,EAAE;AAChB;AACA;AACA,CAAC,cAAc,EAAE,EAAE;AACnB;AACA;AACA;AACA,CAAC,aAAa,EAAE,EAAE;AAClB,CAAC,YAAY,EAAE,YAAY;AAC3B,CAAC,aAAa,EAAE,MAAM;AACtB,CAAC,aAAa,EAAE,MAAM;AACtB;AACA;AACA,CAAC,WAAW,EAAE,EAAE;AAChB;AACA;AACA,CAAC,kBAAkB,EAAE,EAAE;AACvB,CAAC,kBAAkB,EAAE,EAAE;AACvB;AACA;AACA;AACA;AACA,CAAC,cAAc,EAAE,EAAE;AACnB,CAAC,mBAAmB,EAAE,YAAY;AAClC,CAAC,sBAAsB,EAAE,aAAa;AACtC,EAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,0BAA0B,GAAG;AAC1C,CAAC,EAAE,EAAE,kBAAkB;AACvB,CAAC,KAAK,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AAC/C,CAAC,OAAO,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AACjD,CAAC,IAAI,EAAE,uBAAuB;AAC9B,CAAC,KAAK,EAAE,EAAE;AACV,CAAC,KAAK,EAAE,UAAU;AAClB,CAAC,aAAa,EAAE,kBAAkB;AAClC,CAAC,WAAW,EAAE,SAAS;AACvB,CAAC,aAAa,EAAE,+BAA+B;AAC/C,CAAC,GAAG,EAAEC,UAAoB;AAC1B,CAAC,MAAM,EAAEC,YAAqB;AAC9B,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAC5B;;;;"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
5
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
*
|
|
7
|
+
* What a finished offline checklist run turns into in the queue.
|
|
8
|
+
*
|
|
9
|
+
* Two shapes, chosen by the leaf's `offlineConfig`:
|
|
10
|
+
*
|
|
11
|
+
* - `resultEndpoint` empty: an object create on `resultSchema`, the shape the
|
|
12
|
+
* leaf has always queued (`inspectionRef`, `checklistTemplateRef`,
|
|
13
|
+
* `items[]` with `questionId` / `answer` / `evidenceRefs`).
|
|
14
|
+
* - `resultEndpoint` set: a `submit` operation that replays the run as ONE
|
|
15
|
+
* POST to an app's own endpoint, for an app that stores a run as something
|
|
16
|
+
* other than a plain object (a completed task, say) and checks the run
|
|
17
|
+
* server side before it stores anything. The endpoint is a path under the
|
|
18
|
+
* Nextcloud root with `{field}` placeholders filled from the planned item,
|
|
19
|
+
* e.g. `/apps/myapp/api/cases/{caseRef}/checklist-run`.
|
|
20
|
+
*
|
|
21
|
+
* Pure: no axios, no IndexedDB, no DOM. The card queues what this returns and
|
|
22
|
+
* the replay service sends it.
|
|
23
|
+
*
|
|
24
|
+
* @module integrations/offline/checklistSubmission
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fill a `{field}` endpoint template from a planned item.
|
|
30
|
+
*
|
|
31
|
+
* Each value is URL-encoded, so a planned item can never steer the request to
|
|
32
|
+
* another path. A placeholder the item does not fill is an error rather than
|
|
33
|
+
* an empty path segment: a run sent to the wrong case is worse than one that
|
|
34
|
+
* waits.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} template The endpoint template.
|
|
37
|
+
* @param {object} plannedItem The planned item the run belongs to.
|
|
38
|
+
*
|
|
39
|
+
* @return {string} The filled path.
|
|
40
|
+
*
|
|
41
|
+
* @throws {Error} When a placeholder has no value on the item.
|
|
42
|
+
*/
|
|
43
|
+
function fillEndpoint(template, plannedItem) {
|
|
44
|
+
return String(template).replace(/\{([A-Za-z0-9_.]+)\}/g, (match, field) => {
|
|
45
|
+
const value = field.split('.').reduce((node, key) => (node === null || node === undefined ? undefined : node[key]), plannedItem);
|
|
46
|
+
if (value === undefined || value === null || String(value) === '') {
|
|
47
|
+
throw new Error(translate('nextcloud-vue', 'This planned item has no {field}, so the answers have nowhere to go.', { field }))
|
|
48
|
+
}
|
|
49
|
+
return encodeURIComponent(String(value))
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The id of an object, wherever OpenRegister carries it.
|
|
55
|
+
*
|
|
56
|
+
* @param {object|null} object The object.
|
|
57
|
+
*
|
|
58
|
+
* @return {string} The id, or an empty string.
|
|
59
|
+
*/
|
|
60
|
+
function idOf(object) {
|
|
61
|
+
return String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the queue operation for a finished checklist run.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} config The leaf's offline config.
|
|
68
|
+
* @param {object} run The run.
|
|
69
|
+
* @param {object} run.plannedItem The planned item.
|
|
70
|
+
* @param {object} run.template The normalised template (`items[]`).
|
|
71
|
+
* @param {object} run.answers Map of questionId to `{ answer, evidenceRefs }`.
|
|
72
|
+
* @param {object|null} [run.gps] The GPS fix at save, or null.
|
|
73
|
+
* @param {string} [run.gpsSource] `sensor` or `sensorless`.
|
|
74
|
+
* @param {string} [run.capturedAt] ISO time of the save.
|
|
75
|
+
* @param {boolean} [run.capturedOffline] Whether the device was offline at save.
|
|
76
|
+
*
|
|
77
|
+
* @return {{ operationType: string, schema: string, endpoint: (string|null), payload: object }}
|
|
78
|
+
* The operation fields for `enqueueMutation`.
|
|
79
|
+
*
|
|
80
|
+
* @throws {Error} When the endpoint cannot be filled from the planned item.
|
|
81
|
+
*/
|
|
82
|
+
function buildChecklistSubmission(config, run) {
|
|
83
|
+
const capturedAt = run.capturedAt || new Date().toISOString();
|
|
84
|
+
const gpsAtAnswer = run.gps ? { ...run.gps, source: run.gpsSource || 'sensor' } : { source: 'sensorless' };
|
|
85
|
+
const items = Array.isArray(run.template?.items) ? run.template.items : [];
|
|
86
|
+
const answers = run.answers ?? {};
|
|
87
|
+
const plannedItemId = idOf(run.plannedItem);
|
|
88
|
+
const templateId = idOf(run.template);
|
|
89
|
+
|
|
90
|
+
if (!config.resultEndpoint) {
|
|
91
|
+
return {
|
|
92
|
+
operationType: 'create',
|
|
93
|
+
schema: config.resultSchema,
|
|
94
|
+
endpoint: null,
|
|
95
|
+
payload: {
|
|
96
|
+
inspectionRef: plannedItemId,
|
|
97
|
+
checklistTemplateRef: templateId,
|
|
98
|
+
items: items.map((q) => ({
|
|
99
|
+
questionId: q.questionId,
|
|
100
|
+
answer: answers[q.questionId]?.answer,
|
|
101
|
+
evidenceRefs: answers[q.questionId]?.evidenceRefs ?? [],
|
|
102
|
+
answeredAt: capturedAt,
|
|
103
|
+
gpsAtAnswer,
|
|
104
|
+
})),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const payload = {
|
|
110
|
+
[config.resultTemplateParam || 'templateId']: templateId,
|
|
111
|
+
[config.resultPlannedItemParam || 'plannedItem']: plannedItemId,
|
|
112
|
+
capturedAt,
|
|
113
|
+
capturedOffline: run.capturedOffline === true,
|
|
114
|
+
items: items
|
|
115
|
+
.filter((q) => {
|
|
116
|
+
const entry = answers[q.questionId] ?? {};
|
|
117
|
+
const hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== '';
|
|
118
|
+
return hasAnswer || (Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0)
|
|
119
|
+
})
|
|
120
|
+
.map((q) => ({
|
|
121
|
+
itemId: q.questionId,
|
|
122
|
+
value: answers[q.questionId]?.answer ?? '',
|
|
123
|
+
photos: answers[q.questionId]?.evidenceRefs ?? [],
|
|
124
|
+
answeredAt: capturedAt,
|
|
125
|
+
gpsAtAnswer,
|
|
126
|
+
})),
|
|
127
|
+
};
|
|
128
|
+
if (run.gps) {
|
|
129
|
+
payload.location = { ...run.gps, source: run.gpsSource || 'sensor' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
operationType: 'submit',
|
|
134
|
+
schema: config.resultSchema || '',
|
|
135
|
+
endpoint: fillEndpoint(config.resultEndpoint, run.plannedItem),
|
|
136
|
+
payload,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { buildChecklistSubmission, fillEndpoint };
|
|
141
|
+
//# sourceMappingURL=checklistSubmission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checklistSubmission.js","sources":["../../../../src/integrations/offline/checklistSubmission.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * What a finished offline checklist run turns into in the queue.\n *\n * Two shapes, chosen by the leaf's `offlineConfig`:\n *\n * - `resultEndpoint` empty: an object create on `resultSchema`, the shape the\n * leaf has always queued (`inspectionRef`, `checklistTemplateRef`,\n * `items[]` with `questionId` / `answer` / `evidenceRefs`).\n * - `resultEndpoint` set: a `submit` operation that replays the run as ONE\n * POST to an app's own endpoint, for an app that stores a run as something\n * other than a plain object (a completed task, say) and checks the run\n * server side before it stores anything. The endpoint is a path under the\n * Nextcloud root with `{field}` placeholders filled from the planned item,\n * e.g. `/apps/myapp/api/cases/{caseRef}/checklist-run`.\n *\n * Pure: no axios, no IndexedDB, no DOM. The card queues what this returns and\n * the replay service sends it.\n *\n * @module integrations/offline/checklistSubmission\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/**\n * Fill a `{field}` endpoint template from a planned item.\n *\n * Each value is URL-encoded, so a planned item can never steer the request to\n * another path. A placeholder the item does not fill is an error rather than\n * an empty path segment: a run sent to the wrong case is worse than one that\n * waits.\n *\n * @param {string} template The endpoint template.\n * @param {object} plannedItem The planned item the run belongs to.\n *\n * @return {string} The filled path.\n *\n * @throws {Error} When a placeholder has no value on the item.\n */\nexport function fillEndpoint(template, plannedItem) {\n\treturn String(template).replace(/\\{([A-Za-z0-9_.]+)\\}/g, (match, field) => {\n\t\tconst value = field.split('.').reduce((node, key) => (node === null || node === undefined ? undefined : node[key]), plannedItem)\n\t\tif (value === undefined || value === null || String(value) === '') {\n\t\t\tthrow new Error(t('nextcloud-vue', 'This planned item has no {field}, so the answers have nowhere to go.', { field }))\n\t\t}\n\t\treturn encodeURIComponent(String(value))\n\t})\n}\n\n/**\n * The id of an object, wherever OpenRegister carries it.\n *\n * @param {object|null} object The object.\n *\n * @return {string} The id, or an empty string.\n */\nfunction idOf(object) {\n\treturn String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')\n}\n\n/**\n * Build the queue operation for a finished checklist run.\n *\n * @param {object} config The leaf's offline config.\n * @param {object} run The run.\n * @param {object} run.plannedItem The planned item.\n * @param {object} run.template The normalised template (`items[]`).\n * @param {object} run.answers Map of questionId to `{ answer, evidenceRefs }`.\n * @param {object|null} [run.gps] The GPS fix at save, or null.\n * @param {string} [run.gpsSource] `sensor` or `sensorless`.\n * @param {string} [run.capturedAt] ISO time of the save.\n * @param {boolean} [run.capturedOffline] Whether the device was offline at save.\n *\n * @return {{ operationType: string, schema: string, endpoint: (string|null), payload: object }}\n * The operation fields for `enqueueMutation`.\n *\n * @throws {Error} When the endpoint cannot be filled from the planned item.\n */\nexport function buildChecklistSubmission(config, run) {\n\tconst capturedAt = run.capturedAt || new Date().toISOString()\n\tconst gpsAtAnswer = run.gps ? { ...run.gps, source: run.gpsSource || 'sensor' } : { source: 'sensorless' }\n\tconst items = Array.isArray(run.template?.items) ? run.template.items : []\n\tconst answers = run.answers ?? {}\n\tconst plannedItemId = idOf(run.plannedItem)\n\tconst templateId = idOf(run.template)\n\n\tif (!config.resultEndpoint) {\n\t\treturn {\n\t\t\toperationType: 'create',\n\t\t\tschema: config.resultSchema,\n\t\t\tendpoint: null,\n\t\t\tpayload: {\n\t\t\t\tinspectionRef: plannedItemId,\n\t\t\t\tchecklistTemplateRef: templateId,\n\t\t\t\titems: items.map((q) => ({\n\t\t\t\t\tquestionId: q.questionId,\n\t\t\t\t\tanswer: answers[q.questionId]?.answer,\n\t\t\t\t\tevidenceRefs: answers[q.questionId]?.evidenceRefs ?? [],\n\t\t\t\t\tansweredAt: capturedAt,\n\t\t\t\t\tgpsAtAnswer,\n\t\t\t\t})),\n\t\t\t},\n\t\t}\n\t}\n\n\tconst payload = {\n\t\t[config.resultTemplateParam || 'templateId']: templateId,\n\t\t[config.resultPlannedItemParam || 'plannedItem']: plannedItemId,\n\t\tcapturedAt,\n\t\tcapturedOffline: run.capturedOffline === true,\n\t\titems: items\n\t\t\t.filter((q) => {\n\t\t\t\tconst entry = answers[q.questionId] ?? {}\n\t\t\t\tconst hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''\n\t\t\t\treturn hasAnswer || (Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0)\n\t\t\t})\n\t\t\t.map((q) => ({\n\t\t\t\titemId: q.questionId,\n\t\t\t\tvalue: answers[q.questionId]?.answer ?? '',\n\t\t\t\tphotos: answers[q.questionId]?.evidenceRefs ?? [],\n\t\t\t\tansweredAt: capturedAt,\n\t\t\t\tgpsAtAnswer,\n\t\t\t})),\n\t}\n\tif (run.gps) {\n\t\tpayload.location = { ...run.gps, source: run.gpsSource || 'sensor' }\n\t}\n\n\treturn {\n\t\toperationType: 'submit',\n\t\tschema: config.resultSchema || '',\n\t\tendpoint: fillEndpoint(config.resultEndpoint, run.plannedItem),\n\t\tpayload,\n\t}\n}\n"],"names":["t"],"mappings":"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"}
|
|
@@ -47,11 +47,71 @@ function classifyGps(fix, available = true) {
|
|
|
47
47
|
return { quality: 'good', source: 'sensor', warning: null }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Read a checklist template into the leaf's flat item contract.
|
|
52
|
+
*
|
|
53
|
+
* A consuming app's template does not have to carry `items[]` with
|
|
54
|
+
* `questionId` / `text` / `type`. Its `offlineConfig` names where those live:
|
|
55
|
+
* `sectionsField` when the items sit inside ordered sections, and the
|
|
56
|
+
* `item*Field` keys for the item's key, text and type. `itemTypeMap` translates
|
|
57
|
+
* the app's own type words onto the leaf's (`yes_no`, `photo_required`, or any
|
|
58
|
+
* other word, which renders as free text). `photoRequiredField` and
|
|
59
|
+
* `photoRequiredValue` mark an item whose answer also needs a photo.
|
|
60
|
+
*
|
|
61
|
+
* The template's own keys are kept, so the id and version stay readable; only
|
|
62
|
+
* `items` is replaced by the normalised list.
|
|
63
|
+
*
|
|
64
|
+
* @param {object|null} template The template as cached.
|
|
65
|
+
* @param {object} [config] The leaf's offline config.
|
|
66
|
+
*
|
|
67
|
+
* @return {object|null} The template with flat `items[]`, or null.
|
|
68
|
+
*/
|
|
69
|
+
function normaliseChecklistTemplate(template, config = {}) {
|
|
70
|
+
if (template === null || typeof template !== 'object') {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const keyField = config.itemKeyField || 'questionId';
|
|
75
|
+
const textField = config.itemTextField || 'text';
|
|
76
|
+
const typeField = config.itemTypeField || 'type';
|
|
77
|
+
const typeMap = (config.itemTypeMap && typeof config.itemTypeMap === 'object') ? config.itemTypeMap : {};
|
|
78
|
+
|
|
79
|
+
const sections = config.sectionsField ? template[config.sectionsField] : null;
|
|
80
|
+
const raw = Array.isArray(sections)
|
|
81
|
+
? sections.flatMap((section) => (Array.isArray(section?.items) ? section.items : []))
|
|
82
|
+
: (Array.isArray(template.items) ? template.items : []);
|
|
83
|
+
|
|
84
|
+
const items = [];
|
|
85
|
+
for (const item of raw) {
|
|
86
|
+
if (item === null || typeof item !== 'object') {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const questionId = String(item[keyField] ?? '');
|
|
90
|
+
if (questionId === '') {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
const ownType = item[typeField];
|
|
94
|
+
const photoRequired = Boolean(config.photoRequiredField)
|
|
95
|
+
&& item[config.photoRequiredField] === config.photoRequiredValue;
|
|
96
|
+
items.push({
|
|
97
|
+
...item,
|
|
98
|
+
questionId,
|
|
99
|
+
text: item[textField] ?? item.text ?? questionId,
|
|
100
|
+
type: typeMap[ownType] ?? ownType ?? 'text',
|
|
101
|
+
required: item.required === true,
|
|
102
|
+
photoRequired,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { ...template, items }
|
|
107
|
+
}
|
|
108
|
+
|
|
50
109
|
/**
|
|
51
110
|
* Validate a set of checklist answers against the template's required items.
|
|
52
111
|
*
|
|
53
|
-
* A `required` item must have a non-empty answer; a `photo_required` item
|
|
54
|
-
*
|
|
112
|
+
* A `required` item must have a non-empty answer; a `photo_required` item, and
|
|
113
|
+
* any item flagged `photoRequired` by {@link normaliseChecklistTemplate}, must
|
|
114
|
+
* have at least one evidence reference. Returns the per-question
|
|
55
115
|
* blocking errors so the UI can prevent save (and the engine never queues an
|
|
56
116
|
* invalid result).
|
|
57
117
|
*
|
|
@@ -67,7 +127,7 @@ function validateChecklistAnswers(template, answersByQuestion) {
|
|
|
67
127
|
const errors = [];
|
|
68
128
|
|
|
69
129
|
for (const item of items) {
|
|
70
|
-
if (item?.required !== true) {
|
|
130
|
+
if (item?.required !== true && item?.photoRequired !== true) {
|
|
71
131
|
continue
|
|
72
132
|
}
|
|
73
133
|
const entry = answers[item.questionId] ?? {};
|
|
@@ -81,7 +141,12 @@ function validateChecklistAnswers(template, answersByQuestion) {
|
|
|
81
141
|
continue
|
|
82
142
|
}
|
|
83
143
|
|
|
84
|
-
if (
|
|
144
|
+
if (item.photoRequired === true && evidenceRefs.length === 0) {
|
|
145
|
+
errors.push({ questionId: item.questionId, message: translate('nextcloud-vue', 'Photo required for this question') });
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (item.required === true && (answer === undefined || answer === null || String(answer).trim() === '')) {
|
|
85
150
|
errors.push({ questionId: item.questionId, message: translate('nextcloud-vue', 'This question is required') });
|
|
86
151
|
}
|
|
87
152
|
}
|
|
@@ -160,5 +225,5 @@ function syncIndicator(pendingCount, online, stuckCount = 0) {
|
|
|
160
225
|
return { tone: 'success', text: translate('nextcloud-vue', 'All changes synced') }
|
|
161
226
|
}
|
|
162
227
|
|
|
163
|
-
export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, syncIndicator, validateChecklistAnswers };
|
|
228
|
+
export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers };
|
|
164
229
|
//# sourceMappingURL=fieldCollectionHelpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldCollectionHelpers.js","sources":["../../../../src/integrations/offline/fieldCollectionHelpers.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Pure helpers for the generic offline data-collection workflow.\n *\n * Checklist required-field validation, N/M progress counting, GPS-accuracy\n * classification and the user-facing sync-status copy. These are DOM-free and\n * carry no app-specific field names: the checklist shape (`items[]` with\n * `questionId` / `type` / `required`) is the generic contract a consuming app's\n * checklist schema/config supplies. Extracted from procest's\n * `fieldInspectionHelpers` so any app can reuse them.\n *\n * @module integrations/offline/fieldCollectionHelpers\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** Accuracy worse than this (metres) triggers the \"poor signal\" warning. */\nexport const GPS_POOR_ACCURACY_M = 50\n\n/**\n * Classify a GPS fix into good / poor / sensorless and supply warning copy.\n *\n * @param {object|null} fix The Geolocation reading.\n * @param {number} [fix.accuracy] Accuracy in metres.\n * @param {boolean} [available] Whether the sensor produced any fix.\n *\n * @return {{ quality: ('good'|'poor'|'sensorless'), source: ('sensor'|'sensorless'), warning: (string|null) }}\n */\nexport function classifyGps(fix, available = true) {\n\tif (available === false || fix === null || fix === undefined) {\n\t\treturn { quality: 'sensorless', source: 'sensorless', warning: null }\n\t}\n\n\tconst accuracy = Number(fix.accuracy ?? Number.POSITIVE_INFINITY)\n\tif (accuracy > GPS_POOR_ACCURACY_M) {\n\t\tconst rounded = Number.isFinite(accuracy) ? Math.round(accuracy) : '?'\n\t\treturn {\n\t\t\tquality: 'poor',\n\t\t\tsource: 'sensor',\n\t\t\twarning: t('nextcloud-vue', 'Location imprecise (±{m}m). Wait for a better signal, or add the address by hand.', { m: rounded }),\n\t\t}\n\t}\n\n\treturn { quality: 'good', source: 'sensor', warning: null }\n}\n\n/**\n * Validate a set of checklist answers against the template's required items.\n *\n * A `required` item must have a non-empty answer; a `photo_required` item must\n * additionally have at least one evidence reference. Returns the per-question\n * blocking errors so the UI can prevent save (and the engine never queues an\n * invalid result).\n *\n * @param {object} template The checklist template (`items[]`).\n * @param {Array} template.items Items (`questionId`, `type`, `required`).\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ valid: boolean, errors: Array<{ questionId: string, message: string }> }}\n */\nexport function validateChecklistAnswers(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tconst errors = []\n\n\tfor (const item of items) {\n\t\tif (item?.required !== true) {\n\t\t\tcontinue\n\t\t}\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst answer = entry.answer\n\t\tconst evidenceRefs = Array.isArray(entry.evidenceRefs) ? entry.evidenceRefs : []\n\n\t\tif (item.type === 'photo_required') {\n\t\t\tif (evidenceRefs.length === 0) {\n\t\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (answer === undefined || answer === null || String(answer).trim() === '') {\n\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'This question is required') })\n\t\t}\n\t}\n\n\treturn { valid: errors.length === 0, errors }\n}\n\n/**\n * Count completed items in a checklist for the N/M progress indicator.\n *\n * @param {object} template The checklist template.\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ done: number, total: number }} Completed and total item counts.\n */\nexport function checklistProgress(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tlet done = 0\n\n\tfor (const item of items) {\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''\n\t\tconst hasEvidence = Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0\n\t\tif (item.type === 'photo_required' ? hasEvidence : hasAnswer) {\n\t\t\tdone += 1\n\t\t}\n\t}\n\n\treturn { done, total: items.length }\n}\n\n/**\n * Human-readable sync-status copy for the green/amber/red indicator.\n *\n * @param {number} pendingCount The number of pending operations.\n * @param {boolean} online Whether the device is online.\n * @param {number} [stuckCount] How many will not send again without somebody acting.\n *\n * @return {{ tone: ('success'|'warning'|'error'), text: string }} Indicator state.\n */\nexport function syncIndicator(pendingCount, online, stuckCount = 0) {\n\t// 🔴 STUCK WORK IS NAMED BEFORE ANYTHING ELSE, INCLUDING BEFORE BEING\n\t// OFFLINE. `pendingCount` counts pending, conflict and syncing; a `failed`\n\t// operation is in none of them, so a device holding a stranded inspection\n\t// used to reach the last line here and report \"All changes synced\" in\n\t// green. The one surface that could have told an inspector their morning\n\t// had not left the device said the opposite.\n\t//\n\t// Being offline is temporary and expected in the field. Being stuck is\n\t// neither, so it outranks it.\n\tif (stuckCount > 0) {\n\t\treturn {\n\t\t\ttone: 'error',\n\t\t\ttext: t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{n} changes are stuck and will not send on their own',\n\t\t\t\t{ n: stuckCount },\n\t\t\t),\n\t\t}\n\t}\n\n\t// Being offline is not a fault. A field device spends its day out of\n\t// signal by design, and red said something had gone wrong every hour of\n\t// every working day, which is how a palette stops being read at all. Red\n\t// is now reserved for the one state that needs somebody: stuck work.\n\tif (online === false) {\n\t\treturn {\n\t\t\ttone: 'warning',\n\t\t\ttext: t('nextcloud-vue', 'No connection. {n} changes are waiting and will send when you are back.', { n: pendingCount }),\n\t\t}\n\t}\n\tif (pendingCount > 0) {\n\t\treturn { tone: 'warning', text: t('nextcloud-vue', '{n} changes waiting for sync', { n: pendingCount }) }\n\t}\n\treturn { tone: 'success', text: t('nextcloud-vue', 'All changes synced') }\n}\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fieldCollectionHelpers.js","sources":["../../../../src/integrations/offline/fieldCollectionHelpers.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Pure helpers for the generic offline data-collection workflow.\n *\n * Checklist required-field validation, N/M progress counting, GPS-accuracy\n * classification and the user-facing sync-status copy. These are DOM-free and\n * carry no app-specific field names: the checklist shape (`items[]` with\n * `questionId` / `type` / `required`) is the generic contract a consuming app's\n * checklist schema/config supplies. Extracted from procest's\n * `fieldInspectionHelpers` so any app can reuse them.\n *\n * @module integrations/offline/fieldCollectionHelpers\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** Accuracy worse than this (metres) triggers the \"poor signal\" warning. */\nexport const GPS_POOR_ACCURACY_M = 50\n\n/**\n * Classify a GPS fix into good / poor / sensorless and supply warning copy.\n *\n * @param {object|null} fix The Geolocation reading.\n * @param {number} [fix.accuracy] Accuracy in metres.\n * @param {boolean} [available] Whether the sensor produced any fix.\n *\n * @return {{ quality: ('good'|'poor'|'sensorless'), source: ('sensor'|'sensorless'), warning: (string|null) }}\n */\nexport function classifyGps(fix, available = true) {\n\tif (available === false || fix === null || fix === undefined) {\n\t\treturn { quality: 'sensorless', source: 'sensorless', warning: null }\n\t}\n\n\tconst accuracy = Number(fix.accuracy ?? Number.POSITIVE_INFINITY)\n\tif (accuracy > GPS_POOR_ACCURACY_M) {\n\t\tconst rounded = Number.isFinite(accuracy) ? Math.round(accuracy) : '?'\n\t\treturn {\n\t\t\tquality: 'poor',\n\t\t\tsource: 'sensor',\n\t\t\twarning: t('nextcloud-vue', 'Location imprecise (±{m}m). Wait for a better signal, or add the address by hand.', { m: rounded }),\n\t\t}\n\t}\n\n\treturn { quality: 'good', source: 'sensor', warning: null }\n}\n\n/**\n * Read a checklist template into the leaf's flat item contract.\n *\n * A consuming app's template does not have to carry `items[]` with\n * `questionId` / `text` / `type`. Its `offlineConfig` names where those live:\n * `sectionsField` when the items sit inside ordered sections, and the\n * `item*Field` keys for the item's key, text and type. `itemTypeMap` translates\n * the app's own type words onto the leaf's (`yes_no`, `photo_required`, or any\n * other word, which renders as free text). `photoRequiredField` and\n * `photoRequiredValue` mark an item whose answer also needs a photo.\n *\n * The template's own keys are kept, so the id and version stay readable; only\n * `items` is replaced by the normalised list.\n *\n * @param {object|null} template The template as cached.\n * @param {object} [config] The leaf's offline config.\n *\n * @return {object|null} The template with flat `items[]`, or null.\n */\nexport function normaliseChecklistTemplate(template, config = {}) {\n\tif (template === null || typeof template !== 'object') {\n\t\treturn null\n\t}\n\n\tconst keyField = config.itemKeyField || 'questionId'\n\tconst textField = config.itemTextField || 'text'\n\tconst typeField = config.itemTypeField || 'type'\n\tconst typeMap = (config.itemTypeMap && typeof config.itemTypeMap === 'object') ? config.itemTypeMap : {}\n\n\tconst sections = config.sectionsField ? template[config.sectionsField] : null\n\tconst raw = Array.isArray(sections)\n\t\t? sections.flatMap((section) => (Array.isArray(section?.items) ? section.items : []))\n\t\t: (Array.isArray(template.items) ? template.items : [])\n\n\tconst items = []\n\tfor (const item of raw) {\n\t\tif (item === null || typeof item !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst questionId = String(item[keyField] ?? '')\n\t\tif (questionId === '') {\n\t\t\tcontinue\n\t\t}\n\t\tconst ownType = item[typeField]\n\t\tconst photoRequired = Boolean(config.photoRequiredField)\n\t\t\t&& item[config.photoRequiredField] === config.photoRequiredValue\n\t\titems.push({\n\t\t\t...item,\n\t\t\tquestionId,\n\t\t\ttext: item[textField] ?? item.text ?? questionId,\n\t\t\ttype: typeMap[ownType] ?? ownType ?? 'text',\n\t\t\trequired: item.required === true,\n\t\t\tphotoRequired,\n\t\t})\n\t}\n\n\treturn { ...template, items }\n}\n\n/**\n * Validate a set of checklist answers against the template's required items.\n *\n * A `required` item must have a non-empty answer; a `photo_required` item, and\n * any item flagged `photoRequired` by {@link normaliseChecklistTemplate}, must\n * have at least one evidence reference. Returns the per-question\n * blocking errors so the UI can prevent save (and the engine never queues an\n * invalid result).\n *\n * @param {object} template The checklist template (`items[]`).\n * @param {Array} template.items Items (`questionId`, `type`, `required`).\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ valid: boolean, errors: Array<{ questionId: string, message: string }> }}\n */\nexport function validateChecklistAnswers(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tconst errors = []\n\n\tfor (const item of items) {\n\t\tif (item?.required !== true && item?.photoRequired !== true) {\n\t\t\tcontinue\n\t\t}\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst answer = entry.answer\n\t\tconst evidenceRefs = Array.isArray(entry.evidenceRefs) ? entry.evidenceRefs : []\n\n\t\tif (item.type === 'photo_required') {\n\t\t\tif (evidenceRefs.length === 0) {\n\t\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (item.photoRequired === true && evidenceRefs.length === 0) {\n\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })\n\t\t\tcontinue\n\t\t}\n\n\t\tif (item.required === true && (answer === undefined || answer === null || String(answer).trim() === '')) {\n\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'This question is required') })\n\t\t}\n\t}\n\n\treturn { valid: errors.length === 0, errors }\n}\n\n/**\n * Count completed items in a checklist for the N/M progress indicator.\n *\n * @param {object} template The checklist template.\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ done: number, total: number }} Completed and total item counts.\n */\nexport function checklistProgress(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tlet done = 0\n\n\tfor (const item of items) {\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''\n\t\tconst hasEvidence = Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0\n\t\tif (item.type === 'photo_required' ? hasEvidence : hasAnswer) {\n\t\t\tdone += 1\n\t\t}\n\t}\n\n\treturn { done, total: items.length }\n}\n\n/**\n * Human-readable sync-status copy for the green/amber/red indicator.\n *\n * @param {number} pendingCount The number of pending operations.\n * @param {boolean} online Whether the device is online.\n * @param {number} [stuckCount] How many will not send again without somebody acting.\n *\n * @return {{ tone: ('success'|'warning'|'error'), text: string }} Indicator state.\n */\nexport function syncIndicator(pendingCount, online, stuckCount = 0) {\n\t// 🔴 STUCK WORK IS NAMED BEFORE ANYTHING ELSE, INCLUDING BEFORE BEING\n\t// OFFLINE. `pendingCount` counts pending, conflict and syncing; a `failed`\n\t// operation is in none of them, so a device holding a stranded inspection\n\t// used to reach the last line here and report \"All changes synced\" in\n\t// green. The one surface that could have told an inspector their morning\n\t// had not left the device said the opposite.\n\t//\n\t// Being offline is temporary and expected in the field. Being stuck is\n\t// neither, so it outranks it.\n\tif (stuckCount > 0) {\n\t\treturn {\n\t\t\ttone: 'error',\n\t\t\ttext: t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{n} changes are stuck and will not send on their own',\n\t\t\t\t{ n: stuckCount },\n\t\t\t),\n\t\t}\n\t}\n\n\t// Being offline is not a fault. A field device spends its day out of\n\t// signal by design, and red said something had gone wrong every hour of\n\t// every working day, which is how a palette stops being read at all. Red\n\t// is now reserved for the one state that needs somebody: stuck work.\n\tif (online === false) {\n\t\treturn {\n\t\t\ttone: 'warning',\n\t\t\ttext: t('nextcloud-vue', 'No connection. {n} changes are waiting and will send when you are back.', { n: pendingCount }),\n\t\t}\n\t}\n\tif (pendingCount > 0) {\n\t\treturn { tone: 'warning', text: t('nextcloud-vue', '{n} changes waiting for sync', { n: pendingCount }) }\n\t}\n\treturn { tone: 'success', text: t('nextcloud-vue', 'All changes synced') }\n}\n"],"names":["t"],"mappings":"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"}
|
|
@@ -2,7 +2,8 @@ export { CONFLICT_RECORD_PREFIX, __resetDbForTests, __setDexie, applyConflictRes
|
|
|
2
2
|
export { BACKOFF_SCHEDULE_MS, MAX_ATTEMPTS, TERMINAL_STATUSES, canRetry, classifyConflict, delayForAttempt, diffVersions, isConflictRetryable, nextState, orderForReplay, resolveConflictChoice } from './syncQueueEngine.js';
|
|
3
3
|
export { drainQueue, replayOperation } from './syncReplayService.js';
|
|
4
4
|
export { buildPlanningQuery, fetchPlanning, fetchReferences, toDayString } from './planningFetch.js';
|
|
5
|
-
export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, syncIndicator, validateChecklistAnswers } from './fieldCollectionHelpers.js';
|
|
5
|
+
export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers } from './fieldCollectionHelpers.js';
|
|
6
|
+
export { buildChecklistSubmission, fillEndpoint } from './checklistSubmission.js';
|
|
6
7
|
export { CACHE_PREFIX, NEVER_CACHED, cacheNameFor, isCacheable, respondTo, staleCacheNames } from '../../offline/serviceWorker.js';
|
|
7
8
|
export { registerOfflineWorker, unregisterOfflineWorker } from '../../offline/registerOfflineWorker.js';
|
|
8
9
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/integrations/offline/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Generic offline data-collection core — public surface.\n *\n * Reusable offline-sync infrastructure extracted from procest's\n * `mobiel-inspectie-offline` PWA so any Conduction app can collect field data\n * offline by registering a checklist/planning schema instead of re-implementing\n * the IndexedDB cache, mutation queue, replay loop and sync-state indicator.\n *\n * @module integrations/offline\n */\n\nexport {\n\t__resetDbForTests,\n\t__setDexie,\n\tapplyConflictResolution,\n\tcacheKey,\n\tCONFLICT_RECORD_PREFIX,\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetDb,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tlistQueue,\n\topenDb,\n\trecordConflict,\n\trequeueOperation,\n\tresolveDeviceId,\n\tstorePlanning,\n} from './offlineDb.js'\n\nexport {\n\tBACKOFF_SCHEDULE_MS,\n\tcanRetry,\n\tclassifyConflict,\n\tdelayForAttempt,\n\tdiffVersions,\n\tisConflictRetryable,\n\tMAX_ATTEMPTS,\n\tnextState,\n\torderForReplay,\n\tresolveConflictChoice,\n\tTERMINAL_STATUSES,\n} from './syncQueueEngine.js'\n\nexport { drainQueue, replayOperation } from './syncReplayService.js'\n\nexport {\n\tbuildPlanningQuery,\n\tfetchPlanning,\n\tfetchReferences,\n\ttoDayString,\n} from './planningFetch.js'\n\nexport {\n\tchecklistProgress,\n\tclassifyGps,\n\tGPS_POOR_ACCURACY_M,\n\tsyncIndicator,\n\tvalidateChecklistAnswers,\n} from './fieldCollectionHelpers.js'\n\nexport {\n\tCACHE_PREFIX,\n\tcacheNameFor,\n\tisCacheable,\n\tNEVER_CACHED,\n\trespondTo,\n\tstaleCacheNames,\n} from '../../offline/serviceWorker.js'\n\nexport {\n\tregisterOfflineWorker,\n\tunregisterOfflineWorker,\n} from '../../offline/registerOfflineWorker.js'\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/integrations/offline/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Generic offline data-collection core — public surface.\n *\n * Reusable offline-sync infrastructure extracted from procest's\n * `mobiel-inspectie-offline` PWA so any Conduction app can collect field data\n * offline by registering a checklist/planning schema instead of re-implementing\n * the IndexedDB cache, mutation queue, replay loop and sync-state indicator.\n *\n * @module integrations/offline\n */\n\nexport {\n\t__resetDbForTests,\n\t__setDexie,\n\tapplyConflictResolution,\n\tcacheKey,\n\tCONFLICT_RECORD_PREFIX,\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetDb,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tlistQueue,\n\topenDb,\n\trecordConflict,\n\trequeueOperation,\n\tresolveDeviceId,\n\tstorePlanning,\n} from './offlineDb.js'\n\nexport {\n\tBACKOFF_SCHEDULE_MS,\n\tcanRetry,\n\tclassifyConflict,\n\tdelayForAttempt,\n\tdiffVersions,\n\tisConflictRetryable,\n\tMAX_ATTEMPTS,\n\tnextState,\n\torderForReplay,\n\tresolveConflictChoice,\n\tTERMINAL_STATUSES,\n} from './syncQueueEngine.js'\n\nexport { drainQueue, replayOperation } from './syncReplayService.js'\n\nexport {\n\tbuildPlanningQuery,\n\tfetchPlanning,\n\tfetchReferences,\n\ttoDayString,\n} from './planningFetch.js'\n\nexport {\n\tchecklistProgress,\n\tclassifyGps,\n\tGPS_POOR_ACCURACY_M,\n\tnormaliseChecklistTemplate,\n\tsyncIndicator,\n\tvalidateChecklistAnswers,\n} from './fieldCollectionHelpers.js'\n\nexport { buildChecklistSubmission, fillEndpoint } from './checklistSubmission.js'\n\nexport {\n\tCACHE_PREFIX,\n\tcacheNameFor,\n\tisCacheable,\n\tNEVER_CACHED,\n\trespondTo,\n\tstaleCacheNames,\n} from '../../offline/serviceWorker.js'\n\nexport {\n\tregisterOfflineWorker,\n\tunregisterOfflineWorker,\n} from '../../offline/registerOfflineWorker.js'\n"],"names":[],"mappings":";;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
|
|
@@ -258,10 +258,12 @@ async function getPlanningMeta(register, schema, collection = 'planning') {
|
|
|
258
258
|
*
|
|
259
259
|
* @param {object} operation The mutation operation.
|
|
260
260
|
* @param {string} operation.deviceId Owning device id (IDOR scope).
|
|
261
|
-
* @param {string} operation.operationType create / update / delete.
|
|
261
|
+
* @param {string} operation.operationType create / update / delete / submit.
|
|
262
262
|
* @param {string} operation.register Target register slug or id.
|
|
263
263
|
* @param {string} operation.schema Target schema slug or id.
|
|
264
264
|
* @param {string} [operation.targetId] Existing object id (update/delete).
|
|
265
|
+
* @param {string} [operation.endpoint] Path under the Nextcloud root a
|
|
266
|
+
* `submit` row is POSTed to.
|
|
265
267
|
* @param {object} [operation.payload] The object body (create/update).
|
|
266
268
|
*
|
|
267
269
|
* @return {Promise<string>} The queued operation id.
|
|
@@ -276,6 +278,9 @@ async function enqueueMutation(operation) {
|
|
|
276
278
|
register: operation.register,
|
|
277
279
|
schema: operation.schema,
|
|
278
280
|
targetId: operation.targetId ?? null,
|
|
281
|
+
// A `submit` operation replays to an app's own endpoint instead of the
|
|
282
|
+
// object API (see checklistSubmission.js); every other row has none.
|
|
283
|
+
endpoint: operation.endpoint ?? null,
|
|
279
284
|
payload: operation.payload ?? {},
|
|
280
285
|
queuedAt: operation.queuedAt || new Date().toISOString(),
|
|
281
286
|
attemptCount: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offlineDb.js","sources":["../../../../src/integrations/offline/offlineDb.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Generic IndexedDB (Dexie.js) store for the offline data-collection core.\n *\n * Unlike procest's bespoke six-table schema, this store is keyed by\n * `register` + `schema` so ANY consuming app can cache its planned objects and\n * queue mutations through one shared database. Three generic tables:\n *\n * - `objectCache` — cached OpenRegister objects (the planned items plus any\n * reference data, e.g. checklist templates). Each row is a\n * `{ key, register, schema, collection, objectId, object }`\n * envelope so multiple apps/schemas never collide.\n * - `mutationQueue` — the offline sync queue: one row per pending create /\n * update / delete to be replayed against the OR object API.\n * - `meta` — singleton rows (per-collection planning state / expiry).\n *\n * Dexie is imported here only — the pure engine (`syncQueueEngine.js`) and the\n * pure helpers carry the unit-test surface and import nothing from this module,\n * so they stay importable in a Node environment with no IndexedDB. Tests for\n * THIS module inject a fake-indexeddb-backed Dexie via `__setDexie`.\n *\n * Dexie is loaded LAZILY, on the first call that needs the database\n * (`openDb()`), and never at import time. This module is reachable from the\n * package root: `src/index.js` exports `offlineCollection`, which reaches\n * `integrations/builtin/field-inspection`, which reaches here. With a static\n * import every consuming app evaluated Dexie on every page, and Dexie throws\n * \"Two different versions of Dexie loaded in the same app\" at module init\n * when a second copy at another version is already on the page. Nextcloud\n * loads several apps' every-page bundles at once (openregister's\n * integration-global among them), so one app a patch release ahead of\n * another blanked whole apps that never touch the offline core. Loaded on\n * demand, Dexie is evaluated only on a page that actually opens the offline\n * database.\n *\n * Dexie stays a REQUIRED peer dependency. The consumer's bundler still\n * resolves `import('dexie')` at BUILD time (it becomes a separate chunk), so\n * an app without it installed fails its build with \"Module not found\". The\n * dependency is required; loading it is not.\n *\n * @module integrations/offline/offlineDb\n */\n\nimport { resolveConflictChoice } from './syncQueueEngine.js'\n\nconst DB_NAME = 'conduction-offline-collection'\n\nlet dbInstance = null\n// Null until `openDb()` loads Dexie, or a test injects it via `__setDexie`.\nlet DexieCtor = null\n\n/**\n * Build the composite cache key for an object.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug or id.\n * @param {string} collection Logical collection name (e.g. 'planning', 'templates').\n * @param {string} objectId The object id.\n *\n * @return {string} A stable composite key.\n */\nexport function cacheKey(register, schema, collection, objectId) {\n\treturn `${register}::${schema}::${collection}::${objectId}`\n}\n\n/**\n * Set the Dexie constructor explicitly (test seam / SSR override).\n *\n * Production code never calls this: `openDb()` lazy-imports `dexie`. Tests pass\n * a fake-indexeddb-backed Dexie so the store can be exercised in Node.\n *\n * @param {new (name: string) => object} ctor The Dexie class.\n *\n * @return {void}\n */\nexport function __setDexie(ctor) {\n\tDexieCtor = ctor\n\tdbInstance = null\n}\n\n/**\n * Load Dexie on first use, then open (and memoise) the database.\n *\n * Every function in the offline core goes through this, so Dexie is only\n * evaluated on a page that actually uses the offline database. A Dexie\n * injected with `__setDexie` is used as is and nothing is imported.\n *\n * @return {Promise<object>} The opened database handle.\n */\nexport async function openDb() {\n\tif (DexieCtor === null) {\n\t\tconst mod = await import('dexie')\n\t\tDexieCtor = mod.default ?? mod.Dexie\n\t}\n\treturn getDb()\n}\n\n/**\n * The memoised database handle, synchronously.\n *\n * Works once Dexie is loaded: after any `openDb()` call, or after a test\n * injected it with `__setDexie`. Call `await openDb()` when neither is\n * certain; that is what every function in this module does.\n *\n * @return {object} The opened database handle.\n *\n * @throws {Error} When Dexie has not been loaded yet.\n */\nexport function getDb() {\n\tif (dbInstance !== null) {\n\t\treturn dbInstance\n\t}\n\tif (DexieCtor === null) {\n\t\tthrow new Error('The offline database is not open yet: call `await openDb()` first. Dexie loads on first use, so a page that never opens the offline database never evaluates it.')\n\t}\n\n\tconst db = new DexieCtor(DB_NAME)\n\tdb.version(1).stores({\n\t\t// Composite key + indexes so a single DB serves every app/schema.\n\t\tobjectCache: 'key, register, schema, collection, objectId',\n\t\tmutationQueue: 'id, deviceId, status, queuedAt, register, schema',\n\t\tmeta: 'key',\n\t})\n\n\tdbInstance = db\n\treturn db\n}\n\n/**\n * Persist a downloaded planning payload into the local cache atomically.\n *\n * Stores the planned items as one collection and any reference data\n * (e.g. checklist templates) as a second collection, plus a per-collection\n * planning meta row recording the offline-ready expiry. Every write happens in\n * one Dexie transaction so a connection drop mid-write never leaves the cache\n * half-populated.\n *\n * @param {object} args Planning storage arguments.\n * @param {string} args.register Register slug or id.\n * @param {string} args.schema Schema slug of the planned items.\n * @param {Array} args.items The planned objects to cache.\n * @param {Array} [args.references] Optional reference objects (templates).\n * @param {string} [args.referenceSchema] Schema slug of the references.\n * @param {string} [args.collection] Planning collection name (default 'planning').\n * @param {object} [args.manifest] Optional download manifest.\n * @param {number} [args.ttlMs] Offline lifetime (default 24h).\n *\n * @return {Promise<void>} Resolves when the planning is stored.\n */\nexport async function storePlanning({\n\tregister,\n\tschema,\n\titems = [],\n\treferences = [],\n\treferenceSchema = null,\n\tcollection = 'planning',\n\tmanifest = null,\n\tttlMs = 24 * 60 * 60 * 1000,\n}) {\n\tconst db = await openDb()\n\tconst planned = Array.isArray(items) ? items : []\n\tconst refs = Array.isArray(references) ? references : []\n\tconst refSchema = referenceSchema || schema\n\tconst expiresAt = new Date(Date.now() + ttlMs).toISOString()\n\n\tconst objectIdOf = (obj) => String(obj?.id ?? obj?.['@self']?.id ?? obj?.uuid ?? '')\n\n\tconst cacheRows = planned\n\t\t.filter((obj) => objectIdOf(obj) !== '')\n\t\t.map((obj) => ({\n\t\t\tkey: cacheKey(register, schema, collection, objectIdOf(obj)),\n\t\t\tregister,\n\t\t\tschema,\n\t\t\tcollection,\n\t\t\tobjectId: objectIdOf(obj),\n\t\t\tobject: obj,\n\t\t}))\n\n\tconst refRows = refs\n\t\t.filter((obj) => objectIdOf(obj) !== '')\n\t\t.map((obj) => ({\n\t\t\tkey: cacheKey(register, refSchema, 'references', objectIdOf(obj)),\n\t\t\tregister,\n\t\t\tschema: refSchema,\n\t\t\tcollection: 'references',\n\t\t\tobjectId: objectIdOf(obj),\n\t\t\tobject: obj,\n\t\t}))\n\n\tawait db.transaction('rw', db.objectCache, db.meta, async () => {\n\t\tawait db.objectCache.bulkPut(cacheRows.concat(refRows))\n\t\tawait db.meta.put({\n\t\t\tkey: `planning::${register}::${schema}::${collection}`,\n\t\t\tregister,\n\t\t\tschema,\n\t\t\tcollection,\n\t\t\tstatus: 'ready_offline',\n\t\t\texpiresAt,\n\t\t\tsyncedAt: new Date().toISOString(),\n\t\t\tcount: cacheRows.length,\n\t\t\tmanifest,\n\t\t})\n\t})\n}\n\n/**\n * Read the cached planned items for a register/schema/collection.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug.\n * @param {string} [collection] Collection name (default 'planning').\n *\n * @return {Promise<Array>} The cached objects (unwrapped from the envelope).\n */\nexport async function getPlannedItems(register, schema, collection = 'planning') {\n\tconst db = await openDb()\n\tconst rows = await db.objectCache\n\t\t.where('collection').equals(collection)\n\t\t.and((row) => row.register === register && row.schema === schema)\n\t\t.toArray()\n\treturn rows.map((row) => row.object)\n}\n\n/**\n * Read a single cached object by id.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug.\n * @param {string} collection Collection name.\n * @param {string} objectId The object id.\n *\n * @return {Promise<object|null>} The cached object, or null.\n */\nexport async function getCachedObject(register, schema, collection, objectId) {\n\tconst db = await openDb()\n\tconst row = await db.objectCache.get(cacheKey(register, schema, collection, objectId))\n\treturn row ? row.object : null\n}\n\n/**\n * Read the planning meta row (or null when never synced) for a collection.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug.\n * @param {string} [collection] Collection name (default 'planning').\n *\n * @return {Promise<object|null>} The planning meta row.\n */\nexport async function getPlanningMeta(register, schema, collection = 'planning') {\n\tconst db = await openDb()\n\treturn (await db.meta.get(`planning::${register}::${schema}::${collection}`)) ?? null\n}\n\n/**\n * Enqueue one offline mutation against the OR object API.\n *\n * @param {object} operation The mutation operation.\n * @param {string} operation.deviceId Owning device id (IDOR scope).\n * @param {string} operation.operationType create / update / delete.\n * @param {string} operation.register Target register slug or id.\n * @param {string} operation.schema Target schema slug or id.\n * @param {string} [operation.targetId] Existing object id (update/delete).\n * @param {object} [operation.payload] The object body (create/update).\n *\n * @return {Promise<string>} The queued operation id.\n */\nexport async function enqueueMutation(operation) {\n\tconst db = await openDb()\n\tconst id = operation.id || `op-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n\tawait db.mutationQueue.put({\n\t\tid,\n\t\tdeviceId: operation.deviceId,\n\t\toperationType: operation.operationType,\n\t\tregister: operation.register,\n\t\tschema: operation.schema,\n\t\ttargetId: operation.targetId ?? null,\n\t\tpayload: operation.payload ?? {},\n\t\tqueuedAt: operation.queuedAt || new Date().toISOString(),\n\t\tattemptCount: 0,\n\t\tstatus: 'pending',\n\t})\n\treturn id\n}\n\n/**\n * Count all queue operations not yet synced — drives the pending badge.\n *\n * @param {string} [deviceId] Optional device scope.\n *\n * @return {Promise<number>} The number of pending/conflict/syncing operations.\n */\nexport async function countPending(deviceId) {\n\tconst db = await openDb()\n\tlet collection = db.mutationQueue.where('status').anyOf(['pending', 'conflict', 'syncing'])\n\tif (typeof deviceId === 'string' && deviceId !== '') {\n\t\tcollection = collection.and((op) => op.deviceId === deviceId)\n\t}\n\treturn await collection.count()\n}\n\n/**\n * Every queued operation, in the order they will replay.\n *\n * 🔴 INCLUDING THE FAILED ONES, WHICH IS THE POINT. `countPending()` counts\n * `pending`, `conflict` and `syncing` and leaves `failed` out, so an operation\n * that exhausted its retries or lost its permission drops out of the badge\n * entirely. The row is still on the device; nothing tells anybody. An\n * inspection a citizen stood beside somebody to give is then stranded in one\n * browser's IndexedDB with no reader, which is worse than having refused it at\n * the door.\n *\n * FIFO by `queuedAt`, the same order the replay loop uses, so the list reads\n * as what will happen next rather than as a set.\n *\n * @param {string} [deviceId] Optional device scope.\n *\n * @return {Promise<object[]>} Every operation, oldest first.\n */\nexport async function listQueue(deviceId) {\n\tconst db = await openDb()\n\tlet collection = db.mutationQueue.orderBy('queuedAt')\n\tif (typeof deviceId === 'string' && deviceId !== '') {\n\t\tcollection = collection.and((op) => op.deviceId === deviceId)\n\t}\n\n\treturn await collection.toArray()\n}\n\n/**\n * How many operations will not replay again without somebody acting.\n *\n * Counted apart from `countPending()` rather than folded into it: \"12 waiting\"\n * and \"12 waiting, 1 stuck\" are different sentences, and a surface that adds\n * them together says neither. Nothing is ever dropped to make this number\n * smaller.\n *\n * @param {string} [deviceId] Optional device scope.\n *\n * @return {Promise<number>} The number of failed operations.\n */\nexport async function countStuck(deviceId) {\n\tconst db = await openDb()\n\tlet collection = db.mutationQueue.where('status').equals('failed')\n\tif (typeof deviceId === 'string' && deviceId !== '') {\n\t\tcollection = collection.and((op) => op.deviceId === deviceId)\n\t}\n\n\treturn await collection.count()\n}\n\n/**\n * Put a failed operation back in the queue, by hand.\n *\n * 🔴 BY HAND, AND NEVER AUTOMATICALLY. `failed` is terminal for the replay\n * loop on purpose: an operation the server refused five times will be refused\n * a sixth, and a queue that retries forever is a queue that never drains. But\n * terminal must not mean forgotten, so somebody who has fixed whatever the\n * server was complaining about can ask for it again, and the attempt count\n * starts over.\n *\n * It does NOT reset a `permission_lost` row. The right to write is gone and\n * retrying cannot bring it back; offering the button would be offering a\n * gesture that fails every time.\n *\n * @param {string} operationId The operation to retry.\n *\n * @return {Promise<boolean>} True when it was re-queued.\n */\nexport async function requeueOperation(operationId) {\n\tconst db = await openDb()\n\tconst operation = await db.mutationQueue.get(operationId)\n\tif (!operation || operation.status !== 'failed') {\n\t\treturn false\n\t}\n\n\tif (operation.lastError === 'permission_lost') {\n\t\treturn false\n\t}\n\n\tawait db.mutationQueue.update(operationId, {\n\t\tstatus: 'pending',\n\t\tattemptCount: 0,\n\t\tlastError: null,\n\t})\n\n\treturn true\n}\n\n/**\n * The prefix of a conflict-record operation's id.\n *\n * Derived from the id of the operation that conflicted, so the record is\n * idempotent by construction: a second drain of the same conflicting row\n * computes the same id and finds it already there.\n */\nexport const CONFLICT_RECORD_PREFIX = 'conflict-'\n\n/**\n * Write a classified conflict to the register as an object of its own.\n *\n * A conflict kept only in this browser's IndexedDB is invisible to everyone who\n * could act on it: the colleague whose edit collided, the supervisor who has to\n * decide, the audit that has to show the decision was taken at all. So the\n * conflict becomes an OpenRegister object, carrying both versions and a\n * reference back to the queue row.\n *\n * The write goes through the queue rather than straight to the network, because\n * the moment a conflict is classified is exactly the moment the connection is\n * unreliable. It replays with everything else.\n *\n * It is written ONCE. The record's id is derived from the conflicting\n * operation's id, so a second drain finds it and leaves it alone rather than\n * filing the same collision twice under two ids.\n *\n * With no `conflictSchema` configured nothing is written, and the queue row is\n * marked so the list can say plainly that this conflict is local to this\n * device. Silently keeping it would be the worse half of both options.\n *\n * @param {object} args Recording arguments.\n * @param {object} args.operation The queue row that conflicted.\n * @param {string} args.conflictType concurrent_edit / deleted_remote / permission_lost.\n * @param {object|null} [args.serverObject] The server's version, when it sent one.\n * @param {string} [args.register] Register to write the record to (default: the operation's).\n * @param {string} [args.conflictSchema] Schema holding conflict records.\n *\n * @return {Promise<string|null>} The record operation id, or null when nothing was written.\n */\nexport async function recordConflict({\n\toperation,\n\tconflictType,\n\tserverObject = null,\n\tregister = '',\n\tconflictSchema = '',\n}) {\n\tconst db = await openDb()\n\n\tif (typeof conflictSchema !== 'string' || conflictSchema === '') {\n\t\tawait db.mutationQueue.update(operation.id, { conflictScope: 'local', conflictType })\n\t\treturn null\n\t}\n\n\tconst recordId = `${CONFLICT_RECORD_PREFIX}${operation.id}`\n\tconst existing = await db.mutationQueue.get(recordId)\n\tif (existing !== undefined) {\n\t\treturn recordId\n\t}\n\n\tawait enqueueMutation({\n\t\tid: recordId,\n\t\tdeviceId: operation.deviceId,\n\t\toperationType: 'create',\n\t\tregister: register || operation.register,\n\t\tschema: conflictSchema,\n\t\tpayload: {\n\t\t\tqueueOperationId: operation.id,\n\t\t\tconflictType,\n\t\t\tdeviceId: operation.deviceId,\n\t\t\ttargetRegister: operation.register,\n\t\t\ttargetSchema: operation.schema,\n\t\t\ttargetId: operation.targetId ?? null,\n\t\t\tclientVersion: operation.payload ?? {},\n\t\t\tserverVersion: serverObject ?? null,\n\t\t\tdetectedAt: new Date().toISOString(),\n\t\t},\n\t})\n\n\tawait db.mutationQueue.update(operation.id, {\n\t\tconflictScope: 'register',\n\t\tconflictType,\n\t\tconflictRecordId: recordId,\n\t\tserverVersion: serverObject ?? null,\n\t})\n\n\treturn recordId\n}\n\n/**\n * Write the resolution onto the conflict object, wherever it currently is.\n *\n * Still queued: its payload is amended in place, so it reaches the register\n * already carrying the decision. Already sent: a follow-up `update` is queued\n * against the id the server gave it. Never sent at all (no schema configured):\n * there is nothing to write to, and the queue row keeps the decision.\n *\n * @param {object} db The Dexie handle.\n * @param {object} operation The conflicting queue row.\n * @param {string} resolution The choice made.\n * @param {string} resolvedBy The uid settling it.\n *\n * @return {Promise<void>} Nothing.\n */\nasync function recordResolutionOnConflictObject(db, operation, resolution, resolvedBy) {\n\tconst recordId = operation.conflictRecordId\n\tif (typeof recordId !== 'string' || recordId === '') {\n\t\treturn\n\t}\n\n\tconst record = await db.mutationQueue.get(recordId)\n\tif (record === undefined) {\n\t\treturn\n\t}\n\n\tconst decision = { resolution, resolvedBy, resolvedAt: new Date().toISOString() }\n\n\tif (record.status !== 'synced') {\n\t\tawait db.mutationQueue.update(recordId, { payload: { ...(record.payload ?? {}), ...decision } })\n\t\treturn\n\t}\n\n\tconst serverId = String(record.serverObjectId ?? '')\n\tif (serverId === '') {\n\t\treturn\n\t}\n\n\tawait enqueueMutation({\n\t\tid: `${recordId}-resolution`,\n\t\tdeviceId: record.deviceId,\n\t\toperationType: 'update',\n\t\tregister: record.register,\n\t\tschema: record.schema,\n\t\ttargetId: serverId,\n\t\tpayload: { ...(record.payload ?? {}), ...decision },\n\t})\n}\n\n/**\n * Apply a person's conflict resolution to the queue and to the record.\n *\n * The decision itself is the pure engine's (`resolveConflictChoice`); this puts\n * the result where it has to live. Three things happen, and the third is the\n * one that is easy to forget:\n *\n * 1. The queue row takes the engine's patch, so keep-mine replays and\n * keep-theirs stops.\n * 2. Keep theirs leaves the local cache holding the SERVER's version. Marking\n * the row synced while the device still shows the abandoned local text is\n * how somebody reads their own discarded answer as the current record.\n * 3. The resolution, who made it and when are written onto the conflict\n * object. A conflict record that says a collision happened and not how it\n * was settled cannot answer the only question anybody asks later.\n *\n * @param {object} args Resolution arguments.\n * @param {string} args.operationId The conflicting queue row.\n * @param {string} args.resolution client_wins / server_wins / manual_merge.\n * @param {object} [args.mergedPayload] The merged body, for a manual merge.\n * @param {string} [args.resolvedBy] The uid settling it.\n *\n * @return {Promise<boolean>} True when a conflict was resolved.\n */\nexport async function applyConflictResolution({\n\toperationId,\n\tresolution,\n\tmergedPayload = null,\n\tresolvedBy = '',\n}) {\n\tconst db = await openDb()\n\tconst operation = await db.mutationQueue.get(operationId)\n\n\tif (operation === undefined || operation.status !== 'conflict') {\n\t\treturn false\n\t}\n\n\tconst { patch } = resolveConflictChoice(resolution, mergedPayload)\n\tawait db.mutationQueue.update(operationId, {\n\t\t...patch,\n\t\tresolution,\n\t\tresolvedBy,\n\t\tresolvedAt: new Date().toISOString(),\n\t})\n\n\tif (resolution === 'server_wins' && operation.serverVersion !== null && operation.serverVersion !== undefined) {\n\t\tconst objectId = String(operation.targetId ?? '')\n\t\tif (objectId !== '') {\n\t\t\tawait db.objectCache.put({\n\t\t\t\tkey: cacheKey(operation.register, operation.schema, 'planning', objectId),\n\t\t\t\tregister: operation.register,\n\t\t\t\tschema: operation.schema,\n\t\t\t\tcollection: 'planning',\n\t\t\t\tobjectId,\n\t\t\t\tobject: operation.serverVersion,\n\t\t\t})\n\t\t}\n\t}\n\n\tawait recordResolutionOnConflictObject(db, operation, resolution, resolvedBy)\n\n\treturn true\n}\n\n/**\n * Resolve a stable per-device id, persisted in localStorage.\n *\n * The device id scopes the mutation queue so the server can re-authorize that\n * only the owning device pushes its own queued operations (IDOR is enforced\n * server-side, never client-asserted).\n *\n * @param {object} [storage] localStorage-like object (test seam).\n *\n * @return {string} The device id.\n */\nexport function resolveDeviceId(storage) {\n\tconst store = storage || (typeof window !== 'undefined' ? window.localStorage : null)\n\tconst STORAGE_KEY = 'cn-offline-device-id'\n\tif (store === null || store === undefined) {\n\t\treturn `device-${Math.random().toString(36).slice(2, 10)}`\n\t}\n\tlet id = store.getItem(STORAGE_KEY)\n\tif (!id) {\n\t\tid = `device-${Math.random().toString(36).slice(2, 10)}`\n\t\tstore.setItem(STORAGE_KEY, id)\n\t}\n\treturn id\n}\n\n/**\n * Reset the memoised handle — test/teardown only.\n *\n * @return {void}\n */\nexport function __resetDbForTests() {\n\tdbInstance = null\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"offlineDb.js","sources":["../../../../src/integrations/offline/offlineDb.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Generic IndexedDB (Dexie.js) store for the offline data-collection core.\n *\n * Unlike procest's bespoke six-table schema, this store is keyed by\n * `register` + `schema` so ANY consuming app can cache its planned objects and\n * queue mutations through one shared database. Three generic tables:\n *\n * - `objectCache` — cached OpenRegister objects (the planned items plus any\n * reference data, e.g. checklist templates). Each row is a\n * `{ key, register, schema, collection, objectId, object }`\n * envelope so multiple apps/schemas never collide.\n * - `mutationQueue` — the offline sync queue: one row per pending create /\n * update / delete to be replayed against the OR object API.\n * - `meta` — singleton rows (per-collection planning state / expiry).\n *\n * Dexie is imported here only — the pure engine (`syncQueueEngine.js`) and the\n * pure helpers carry the unit-test surface and import nothing from this module,\n * so they stay importable in a Node environment with no IndexedDB. Tests for\n * THIS module inject a fake-indexeddb-backed Dexie via `__setDexie`.\n *\n * Dexie is loaded LAZILY, on the first call that needs the database\n * (`openDb()`), and never at import time. This module is reachable from the\n * package root: `src/index.js` exports `offlineCollection`, which reaches\n * `integrations/builtin/field-inspection`, which reaches here. With a static\n * import every consuming app evaluated Dexie on every page, and Dexie throws\n * \"Two different versions of Dexie loaded in the same app\" at module init\n * when a second copy at another version is already on the page. Nextcloud\n * loads several apps' every-page bundles at once (openregister's\n * integration-global among them), so one app a patch release ahead of\n * another blanked whole apps that never touch the offline core. Loaded on\n * demand, Dexie is evaluated only on a page that actually opens the offline\n * database.\n *\n * Dexie stays a REQUIRED peer dependency. The consumer's bundler still\n * resolves `import('dexie')` at BUILD time (it becomes a separate chunk), so\n * an app without it installed fails its build with \"Module not found\". The\n * dependency is required; loading it is not.\n *\n * @module integrations/offline/offlineDb\n */\n\nimport { resolveConflictChoice } from './syncQueueEngine.js'\n\nconst DB_NAME = 'conduction-offline-collection'\n\nlet dbInstance = null\n// Null until `openDb()` loads Dexie, or a test injects it via `__setDexie`.\nlet DexieCtor = null\n\n/**\n * Build the composite cache key for an object.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug or id.\n * @param {string} collection Logical collection name (e.g. 'planning', 'templates').\n * @param {string} objectId The object id.\n *\n * @return {string} A stable composite key.\n */\nexport function cacheKey(register, schema, collection, objectId) {\n\treturn `${register}::${schema}::${collection}::${objectId}`\n}\n\n/**\n * Set the Dexie constructor explicitly (test seam / SSR override).\n *\n * Production code never calls this: `openDb()` lazy-imports `dexie`. Tests pass\n * a fake-indexeddb-backed Dexie so the store can be exercised in Node.\n *\n * @param {new (name: string) => object} ctor The Dexie class.\n *\n * @return {void}\n */\nexport function __setDexie(ctor) {\n\tDexieCtor = ctor\n\tdbInstance = null\n}\n\n/**\n * Load Dexie on first use, then open (and memoise) the database.\n *\n * Every function in the offline core goes through this, so Dexie is only\n * evaluated on a page that actually uses the offline database. A Dexie\n * injected with `__setDexie` is used as is and nothing is imported.\n *\n * @return {Promise<object>} The opened database handle.\n */\nexport async function openDb() {\n\tif (DexieCtor === null) {\n\t\tconst mod = await import('dexie')\n\t\tDexieCtor = mod.default ?? mod.Dexie\n\t}\n\treturn getDb()\n}\n\n/**\n * The memoised database handle, synchronously.\n *\n * Works once Dexie is loaded: after any `openDb()` call, or after a test\n * injected it with `__setDexie`. Call `await openDb()` when neither is\n * certain; that is what every function in this module does.\n *\n * @return {object} The opened database handle.\n *\n * @throws {Error} When Dexie has not been loaded yet.\n */\nexport function getDb() {\n\tif (dbInstance !== null) {\n\t\treturn dbInstance\n\t}\n\tif (DexieCtor === null) {\n\t\tthrow new Error('The offline database is not open yet: call `await openDb()` first. Dexie loads on first use, so a page that never opens the offline database never evaluates it.')\n\t}\n\n\tconst db = new DexieCtor(DB_NAME)\n\tdb.version(1).stores({\n\t\t// Composite key + indexes so a single DB serves every app/schema.\n\t\tobjectCache: 'key, register, schema, collection, objectId',\n\t\tmutationQueue: 'id, deviceId, status, queuedAt, register, schema',\n\t\tmeta: 'key',\n\t})\n\n\tdbInstance = db\n\treturn db\n}\n\n/**\n * Persist a downloaded planning payload into the local cache atomically.\n *\n * Stores the planned items as one collection and any reference data\n * (e.g. checklist templates) as a second collection, plus a per-collection\n * planning meta row recording the offline-ready expiry. Every write happens in\n * one Dexie transaction so a connection drop mid-write never leaves the cache\n * half-populated.\n *\n * @param {object} args Planning storage arguments.\n * @param {string} args.register Register slug or id.\n * @param {string} args.schema Schema slug of the planned items.\n * @param {Array} args.items The planned objects to cache.\n * @param {Array} [args.references] Optional reference objects (templates).\n * @param {string} [args.referenceSchema] Schema slug of the references.\n * @param {string} [args.collection] Planning collection name (default 'planning').\n * @param {object} [args.manifest] Optional download manifest.\n * @param {number} [args.ttlMs] Offline lifetime (default 24h).\n *\n * @return {Promise<void>} Resolves when the planning is stored.\n */\nexport async function storePlanning({\n\tregister,\n\tschema,\n\titems = [],\n\treferences = [],\n\treferenceSchema = null,\n\tcollection = 'planning',\n\tmanifest = null,\n\tttlMs = 24 * 60 * 60 * 1000,\n}) {\n\tconst db = await openDb()\n\tconst planned = Array.isArray(items) ? items : []\n\tconst refs = Array.isArray(references) ? references : []\n\tconst refSchema = referenceSchema || schema\n\tconst expiresAt = new Date(Date.now() + ttlMs).toISOString()\n\n\tconst objectIdOf = (obj) => String(obj?.id ?? obj?.['@self']?.id ?? obj?.uuid ?? '')\n\n\tconst cacheRows = planned\n\t\t.filter((obj) => objectIdOf(obj) !== '')\n\t\t.map((obj) => ({\n\t\t\tkey: cacheKey(register, schema, collection, objectIdOf(obj)),\n\t\t\tregister,\n\t\t\tschema,\n\t\t\tcollection,\n\t\t\tobjectId: objectIdOf(obj),\n\t\t\tobject: obj,\n\t\t}))\n\n\tconst refRows = refs\n\t\t.filter((obj) => objectIdOf(obj) !== '')\n\t\t.map((obj) => ({\n\t\t\tkey: cacheKey(register, refSchema, 'references', objectIdOf(obj)),\n\t\t\tregister,\n\t\t\tschema: refSchema,\n\t\t\tcollection: 'references',\n\t\t\tobjectId: objectIdOf(obj),\n\t\t\tobject: obj,\n\t\t}))\n\n\tawait db.transaction('rw', db.objectCache, db.meta, async () => {\n\t\tawait db.objectCache.bulkPut(cacheRows.concat(refRows))\n\t\tawait db.meta.put({\n\t\t\tkey: `planning::${register}::${schema}::${collection}`,\n\t\t\tregister,\n\t\t\tschema,\n\t\t\tcollection,\n\t\t\tstatus: 'ready_offline',\n\t\t\texpiresAt,\n\t\t\tsyncedAt: new Date().toISOString(),\n\t\t\tcount: cacheRows.length,\n\t\t\tmanifest,\n\t\t})\n\t})\n}\n\n/**\n * Read the cached planned items for a register/schema/collection.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug.\n * @param {string} [collection] Collection name (default 'planning').\n *\n * @return {Promise<Array>} The cached objects (unwrapped from the envelope).\n */\nexport async function getPlannedItems(register, schema, collection = 'planning') {\n\tconst db = await openDb()\n\tconst rows = await db.objectCache\n\t\t.where('collection').equals(collection)\n\t\t.and((row) => row.register === register && row.schema === schema)\n\t\t.toArray()\n\treturn rows.map((row) => row.object)\n}\n\n/**\n * Read a single cached object by id.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug.\n * @param {string} collection Collection name.\n * @param {string} objectId The object id.\n *\n * @return {Promise<object|null>} The cached object, or null.\n */\nexport async function getCachedObject(register, schema, collection, objectId) {\n\tconst db = await openDb()\n\tconst row = await db.objectCache.get(cacheKey(register, schema, collection, objectId))\n\treturn row ? row.object : null\n}\n\n/**\n * Read the planning meta row (or null when never synced) for a collection.\n *\n * @param {string} register Register slug or id.\n * @param {string} schema Schema slug.\n * @param {string} [collection] Collection name (default 'planning').\n *\n * @return {Promise<object|null>} The planning meta row.\n */\nexport async function getPlanningMeta(register, schema, collection = 'planning') {\n\tconst db = await openDb()\n\treturn (await db.meta.get(`planning::${register}::${schema}::${collection}`)) ?? null\n}\n\n/**\n * Enqueue one offline mutation against the OR object API.\n *\n * @param {object} operation The mutation operation.\n * @param {string} operation.deviceId Owning device id (IDOR scope).\n * @param {string} operation.operationType create / update / delete / submit.\n * @param {string} operation.register Target register slug or id.\n * @param {string} operation.schema Target schema slug or id.\n * @param {string} [operation.targetId] Existing object id (update/delete).\n * @param {string} [operation.endpoint] Path under the Nextcloud root a\n * `submit` row is POSTed to.\n * @param {object} [operation.payload] The object body (create/update).\n *\n * @return {Promise<string>} The queued operation id.\n */\nexport async function enqueueMutation(operation) {\n\tconst db = await openDb()\n\tconst id = operation.id || `op-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n\tawait db.mutationQueue.put({\n\t\tid,\n\t\tdeviceId: operation.deviceId,\n\t\toperationType: operation.operationType,\n\t\tregister: operation.register,\n\t\tschema: operation.schema,\n\t\ttargetId: operation.targetId ?? null,\n\t\t// A `submit` operation replays to an app's own endpoint instead of the\n\t\t// object API (see checklistSubmission.js); every other row has none.\n\t\tendpoint: operation.endpoint ?? null,\n\t\tpayload: operation.payload ?? {},\n\t\tqueuedAt: operation.queuedAt || new Date().toISOString(),\n\t\tattemptCount: 0,\n\t\tstatus: 'pending',\n\t})\n\treturn id\n}\n\n/**\n * Count all queue operations not yet synced — drives the pending badge.\n *\n * @param {string} [deviceId] Optional device scope.\n *\n * @return {Promise<number>} The number of pending/conflict/syncing operations.\n */\nexport async function countPending(deviceId) {\n\tconst db = await openDb()\n\tlet collection = db.mutationQueue.where('status').anyOf(['pending', 'conflict', 'syncing'])\n\tif (typeof deviceId === 'string' && deviceId !== '') {\n\t\tcollection = collection.and((op) => op.deviceId === deviceId)\n\t}\n\treturn await collection.count()\n}\n\n/**\n * Every queued operation, in the order they will replay.\n *\n * 🔴 INCLUDING THE FAILED ONES, WHICH IS THE POINT. `countPending()` counts\n * `pending`, `conflict` and `syncing` and leaves `failed` out, so an operation\n * that exhausted its retries or lost its permission drops out of the badge\n * entirely. The row is still on the device; nothing tells anybody. An\n * inspection a citizen stood beside somebody to give is then stranded in one\n * browser's IndexedDB with no reader, which is worse than having refused it at\n * the door.\n *\n * FIFO by `queuedAt`, the same order the replay loop uses, so the list reads\n * as what will happen next rather than as a set.\n *\n * @param {string} [deviceId] Optional device scope.\n *\n * @return {Promise<object[]>} Every operation, oldest first.\n */\nexport async function listQueue(deviceId) {\n\tconst db = await openDb()\n\tlet collection = db.mutationQueue.orderBy('queuedAt')\n\tif (typeof deviceId === 'string' && deviceId !== '') {\n\t\tcollection = collection.and((op) => op.deviceId === deviceId)\n\t}\n\n\treturn await collection.toArray()\n}\n\n/**\n * How many operations will not replay again without somebody acting.\n *\n * Counted apart from `countPending()` rather than folded into it: \"12 waiting\"\n * and \"12 waiting, 1 stuck\" are different sentences, and a surface that adds\n * them together says neither. Nothing is ever dropped to make this number\n * smaller.\n *\n * @param {string} [deviceId] Optional device scope.\n *\n * @return {Promise<number>} The number of failed operations.\n */\nexport async function countStuck(deviceId) {\n\tconst db = await openDb()\n\tlet collection = db.mutationQueue.where('status').equals('failed')\n\tif (typeof deviceId === 'string' && deviceId !== '') {\n\t\tcollection = collection.and((op) => op.deviceId === deviceId)\n\t}\n\n\treturn await collection.count()\n}\n\n/**\n * Put a failed operation back in the queue, by hand.\n *\n * 🔴 BY HAND, AND NEVER AUTOMATICALLY. `failed` is terminal for the replay\n * loop on purpose: an operation the server refused five times will be refused\n * a sixth, and a queue that retries forever is a queue that never drains. But\n * terminal must not mean forgotten, so somebody who has fixed whatever the\n * server was complaining about can ask for it again, and the attempt count\n * starts over.\n *\n * It does NOT reset a `permission_lost` row. The right to write is gone and\n * retrying cannot bring it back; offering the button would be offering a\n * gesture that fails every time.\n *\n * @param {string} operationId The operation to retry.\n *\n * @return {Promise<boolean>} True when it was re-queued.\n */\nexport async function requeueOperation(operationId) {\n\tconst db = await openDb()\n\tconst operation = await db.mutationQueue.get(operationId)\n\tif (!operation || operation.status !== 'failed') {\n\t\treturn false\n\t}\n\n\tif (operation.lastError === 'permission_lost') {\n\t\treturn false\n\t}\n\n\tawait db.mutationQueue.update(operationId, {\n\t\tstatus: 'pending',\n\t\tattemptCount: 0,\n\t\tlastError: null,\n\t})\n\n\treturn true\n}\n\n/**\n * The prefix of a conflict-record operation's id.\n *\n * Derived from the id of the operation that conflicted, so the record is\n * idempotent by construction: a second drain of the same conflicting row\n * computes the same id and finds it already there.\n */\nexport const CONFLICT_RECORD_PREFIX = 'conflict-'\n\n/**\n * Write a classified conflict to the register as an object of its own.\n *\n * A conflict kept only in this browser's IndexedDB is invisible to everyone who\n * could act on it: the colleague whose edit collided, the supervisor who has to\n * decide, the audit that has to show the decision was taken at all. So the\n * conflict becomes an OpenRegister object, carrying both versions and a\n * reference back to the queue row.\n *\n * The write goes through the queue rather than straight to the network, because\n * the moment a conflict is classified is exactly the moment the connection is\n * unreliable. It replays with everything else.\n *\n * It is written ONCE. The record's id is derived from the conflicting\n * operation's id, so a second drain finds it and leaves it alone rather than\n * filing the same collision twice under two ids.\n *\n * With no `conflictSchema` configured nothing is written, and the queue row is\n * marked so the list can say plainly that this conflict is local to this\n * device. Silently keeping it would be the worse half of both options.\n *\n * @param {object} args Recording arguments.\n * @param {object} args.operation The queue row that conflicted.\n * @param {string} args.conflictType concurrent_edit / deleted_remote / permission_lost.\n * @param {object|null} [args.serverObject] The server's version, when it sent one.\n * @param {string} [args.register] Register to write the record to (default: the operation's).\n * @param {string} [args.conflictSchema] Schema holding conflict records.\n *\n * @return {Promise<string|null>} The record operation id, or null when nothing was written.\n */\nexport async function recordConflict({\n\toperation,\n\tconflictType,\n\tserverObject = null,\n\tregister = '',\n\tconflictSchema = '',\n}) {\n\tconst db = await openDb()\n\n\tif (typeof conflictSchema !== 'string' || conflictSchema === '') {\n\t\tawait db.mutationQueue.update(operation.id, { conflictScope: 'local', conflictType })\n\t\treturn null\n\t}\n\n\tconst recordId = `${CONFLICT_RECORD_PREFIX}${operation.id}`\n\tconst existing = await db.mutationQueue.get(recordId)\n\tif (existing !== undefined) {\n\t\treturn recordId\n\t}\n\n\tawait enqueueMutation({\n\t\tid: recordId,\n\t\tdeviceId: operation.deviceId,\n\t\toperationType: 'create',\n\t\tregister: register || operation.register,\n\t\tschema: conflictSchema,\n\t\tpayload: {\n\t\t\tqueueOperationId: operation.id,\n\t\t\tconflictType,\n\t\t\tdeviceId: operation.deviceId,\n\t\t\ttargetRegister: operation.register,\n\t\t\ttargetSchema: operation.schema,\n\t\t\ttargetId: operation.targetId ?? null,\n\t\t\tclientVersion: operation.payload ?? {},\n\t\t\tserverVersion: serverObject ?? null,\n\t\t\tdetectedAt: new Date().toISOString(),\n\t\t},\n\t})\n\n\tawait db.mutationQueue.update(operation.id, {\n\t\tconflictScope: 'register',\n\t\tconflictType,\n\t\tconflictRecordId: recordId,\n\t\tserverVersion: serverObject ?? null,\n\t})\n\n\treturn recordId\n}\n\n/**\n * Write the resolution onto the conflict object, wherever it currently is.\n *\n * Still queued: its payload is amended in place, so it reaches the register\n * already carrying the decision. Already sent: a follow-up `update` is queued\n * against the id the server gave it. Never sent at all (no schema configured):\n * there is nothing to write to, and the queue row keeps the decision.\n *\n * @param {object} db The Dexie handle.\n * @param {object} operation The conflicting queue row.\n * @param {string} resolution The choice made.\n * @param {string} resolvedBy The uid settling it.\n *\n * @return {Promise<void>} Nothing.\n */\nasync function recordResolutionOnConflictObject(db, operation, resolution, resolvedBy) {\n\tconst recordId = operation.conflictRecordId\n\tif (typeof recordId !== 'string' || recordId === '') {\n\t\treturn\n\t}\n\n\tconst record = await db.mutationQueue.get(recordId)\n\tif (record === undefined) {\n\t\treturn\n\t}\n\n\tconst decision = { resolution, resolvedBy, resolvedAt: new Date().toISOString() }\n\n\tif (record.status !== 'synced') {\n\t\tawait db.mutationQueue.update(recordId, { payload: { ...(record.payload ?? {}), ...decision } })\n\t\treturn\n\t}\n\n\tconst serverId = String(record.serverObjectId ?? '')\n\tif (serverId === '') {\n\t\treturn\n\t}\n\n\tawait enqueueMutation({\n\t\tid: `${recordId}-resolution`,\n\t\tdeviceId: record.deviceId,\n\t\toperationType: 'update',\n\t\tregister: record.register,\n\t\tschema: record.schema,\n\t\ttargetId: serverId,\n\t\tpayload: { ...(record.payload ?? {}), ...decision },\n\t})\n}\n\n/**\n * Apply a person's conflict resolution to the queue and to the record.\n *\n * The decision itself is the pure engine's (`resolveConflictChoice`); this puts\n * the result where it has to live. Three things happen, and the third is the\n * one that is easy to forget:\n *\n * 1. The queue row takes the engine's patch, so keep-mine replays and\n * keep-theirs stops.\n * 2. Keep theirs leaves the local cache holding the SERVER's version. Marking\n * the row synced while the device still shows the abandoned local text is\n * how somebody reads their own discarded answer as the current record.\n * 3. The resolution, who made it and when are written onto the conflict\n * object. A conflict record that says a collision happened and not how it\n * was settled cannot answer the only question anybody asks later.\n *\n * @param {object} args Resolution arguments.\n * @param {string} args.operationId The conflicting queue row.\n * @param {string} args.resolution client_wins / server_wins / manual_merge.\n * @param {object} [args.mergedPayload] The merged body, for a manual merge.\n * @param {string} [args.resolvedBy] The uid settling it.\n *\n * @return {Promise<boolean>} True when a conflict was resolved.\n */\nexport async function applyConflictResolution({\n\toperationId,\n\tresolution,\n\tmergedPayload = null,\n\tresolvedBy = '',\n}) {\n\tconst db = await openDb()\n\tconst operation = await db.mutationQueue.get(operationId)\n\n\tif (operation === undefined || operation.status !== 'conflict') {\n\t\treturn false\n\t}\n\n\tconst { patch } = resolveConflictChoice(resolution, mergedPayload)\n\tawait db.mutationQueue.update(operationId, {\n\t\t...patch,\n\t\tresolution,\n\t\tresolvedBy,\n\t\tresolvedAt: new Date().toISOString(),\n\t})\n\n\tif (resolution === 'server_wins' && operation.serverVersion !== null && operation.serverVersion !== undefined) {\n\t\tconst objectId = String(operation.targetId ?? '')\n\t\tif (objectId !== '') {\n\t\t\tawait db.objectCache.put({\n\t\t\t\tkey: cacheKey(operation.register, operation.schema, 'planning', objectId),\n\t\t\t\tregister: operation.register,\n\t\t\t\tschema: operation.schema,\n\t\t\t\tcollection: 'planning',\n\t\t\t\tobjectId,\n\t\t\t\tobject: operation.serverVersion,\n\t\t\t})\n\t\t}\n\t}\n\n\tawait recordResolutionOnConflictObject(db, operation, resolution, resolvedBy)\n\n\treturn true\n}\n\n/**\n * Resolve a stable per-device id, persisted in localStorage.\n *\n * The device id scopes the mutation queue so the server can re-authorize that\n * only the owning device pushes its own queued operations (IDOR is enforced\n * server-side, never client-asserted).\n *\n * @param {object} [storage] localStorage-like object (test seam).\n *\n * @return {string} The device id.\n */\nexport function resolveDeviceId(storage) {\n\tconst store = storage || (typeof window !== 'undefined' ? window.localStorage : null)\n\tconst STORAGE_KEY = 'cn-offline-device-id'\n\tif (store === null || store === undefined) {\n\t\treturn `device-${Math.random().toString(36).slice(2, 10)}`\n\t}\n\tlet id = store.getItem(STORAGE_KEY)\n\tif (!id) {\n\t\tid = `device-${Math.random().toString(36).slice(2, 10)}`\n\t\tstore.setItem(STORAGE_KEY, id)\n\t}\n\treturn id\n}\n\n/**\n * Reset the memoised handle — test/teardown only.\n *\n * @return {void}\n */\nexport function __resetDbForTests() {\n\tdbInstance = null\n}\n"],"names":[],"mappings":"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"}
|