@conduction/nextcloud-vue 2.77.0 → 2.78.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/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +37 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +8 -7
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +7 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
- 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 +6 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +11 -1
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/composables/useObjectPresence.js +15 -4
- package/dist/esm/composables/useObjectPresence.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +212 -1
- package/dist/esm/utils/badgeVariants.js +32 -0
- package/dist/esm/utils/badgeVariants.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +533 -233
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +212 -1
- package/l10n/en.json +7 -0
- package/l10n/nl.json +7 -0
- package/package.json +1 -1
- package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
- package/src/components/CnBoardView/CnBoardView.vue +2 -1
- package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +50 -8
- package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
- package/src/components/CnDataTable/CnDataTable.vue +7 -2
- package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
- package/src/components/CnFormPage/CnFormPage.vue +4 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +6 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
- package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
- package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
- package/src/composables/useFormDraft.js +11 -1
- package/src/composables/useObjectPresence.js +15 -4
- package/src/css/badge.css +31 -0
- package/src/css/kpi-card.css +51 -0
- package/src/css/look-board-index.css +36 -0
- package/src/css/look-board.css +8 -0
- package/src/schemas/app-manifest-v2.schema.json +5 -5
- package/src/utils/badgeVariants.js +29 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnStatusBadge.vue2.js","sources":["../../../../src/components/CnStatusBadge/CnStatusBadge.vue"],"sourcesContent":["<template>\n\t<span\n\t\tclass=\"cn-status-badge\"\n\t\t:class=\"badgeClasses\">\n\t\t<slot>\n\t\t\t<slot name=\"icon\" />\n\t\t\t{{ label }}\n\t\t</slot>\n\t</span>\n</template>\n\n<script>\n/**\n * CnStatusBadge — Color-coded pill badge for status, priority, or category display.\n *\n * Replaces the various .status-badge / .priority-badge CSS patterns duplicated\n * across Pipelinq and Dossiq. Supports a colorMap for automatic variant lookup.\n *\n * ```vue\n * <CnStatusBadge label=\"Open\" variant=\"success\" />\n * <CnStatusBadge label=\"Urgent\" variant=\"error\" size=\"small\" />\n * ```\n *\n * <!-- With colorMap: variant auto-resolved from label -->\n * ```vue\n * <CnStatusBadge\n * label=\"overdue\"\n * :color-map=\"{ open: 'success', closed: 'default', overdue: 'error' }\" />\n * ```\n */\nexport default {\n\tname: 'CnStatusBadge',\n\n\tprops: {\n\t\t/** Badge label text */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Color variant: 'default', 'primary', 'success', 'warning', 'error'
|
|
1
|
+
{"version":3,"file":"CnStatusBadge.vue2.js","sources":["../../../../src/components/CnStatusBadge/CnStatusBadge.vue"],"sourcesContent":["<template>\n\t<span\n\t\tclass=\"cn-status-badge\"\n\t\t:class=\"badgeClasses\">\n\t\t<slot>\n\t\t\t<slot name=\"icon\" />\n\t\t\t{{ label }}\n\t\t</slot>\n\t</span>\n</template>\n\n<script>\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\n\n/**\n * CnStatusBadge — Color-coded pill badge for status, priority, or category display.\n *\n * Replaces the various .status-badge / .priority-badge CSS patterns duplicated\n * across Pipelinq and Dossiq. Supports a colorMap for automatic variant lookup.\n *\n * ```vue\n * <CnStatusBadge label=\"Open\" variant=\"success\" />\n * <CnStatusBadge label=\"Urgent\" variant=\"error\" size=\"small\" />\n * ```\n *\n * <!-- With colorMap: variant auto-resolved from label -->\n * ```vue\n * <CnStatusBadge\n * label=\"overdue\"\n * :color-map=\"{ open: 'success', closed: 'default', overdue: 'error' }\" />\n * ```\n */\nexport default {\n\tname: 'CnStatusBadge',\n\n\tprops: {\n\t\t/** Badge label text */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Color variant: 'default', 'primary', 'success', 'warning', 'error',\n\t\t * 'info', or one of the two board tones without a Nextcloud role,\n\t\t * 'purple' (work in progress) and 'teal' (handed on).\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => isBadgeVariant(v),\n\t\t},\n\n\t\t/** Size: 'small' or 'medium' */\n\t\tsize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'medium',\n\t\t\tvalidator: (v) => ['small', 'medium'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Use solid background with white text instead of light background with colored text.\n\t\t * Useful when the badge is placed on a colored background (e.g., an active card).\n\t\t */\n\t\tsolid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Map of label values to variants. When provided, the variant is resolved\n\t\t * from this map using the label (case-insensitive). Falls back to the variant prop.\n\t\t * { open: 'success', closed: 'default', overdue: 'error' }\n\t\t */\n\t\tcolorMap: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Key to look up in `colorMap` instead of the label. Set this when the\n\t\t * label is a translated/display string but the map is keyed on the raw\n\t\t * value (e.g. a schema enum code) — without it a translated label misses\n\t\t * every map entry and the badge silently falls back to `variant`.\n\t\t */\n\t\tcolorKey: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** The value the colorMap is looked up by — `colorKey` when set, else the label. */\n\t\tresolvedColorKey() {\n\t\t\treturn this.colorKey || this.label\n\t\t},\n\n\t\tresolvedVariant() {\n\t\t\tif (this.colorMap && this.resolvedColorKey) {\n\t\t\t\tconst key = this.resolvedColorKey.toLowerCase()\n\t\t\t\tconst normalizedColorMap = Object.fromEntries(Object.entries(this.colorMap).map(([k, v]) => [k.toLowerCase(), v]))\n\t\t\t\treturn normalizedColorMap[key] || this.variant\n\t\t\t}\n\t\t\treturn this.variant\n\t\t},\n\n\t\tbadgeClasses() {\n\t\t\treturn {\n\t\t\t\t['cn-status-badge--' + this.resolvedVariant]: true,\n\t\t\t\t'cn-status-badge--small': this.size === 'small',\n\t\t\t\t'cn-status-badge--solid': this.solid,\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n"],"names":[],"mappings":";;AAcA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,eAAe;;AAErB,CAAA,KAAK,EAAE;AACN;AACA,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;;AAED;;;AAGC;;AAED,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,SAAS;AAClB,GAAA,SAAS,EAAE,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,CAAC;GACnC;;;EAGD,IAAI,EAAE;GACL,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,QAAQ;AACjB,GAAA,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;GACjD;;AAED;AACC;AACA;;AAED,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;;AAED;;AAEC;;;AAGD,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;;;;;;AAMA,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;EACD;;AAED,CAAA,QAAQ,EAAE;AACT;AACA,EAAA,gBAAgB,GAAG;AAClB,GAAA,OAAO,IAAI,CAAC,QAAO,IAAK,IAAI,CAAC,KAAI;GACjC;;AAED,EAAA,eAAe,GAAG;AACjB,GAAA,IAAI,IAAI,CAAC,QAAS,IAAG,IAAI,CAAC,gBAAgB,EAAE;AAC3C,IAAA,MAAM,MAAM,IAAI,CAAC,gBAAgB,CAAC,WAAW,GAAC;IAC9C,MAAM,kBAAmB,GAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAA;AACjH,IAAA,OAAO,kBAAkB,CAAC,GAAG,CAAA,IAAK,IAAI,CAAC,OAAM;AAC9C,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,OAAM;GAClB;;AAED,EAAA,YAAY,GAAG;AACd,GAAA,OAAO;IACN,CAAC,mBAAkB,GAAI,IAAI,CAAC,eAAe,GAAG,IAAI;AAClD,IAAA,wBAAwB,EAAE,IAAI,CAAC,SAAS,OAAO;AAC/C,IAAA,wBAAwB,EAAE,IAAI,CAAC,KAAK;AACrC,IAAA;GACA;EACD;AACF;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnStatusBadge.vue3.js","sources":["../../../../src/components/CnStatusBadge/CnStatusBadge.vue?vue&type=template&id=168d8846&lang.js"],"sourcesContent":["<template>\n\t<span\n\t\tclass=\"cn-status-badge\"\n\t\t:class=\"badgeClasses\">\n\t\t<slot>\n\t\t\t<slot name=\"icon\" />\n\t\t\t{{ label }}\n\t\t</slot>\n\t</span>\n</template>\n\n<script>\n/**\n * CnStatusBadge — Color-coded pill badge for status, priority, or category display.\n *\n * Replaces the various .status-badge / .priority-badge CSS patterns duplicated\n * across Pipelinq and Dossiq. Supports a colorMap for automatic variant lookup.\n *\n * ```vue\n * <CnStatusBadge label=\"Open\" variant=\"success\" />\n * <CnStatusBadge label=\"Urgent\" variant=\"error\" size=\"small\" />\n * ```\n *\n * <!-- With colorMap: variant auto-resolved from label -->\n * ```vue\n * <CnStatusBadge\n * label=\"overdue\"\n * :color-map=\"{ open: 'success', closed: 'default', overdue: 'error' }\" />\n * ```\n */\nexport default {\n\tname: 'CnStatusBadge',\n\n\tprops: {\n\t\t/** Badge label text */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Color variant: 'default', 'primary', 'success', 'warning', 'error'
|
|
1
|
+
{"version":3,"file":"CnStatusBadge.vue3.js","sources":["../../../../src/components/CnStatusBadge/CnStatusBadge.vue?vue&type=template&id=168d8846&lang.js"],"sourcesContent":["<template>\n\t<span\n\t\tclass=\"cn-status-badge\"\n\t\t:class=\"badgeClasses\">\n\t\t<slot>\n\t\t\t<slot name=\"icon\" />\n\t\t\t{{ label }}\n\t\t</slot>\n\t</span>\n</template>\n\n<script>\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\n\n/**\n * CnStatusBadge — Color-coded pill badge for status, priority, or category display.\n *\n * Replaces the various .status-badge / .priority-badge CSS patterns duplicated\n * across Pipelinq and Dossiq. Supports a colorMap for automatic variant lookup.\n *\n * ```vue\n * <CnStatusBadge label=\"Open\" variant=\"success\" />\n * <CnStatusBadge label=\"Urgent\" variant=\"error\" size=\"small\" />\n * ```\n *\n * <!-- With colorMap: variant auto-resolved from label -->\n * ```vue\n * <CnStatusBadge\n * label=\"overdue\"\n * :color-map=\"{ open: 'success', closed: 'default', overdue: 'error' }\" />\n * ```\n */\nexport default {\n\tname: 'CnStatusBadge',\n\n\tprops: {\n\t\t/** Badge label text */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Color variant: 'default', 'primary', 'success', 'warning', 'error',\n\t\t * 'info', or one of the two board tones without a Nextcloud role,\n\t\t * 'purple' (work in progress) and 'teal' (handed on).\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => isBadgeVariant(v),\n\t\t},\n\n\t\t/** Size: 'small' or 'medium' */\n\t\tsize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'medium',\n\t\t\tvalidator: (v) => ['small', 'medium'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Use solid background with white text instead of light background with colored text.\n\t\t * Useful when the badge is placed on a colored background (e.g., an active card).\n\t\t */\n\t\tsolid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Map of label values to variants. When provided, the variant is resolved\n\t\t * from this map using the label (case-insensitive). Falls back to the variant prop.\n\t\t * { open: 'success', closed: 'default', overdue: 'error' }\n\t\t */\n\t\tcolorMap: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Key to look up in `colorMap` instead of the label. Set this when the\n\t\t * label is a translated/display string but the map is keyed on the raw\n\t\t * value (e.g. a schema enum code) — without it a translated label misses\n\t\t * every map entry and the badge silently falls back to `variant`.\n\t\t */\n\t\tcolorKey: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** The value the colorMap is looked up by — `colorKey` when set, else the label. */\n\t\tresolvedColorKey() {\n\t\t\treturn this.colorKey || this.label\n\t\t},\n\n\t\tresolvedVariant() {\n\t\t\tif (this.colorMap && this.resolvedColorKey) {\n\t\t\t\tconst key = this.resolvedColorKey.toLowerCase()\n\t\t\t\tconst normalizedColorMap = Object.fromEntries(Object.entries(this.colorMap).map(([k, v]) => [k.toLowerCase(), v]))\n\t\t\t\treturn normalizedColorMap[key] || this.variant\n\t\t\t}\n\t\t\treturn this.variant\n\t\t},\n\n\t\tbadgeClasses() {\n\t\t\treturn {\n\t\t\t\t['cn-status-badge--' + this.resolvedVariant]: true,\n\t\t\t\t'cn-status-badge--small': this.size === 'small',\n\t\t\t\t'cn-status-badge--solid': this.solid,\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_createTextVNode"],"mappings":";;;uBACCA,kBAOM,CAAA,MAAA,EAAA;IANL,KAAK,EAAAC,cAAA,CAAA,CAAC,iBAAgB,EACd,QAAY,CAAA,YAAA,CAAA,CAAA;;AACpB,IAAAC,UAAA,CAGM,4BAHN,MAGM;MAFLA,UAAmB,CAAA,IAAA,CAAA,MAAA,EAAA,MAAA,CAAA;AAAA,MAAAC,eAAA,CAAA,GACnB,mBAAG;;;;;;;"}
|
|
@@ -128,12 +128,22 @@ function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {
|
|
|
128
128
|
* What the draft indicator says: "Saving" while a local write is pending,
|
|
129
129
|
* "Saved just now" / "Saved 3 minutes ago" once it landed, nothing before that.
|
|
130
130
|
*
|
|
131
|
+
* After a failed save (`options.saveFailed`) it never says Saving or Saved:
|
|
132
|
+
* next to a server error those words read as if the record had been saved.
|
|
133
|
+
* It then says "Draft kept on this device" when a local copy is stored, and
|
|
134
|
+
* nothing otherwise.
|
|
135
|
+
*
|
|
131
136
|
* @param {string} state `idle`, `saving` or `saved`.
|
|
132
137
|
* @param {number} [savedAt] When the last write landed, in epoch ms.
|
|
133
138
|
* @param {number} [now] The current epoch ms, for testing.
|
|
139
|
+
* @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.
|
|
134
140
|
* @return {string} The text, or '' when it says nothing.
|
|
141
|
+
* @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save
|
|
135
142
|
*/
|
|
136
|
-
function draftIndicatorText(state, savedAt = 0, now = Date.now()) {
|
|
143
|
+
function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {
|
|
144
|
+
if (options && options.saveFailed === true) {
|
|
145
|
+
return state === 'saved' ? translate('nextcloud-vue', 'Draft kept on this device') : ''
|
|
146
|
+
}
|
|
137
147
|
if (state === 'saving') {
|
|
138
148
|
return translate('nextcloud-vue', 'Saving')
|
|
139
149
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFormDraft.js","sources":["../../../src/composables/useFormDraft.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: AGPL-3.0-or-later\n *\n * useFormDraft — keep what somebody typed when the dialog closes on them.\n *\n * A handler filling a 27-field case type form and losing the tab starts over.\n * This writes the values to `localStorage` as they type and offers them back\n * the next time the same form opens.\n *\n * 🔴 IT NEVER DECIDES TO RESTORE. The draft is offered and the person chooses.\n * Silently refilling a form is how somebody submits last week's answers without\n * noticing they were there, and a form that fills itself is indistinguishable\n * from a form the server prefilled.\n *\n * 🔴 EVERY READ AND WRITE IS WRAPPED. `localStorage` throws in a private\n * window, with site data blocked, and inside some embedded webviews. A draft\n * that cannot be saved is a small loss; a dialog that will not open because\n * saving the draft threw is a large one.\n *\n * WHAT IT DELIBERATELY DOES NOT DO. It does not touch the server. A keystroke\n * autosave against the object API writes an audit row for every half-typed\n * value, and the audit trail is read by people answering \"who changed this\".\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** How long a draft stays interesting, in milliseconds (7 days). */\nexport const DRAFT_MAX_AGE_MS = (7 * 24 * 60 * 60 * 1000)\n\n/** How long to wait after the last keystroke before writing, in milliseconds. */\nexport const DRAFT_DEBOUNCE_MS = 500\n\n/** The prefix every draft key carries, so a sweep can find them. */\nexport const DRAFT_KEY_PREFIX = 'cn-form-draft'\n\n/**\n * The storage key one form's draft lives under.\n *\n * 🔴 THE USER ID IS PART OF THE KEY. A shared browser profile at a service\n * desk is the ordinary case in a municipality, and a draft keyed without the\n * user hands the next person at the counter what the last one typed.\n *\n * @param {object} parts The identifying parts.\n * @param {string} [parts.appId] The app the form belongs to.\n * @param {string} [parts.schema] The schema being filled in.\n * @param {string} [parts.objectId] The object being edited, or '' when new.\n * @param {string} [parts.userId] Who is typing.\n * @return {string} The storage key.\n */\nexport function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {}) {\n\treturn [\n\t\tDRAFT_KEY_PREFIX,\n\t\tString(appId || 'app'),\n\t\tString(schema || 'schema'),\n\t\tString(objectId || 'new'),\n\t\tString(userId || 'anonymous'),\n\t].join(':')\n}\n\n/**\n * Read one draft back, or null when there is nothing usable.\n *\n * Returns null for an absent key, unreadable storage, unparseable JSON, a\n * draft older than {@link DRAFT_MAX_AGE_MS}, and a draft older than the object\n * it belongs to. That last one matters: a colleague who saved the record after\n * you started typing has said something more recent than your draft, and\n * offering yours on top would invite you to overwrite them without being told.\n *\n * @param {string} key The storage key.\n * @param {object} [options] Reading options.\n * @param {string} [options.objectUpdated] The object's `updated` timestamp, when it has one.\n * @param {number} [options.now] The current epoch ms, for testing.\n * @return {{values: object, savedAt: number}|null} The draft, or null.\n */\nexport function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {\n\tlet raw\n\ttry {\n\t\traw = window.localStorage.getItem(key)\n\t} catch {\n\t\t// Blocked or unavailable storage reads as \"no draft\", never as a throw.\n\t\treturn null\n\t}\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tlet parsed\n\ttry {\n\t\tparsed = JSON.parse(raw)\n\t} catch {\n\t\t// A corrupted entry is dropped rather than offered. Nothing here can\n\t\t// repair it, and offering half a form is worse than offering none.\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif ((now - parsed.savedAt) > DRAFT_MAX_AGE_MS) {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (objectUpdated) {\n\t\tconst updatedAt = Date.parse(String(objectUpdated))\n\t\tif (!Number.isNaN(updatedAt) && updatedAt > parsed.savedAt) {\n\t\t\t// The record moved on after this draft was written.\n\t\t\tclearDraft(key)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tif (!parsed.values || typeof parsed.values !== 'object') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\treturn { values: parsed.values, savedAt: parsed.savedAt }\n}\n\n/**\n * What the draft indicator says: \"Saving\" while a local write is pending,\n * \"Saved just now\" / \"Saved 3 minutes ago\" once it landed, nothing before that.\n *\n * @param {string} state `idle`, `saving` or `saved`.\n * @param {number} [savedAt] When the last write landed, in epoch ms.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {string} The text, or '' when it says nothing.\n */\nexport function draftIndicatorText(state, savedAt = 0, now = Date.now()) {\n\tif (state === 'saving') {\n\t\treturn t('nextcloud-vue', 'Saving')\n\t}\n\tif (state !== 'saved') {\n\t\treturn ''\n\t}\n\tconst minutes = Math.floor(Math.max(0, now - savedAt) / 60000)\n\tif (!savedAt || minutes < 1) {\n\t\treturn t('nextcloud-vue', 'Saved just now')\n\t}\n\ttry {\n\t\tconst relative = new Intl.RelativeTimeFormat(document.documentElement.lang || 'en', { numeric: 'auto' }).format(-minutes, 'minute')\n\t\treturn t('nextcloud-vue', 'Saved {time}', { time: relative })\n\t} catch {\n\t\treturn t('nextcloud-vue', 'Saved')\n\t}\n}\n\n/**\n * Write one draft.\n *\n * @param {string} key The storage key.\n * @param {object} values The form values.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {boolean} True when it was stored.\n */\nexport function writeDraft(key, values, now = Date.now()) {\n\ttry {\n\t\twindow.localStorage.setItem(\n\t\t\tkey,\n\t\t\tJSON.stringify({ savedAt: now, values: (values || {}) }),\n\t\t)\n\t\treturn true\n\t} catch {\n\t\t// Quota exceeded, or storage blocked. The form keeps working; the\n\t\t// recovery does not exist for this session. Reported to the caller so\n\t\t// the indicator can stay quiet rather than claim \"Saved\".\n\t\treturn false\n\t}\n}\n\n/**\n * Forget one draft.\n *\n * @param {string} key The storage key.\n * @return {void}\n */\nexport function clearDraft(key) {\n\ttry {\n\t\twindow.localStorage.removeItem(key)\n\t} catch {\n\t\t// Nothing to do and nothing worth failing a dialog over.\n\t}\n}\n\n/**\n * The Options API mixin the library's components use.\n *\n * A mixin rather than a Vue 3 composable, because these components are Options\n * API and the library builds for both Vue 2.7 and Vue 3.\n *\n * @return {object} The mixin.\n */\nexport function formDraftMixin() {\n\treturn {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\t/** The draft offered on open, or null when there is none. */\n\t\t\t\tdraftOffer: null,\n\t\t\t\t/** 'idle' | 'saving' | 'saved' — what the indicator announces. */\n\t\t\t\tdraftState: 'idle',\n\t\t\t\t/** When the last local write landed, in epoch ms. */\n\t\t\t\tdraftSavedAt: 0,\n\t\t\t\t/** Non-reactive handle for the debounce. */\n\t\t\t\tdraftTimeout: null,\n\t\t\t\t/**\n\t\t\t\t * Whether the form's own initial population has happened.\n\t\t\t\t *\n\t\t\t\t * The first change to the values is the component building\n\t\t\t\t * them, not a person typing. See the watcher that reads this.\n\t\t\t\t */\n\t\t\t\tdraftSeeded: false,\n\t\t\t}\n\t\t},\n\n\t\tbeforeUnmount() {\n\t\t\tif (this.draftTimeout) {\n\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\tthis.draftTimeout = null\n\t\t\t}\n\t\t},\n\n\t\tmethods: {\n\t\t\t/**\n\t\t\t * Schedule a draft write, debounced.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @param {object} values The current form values.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tscheduleDraftWrite(key, values) {\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.draftState = 'saving'\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t}\n\n\t\t\t\tthis.draftTimeout = setTimeout(() => {\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t\t// 'idle' and not 'saved' when the write failed: an\n\t\t\t\t\t// indicator that says Saved about a draft that is not\n\t\t\t\t\t// stored is the whole class of defect this library has\n\t\t\t\t\t// been paying down, in the one place a user is trusting it.\n\t\t\t\t\tconst stored = writeDraft(key, values)\n\t\t\t\t\tthis.draftState = stored ? 'saved' : 'idle'\n\t\t\t\t\tthis.draftSavedAt = stored ? Date.now() : 0\n\t\t\t\t}, DRAFT_DEBOUNCE_MS)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop the draft and stop announcing anything about it.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tforgetDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\n\t\t\t\tclearDraft(key)\n\t\t\t\tthis.draftOffer = null\n\t\t\t\tthis.draftState = 'idle'\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useFormDraft.js","sources":["../../../src/composables/useFormDraft.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: AGPL-3.0-or-later\n *\n * useFormDraft — keep what somebody typed when the dialog closes on them.\n *\n * A handler filling a 27-field case type form and losing the tab starts over.\n * This writes the values to `localStorage` as they type and offers them back\n * the next time the same form opens.\n *\n * 🔴 IT NEVER DECIDES TO RESTORE. The draft is offered and the person chooses.\n * Silently refilling a form is how somebody submits last week's answers without\n * noticing they were there, and a form that fills itself is indistinguishable\n * from a form the server prefilled.\n *\n * 🔴 EVERY READ AND WRITE IS WRAPPED. `localStorage` throws in a private\n * window, with site data blocked, and inside some embedded webviews. A draft\n * that cannot be saved is a small loss; a dialog that will not open because\n * saving the draft threw is a large one.\n *\n * WHAT IT DELIBERATELY DOES NOT DO. It does not touch the server. A keystroke\n * autosave against the object API writes an audit row for every half-typed\n * value, and the audit trail is read by people answering \"who changed this\".\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** How long a draft stays interesting, in milliseconds (7 days). */\nexport const DRAFT_MAX_AGE_MS = (7 * 24 * 60 * 60 * 1000)\n\n/** How long to wait after the last keystroke before writing, in milliseconds. */\nexport const DRAFT_DEBOUNCE_MS = 500\n\n/** The prefix every draft key carries, so a sweep can find them. */\nexport const DRAFT_KEY_PREFIX = 'cn-form-draft'\n\n/**\n * The storage key one form's draft lives under.\n *\n * 🔴 THE USER ID IS PART OF THE KEY. A shared browser profile at a service\n * desk is the ordinary case in a municipality, and a draft keyed without the\n * user hands the next person at the counter what the last one typed.\n *\n * @param {object} parts The identifying parts.\n * @param {string} [parts.appId] The app the form belongs to.\n * @param {string} [parts.schema] The schema being filled in.\n * @param {string} [parts.objectId] The object being edited, or '' when new.\n * @param {string} [parts.userId] Who is typing.\n * @return {string} The storage key.\n */\nexport function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {}) {\n\treturn [\n\t\tDRAFT_KEY_PREFIX,\n\t\tString(appId || 'app'),\n\t\tString(schema || 'schema'),\n\t\tString(objectId || 'new'),\n\t\tString(userId || 'anonymous'),\n\t].join(':')\n}\n\n/**\n * Read one draft back, or null when there is nothing usable.\n *\n * Returns null for an absent key, unreadable storage, unparseable JSON, a\n * draft older than {@link DRAFT_MAX_AGE_MS}, and a draft older than the object\n * it belongs to. That last one matters: a colleague who saved the record after\n * you started typing has said something more recent than your draft, and\n * offering yours on top would invite you to overwrite them without being told.\n *\n * @param {string} key The storage key.\n * @param {object} [options] Reading options.\n * @param {string} [options.objectUpdated] The object's `updated` timestamp, when it has one.\n * @param {number} [options.now] The current epoch ms, for testing.\n * @return {{values: object, savedAt: number}|null} The draft, or null.\n */\nexport function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {\n\tlet raw\n\ttry {\n\t\traw = window.localStorage.getItem(key)\n\t} catch {\n\t\t// Blocked or unavailable storage reads as \"no draft\", never as a throw.\n\t\treturn null\n\t}\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tlet parsed\n\ttry {\n\t\tparsed = JSON.parse(raw)\n\t} catch {\n\t\t// A corrupted entry is dropped rather than offered. Nothing here can\n\t\t// repair it, and offering half a form is worse than offering none.\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif ((now - parsed.savedAt) > DRAFT_MAX_AGE_MS) {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (objectUpdated) {\n\t\tconst updatedAt = Date.parse(String(objectUpdated))\n\t\tif (!Number.isNaN(updatedAt) && updatedAt > parsed.savedAt) {\n\t\t\t// The record moved on after this draft was written.\n\t\t\tclearDraft(key)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tif (!parsed.values || typeof parsed.values !== 'object') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\treturn { values: parsed.values, savedAt: parsed.savedAt }\n}\n\n/**\n * What the draft indicator says: \"Saving\" while a local write is pending,\n * \"Saved just now\" / \"Saved 3 minutes ago\" once it landed, nothing before that.\n *\n * After a failed save (`options.saveFailed`) it never says Saving or Saved:\n * next to a server error those words read as if the record had been saved.\n * It then says \"Draft kept on this device\" when a local copy is stored, and\n * nothing otherwise.\n *\n * @param {string} state `idle`, `saving` or `saved`.\n * @param {number} [savedAt] When the last write landed, in epoch ms.\n * @param {number} [now] The current epoch ms, for testing.\n * @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.\n * @return {string} The text, or '' when it says nothing.\n * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save\n */\nexport function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {\n\tif (options && options.saveFailed === true) {\n\t\treturn state === 'saved' ? t('nextcloud-vue', 'Draft kept on this device') : ''\n\t}\n\tif (state === 'saving') {\n\t\treturn t('nextcloud-vue', 'Saving')\n\t}\n\tif (state !== 'saved') {\n\t\treturn ''\n\t}\n\tconst minutes = Math.floor(Math.max(0, now - savedAt) / 60000)\n\tif (!savedAt || minutes < 1) {\n\t\treturn t('nextcloud-vue', 'Saved just now')\n\t}\n\ttry {\n\t\tconst relative = new Intl.RelativeTimeFormat(document.documentElement.lang || 'en', { numeric: 'auto' }).format(-minutes, 'minute')\n\t\treturn t('nextcloud-vue', 'Saved {time}', { time: relative })\n\t} catch {\n\t\treturn t('nextcloud-vue', 'Saved')\n\t}\n}\n\n/**\n * Write one draft.\n *\n * @param {string} key The storage key.\n * @param {object} values The form values.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {boolean} True when it was stored.\n */\nexport function writeDraft(key, values, now = Date.now()) {\n\ttry {\n\t\twindow.localStorage.setItem(\n\t\t\tkey,\n\t\t\tJSON.stringify({ savedAt: now, values: (values || {}) }),\n\t\t)\n\t\treturn true\n\t} catch {\n\t\t// Quota exceeded, or storage blocked. The form keeps working; the\n\t\t// recovery does not exist for this session. Reported to the caller so\n\t\t// the indicator can stay quiet rather than claim \"Saved\".\n\t\treturn false\n\t}\n}\n\n/**\n * Forget one draft.\n *\n * @param {string} key The storage key.\n * @return {void}\n */\nexport function clearDraft(key) {\n\ttry {\n\t\twindow.localStorage.removeItem(key)\n\t} catch {\n\t\t// Nothing to do and nothing worth failing a dialog over.\n\t}\n}\n\n/**\n * The Options API mixin the library's components use.\n *\n * A mixin rather than a Vue 3 composable, because these components are Options\n * API and the library builds for both Vue 2.7 and Vue 3.\n *\n * @return {object} The mixin.\n */\nexport function formDraftMixin() {\n\treturn {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\t/** The draft offered on open, or null when there is none. */\n\t\t\t\tdraftOffer: null,\n\t\t\t\t/** 'idle' | 'saving' | 'saved' — what the indicator announces. */\n\t\t\t\tdraftState: 'idle',\n\t\t\t\t/** When the last local write landed, in epoch ms. */\n\t\t\t\tdraftSavedAt: 0,\n\t\t\t\t/** Non-reactive handle for the debounce. */\n\t\t\t\tdraftTimeout: null,\n\t\t\t\t/**\n\t\t\t\t * Whether the form's own initial population has happened.\n\t\t\t\t *\n\t\t\t\t * The first change to the values is the component building\n\t\t\t\t * them, not a person typing. See the watcher that reads this.\n\t\t\t\t */\n\t\t\t\tdraftSeeded: false,\n\t\t\t}\n\t\t},\n\n\t\tbeforeUnmount() {\n\t\t\tif (this.draftTimeout) {\n\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\tthis.draftTimeout = null\n\t\t\t}\n\t\t},\n\n\t\tmethods: {\n\t\t\t/**\n\t\t\t * Schedule a draft write, debounced.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @param {object} values The current form values.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tscheduleDraftWrite(key, values) {\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.draftState = 'saving'\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t}\n\n\t\t\t\tthis.draftTimeout = setTimeout(() => {\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t\t// 'idle' and not 'saved' when the write failed: an\n\t\t\t\t\t// indicator that says Saved about a draft that is not\n\t\t\t\t\t// stored is the whole class of defect this library has\n\t\t\t\t\t// been paying down, in the one place a user is trusting it.\n\t\t\t\t\tconst stored = writeDraft(key, values)\n\t\t\t\t\tthis.draftState = stored ? 'saved' : 'idle'\n\t\t\t\t\tthis.draftSavedAt = stored ? Date.now() : 0\n\t\t\t\t}, DRAFT_DEBOUNCE_MS)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop the draft and stop announcing anything about it.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tforgetDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\n\t\t\t\tclearDraft(key)\n\t\t\t\tthis.draftOffer = null\n\t\t\t\tthis.draftState = 'idle'\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":["t"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCurrentUser } from '@nextcloud/auth';
|
|
1
|
+
import { getCurrentUser, getRequestToken } from '@nextcloud/auth';
|
|
2
2
|
import axios__default from '@nextcloud/axios';
|
|
3
3
|
import { generateUrl } from '@nextcloud/router';
|
|
4
4
|
import { ref, computed, watch, getCurrentScope, onScopeDispose, unref } from 'vue';
|
|
@@ -202,7 +202,15 @@ function useObjectPresence(register, schema, objectUuid, options = {}) {
|
|
|
202
202
|
* the browser and survives the navigation, which is the only mechanism
|
|
203
203
|
* that does.
|
|
204
204
|
*
|
|
205
|
+
* 🔴 THE TOKEN RIDES IN THE BODY. A beacon cannot set headers, so the
|
|
206
|
+
* `requesttoken` header axios adds is missing, and the departure route
|
|
207
|
+
* keeps Nextcloud's CSRF check. Nextcloud also reads the token from a
|
|
208
|
+
* POST field, so it goes in a form body. Without it the beacon answers
|
|
209
|
+
* 412; before OpenRegister registered POST on `/presence` it answered 405
|
|
210
|
+
* (seen on cloud.conduction.nl on every dossiq case page).
|
|
211
|
+
*
|
|
205
212
|
* @return {void}
|
|
213
|
+
* @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/object-presence/spec.md#requirement-a-closing-tab-departs-through-a-route-that-exists
|
|
206
214
|
*/
|
|
207
215
|
function beaconDepart() {
|
|
208
216
|
const url = presenceUrl();
|
|
@@ -210,9 +218,12 @@ function useObjectPresence(register, schema, objectUuid, options = {}) {
|
|
|
210
218
|
return
|
|
211
219
|
}
|
|
212
220
|
try {
|
|
213
|
-
// No DELETE from a beacon
|
|
214
|
-
//
|
|
215
|
-
|
|
221
|
+
// No DELETE from a beacon, it is always a POST, so the server's
|
|
222
|
+
// POST departure route is used, marked `_method=DELETE`. Both reach
|
|
223
|
+
// one implementation.
|
|
224
|
+
const body = new FormData();
|
|
225
|
+
body.append('requesttoken', getRequestToken() || '');
|
|
226
|
+
navigator.sendBeacon?.(url.replace(/\/presence$/, '/presence?_method=DELETE'), body);
|
|
216
227
|
} catch {
|
|
217
228
|
// Best effort.
|
|
218
229
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectPresence.js","sources":["../../../src/composables/useObjectPresence.js"],"sourcesContent":["// SPDX-License-Identifier: EUPL-1.2\n// Copyright (C) 2026 Conduction B.V.\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'\nimport { buildObjectKey } from '../store/liveUpdates/eventKeys.js'\nimport { getLiveUpdates } from '../store/liveUpdates/transport.js'\n\n/**\n * How long the server believes a reader after their last beat, in seconds.\n *\n * Mirrored from `PresenceService::WINDOW_SECONDS` and used ONLY as the\n * fallback pace when a server does not answer with its own. The server's\n * `beatSeconds` wins whenever it is present, because the window is its\n * decision and a client pacing itself to a stale copy of it drops off the\n * list while its user is still reading.\n */\nconst FALLBACK_BEAT_SECONDS = 30\n\n/**\n * useObjectPresence — who else has this object open.\n *\n * 🔴 A HEARTBEAT, NOT A CONNECTION. notify_push tells the server nothing about\n * who is looking at what: a socket can be open while the tab showing this\n * object closed ten minutes ago, and a socket can drop while the reader is\n * still there. So the client says \"still here\" on a timer and the server stops\n * believing it after its window. Missing two beats reads as gone, which\n * survives a lost socket where connection tracking does not.\n *\n * 🔑 THE LIST COMES FROM TWO PLACES, AND THAT IS DELIBERATE. Every beat answers\n * the current list, so the component is correct even on an instance with no\n * notify_push at all — it simply learns at beat pace. The push is the\n * OPTIMISATION: it carries the new list on the object's own channel the moment\n * somebody arrives or leaves. Relying on the push alone would make presence a\n * feature that silently does nothing wherever notify_push is not installed,\n * which is most development instances and some production ones.\n *\n * 🔑 DEPARTURE IS SENT TWICE, ON PURPOSE. `tryOnScopeDispose` covers a route\n * change and an unmount; `beforeunload` with `sendBeacon` covers a closed tab,\n * which fires no Vue hook at all. Without the beacon a closed tab lingers for a\n * whole window on everybody else's screen; without the dispose a route change\n * does. Both are cheap and the server is idempotent about it.\n *\n * @param {string|import('vue').Ref<string>} register OpenRegister register slug.\n * @param {string|import('vue').Ref<string>} schema OpenRegister schema slug.\n * @param {string|import('vue').Ref<string>} objectUuid The object being read.\n * @param {object} [options] Optional config.\n * @param {boolean|import('vue').Ref<boolean>|(() => boolean)} [options.enabled] Set false to make the\n * whole thing inert. A ref or getter is read on every use, so a consumer whose answer changes\n * after mount turns the subscription on when it flips.\n * @return {{\n * others: import('vue').ComputedRef<Array<object>>,\n * count: import('vue').ComputedRef<number>,\n * active: import('vue').Ref<boolean>,\n * depart: () => Promise<void>\n * }} The other readers, and a way to leave early.\n */\nexport function useObjectPresence(register, schema, objectUuid, options = {}) {\n\tconst present = ref([])\n\tconst active = ref(false)\n\tlet beatTimer = null\n\tlet subscription = null\n\tlet beatSeconds = FALLBACK_BEAT_SECONDS\n\n\t// A value, a ref or a getter, like the address parts below: a consumer whose\n\t// answer changes after mount (widgets that arrive late) has to be able to say so.\n\tconst enabled = () => unref(typeof options.enabled === 'function' ? options.enabled() : options.enabled) !== false\n\n\t/**\n\t * Read an address part that may be a value, a ref OR a getter.\n\t *\n\t * 🔑 GETTERS MATTER HERE. A detail page reuses one widget host across a\n\t * route change, so the record under a mounted consumer changes. A consumer\n\t * that could only pass a VALUE would leave this beating on the record the\n\t * reader left — the shape that puts a stranger's avatar on your page — and\n\t * `unref` alone does not call a function.\n\t *\n\t * @param {string|import('vue').Ref<string>|(() => string)} source A value, a ref or a getter.\n\t * @return {string} The part.\n\t */\n\tfunction read(source) {\n\t\treturn String((typeof source === 'function' ? source() : unref(source)) || '')\n\t}\n\n\tconst readUuid = () => read(objectUuid)\n\n\t/**\n\t * The presence endpoint of the object currently being read.\n\t *\n\t * Named `presenceUrl` rather than `endpoint` because\n\t * `tests/packaging/api-fetches-are-url-prefixed` recognises module-local\n\t * URL builders BY NAME, alongside `objectsUrl` and `agentsUrl`. A generic\n\t * `endpoint` would have to be exempted globally, which would blanket every\n\t * function of that name in the library.\n\t *\n\t * @return {string} The url, or '' when the object is not resolved yet.\n\t */\n\tfunction presenceUrl() {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tconst i = readUuid()\n\t\tif (!r || !s || !i) {\n\t\t\treturn ''\n\t\t}\n\t\treturn generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(i)}/presence`)\n\t}\n\n\t/**\n\t * Everybody present except the viewer.\n\t *\n\t * The server already leaves the caller out of the beat's answer, but a\n\t * PUSH carries the whole list — it is one payload for every recipient and\n\t * cannot be personalised. Filtering here is what stops a reader seeing\n\t * their own avatar appear the moment somebody else arrives.\n\t */\n\tconst others = computed(() => {\n\t\tconst me = getCurrentUser()?.uid\n\t\treturn present.value.filter((entry) => !me || entry?.user !== me)\n\t})\n\n\tconst count = computed(() => others.value.length)\n\n\t/**\n\t * Replace the list with whatever the server just said.\n\t *\n\t * @param {Array<object>} list The readers.\n\t * @return {void}\n\t */\n\tfunction adopt(list) {\n\t\tpresent.value = Array.isArray(list) ? list : []\n\t}\n\n\t/**\n\t * Say we are still here, and take the answer.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function beat() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !enabled()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst { data } = await axios.put(url)\n\t\t\tadopt(data?.present)\n\t\t\tactive.value = true\n\t\t\t// The server's pace, when it offers one. See FALLBACK_BEAT_SECONDS.\n\t\t\tif (Number.isFinite(data?.beatSeconds) && data.beatSeconds > 0) {\n\t\t\t\tbeatSeconds = data.beatSeconds\n\t\t\t}\n\t\t} catch {\n\t\t\t// A failed beat is NOT an empty list. The reader drops off other\n\t\t\t// people's screens in one window either way, and clearing our own\n\t\t\t// list here would tell this reader they are alone on a page two\n\t\t\t// colleagues are looking at.\n\t\t\tactive.value = false\n\t\t}\n\t}\n\n\t/**\n\t * Say we have gone.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function depart() {\n\t\tstop()\n\t\tawait departFrom(readUuid())\n\t\tpresent.value = []\n\t\tactive.value = false\n\t}\n\n\t/**\n\t * Leave one specific record, which may no longer be the current one.\n\t *\n\t * Takes the uuid rather than reading it, because the watcher calls this\n\t * for the record just LEFT, after the address has already moved on.\n\t *\n\t * @param {string} uuid The record to leave.\n\t * @return {Promise<void>}\n\t */\n\tasync function departFrom(uuid) {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tif (!r || !s || !uuid) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait axios.delete(generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(uuid)}/presence`))\n\t\t} catch {\n\t\t\t// Best effort: the window expires us anyway.\n\t\t}\n\t}\n\n\t/**\n\t * Tell the server we are gone from a tab that is closing.\n\t *\n\t * `sendBeacon` because `beforeunload` gives no time for a promise: an\n\t * axios call from here is cancelled with the page. A beacon is queued by\n\t * the browser and survives the navigation, which is the only mechanism\n\t * that does.\n\t *\n\t * @return {void}\n\t */\n\tfunction beaconDepart() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !active.value) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\t// No DELETE from a beacon — it is always a POST — so the server's\n\t\t\t// own POST release path is used. Both reach one implementation.\n\t\t\tnavigator.sendBeacon?.(url.replace(/\\/presence$/, '/presence?_method=DELETE'))\n\t\t} catch {\n\t\t\t// Best effort.\n\t\t}\n\t}\n\n\t/**\n\t * Begin beating and listen for other people's arrivals.\n\t *\n\t * @return {void}\n\t */\n\tfunction start() {\n\t\tif (!enabled() || beatTimer) {\n\t\t\treturn\n\t\t}\n\n\t\tbeat()\n\t\tbeatTimer = setInterval(beat, beatSeconds * 1000)\n\n\t\ttry {\n\t\t\tconst live = getLiveUpdates()\n\t\t\tsubscription = live.subscribe(\n\t\t\t\tbuildObjectKey(readUuid()),\n\t\t\t\t(event, body) => {\n\t\t\t\t\t// ONLY the presence event. The same channel carries create,\n\t\t\t\t\t// update and delete, and adopting a lifecycle payload's\n\t\t\t\t\t// absent `present` key would blank the list on every save.\n\t\t\t\t\tif (body?.action === 'presence') {\n\t\t\t\t\t\tadopt(body?.present)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ isObject: true },\n\t\t\t)\n\t\t} catch {\n\t\t\t// No transport is not an error: the beats still answer the list,\n\t\t\t// and presence simply updates at beat pace.\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t/**\n\t * Stop beating and listening.\n\t *\n\t * @return {void}\n\t */\n\tfunction stop() {\n\t\tif (beatTimer) {\n\t\t\tclearInterval(beatTimer)\n\t\t\tbeatTimer = null\n\t\t}\n\t\tif (subscription) {\n\t\t\ttry {\n\t\t\t\tgetLiveUpdates().unsubscribe(subscription)\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t// 🔴 STARTED HERE, NOT IN `onMounted`. `onMounted` only fires inside a\n\t// COMPONENT: called from a plain `effectScope`, or from a composable a\n\t// consumer wires up outside setup, it warns and never runs, and presence\n\t// would be a feature that silently does nothing in exactly the places\n\t// somebody would try it first. `tryOnScopeDispose` below works in both, so\n\t// the pairing is scope-based at both ends rather than half component and\n\t// half scope.\n\t//\n\t// Beating before mount is also the behaviour we want: a reader has the\n\t// object open the moment its page is being built, not one frame later.\n\twindow.addEventListener('beforeunload', beaconDepart)\n\tstart()\n\n\t// 🔴 THE RECORD CAN CHANGE UNDER A MOUNTED CONSUMER. A detail page reuses\n\t// one widget host across a route change, so without this the beats would go\n\t// on naming the record the reader has left: they would appear present on a\n\t// page they closed, and absent on the one they are reading. Depart the old\n\t// one first, so the people still on it are told, rather than waiting a whole\n\t// window to find out.\n\twatch(\n\t\t() => readUuid(),\n\t\t(next, previous) => {\n\t\t\tif (next === previous) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstop()\n\t\t\tif (previous) {\n\t\t\t\tdepartFrom(previous)\n\t\t\t}\n\t\t\tpresent.value = []\n\t\t\tactive.value = false\n\t\t\tstart()\n\t\t},\n\t)\n\n\t// A consumer may only learn it needs presence AFTER mount — a detail page\n\t// whose `widgets` arrive with the manifest reads `false` here on the first\n\t// pass. Without this the beat never starts for it, and the row it gates is\n\t// dropped permanently rather than until somebody arrives.\n\twatch(\n\t\t() => enabled(),\n\t\t(on, was) => {\n\t\t\tif (on === was) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (on) {\n\t\t\t\tstart()\n\t\t\t} else {\n\t\t\t\tdepart()\n\t\t\t}\n\t\t},\n\t)\n\n\t// 🔴 VUE'S OWN `onScopeDispose`, NOT `@vueuse/core`'s `tryOnScopeDispose`.\n\t// Measured in this repo's jest on 2026-09-18: a `tryOnScopeDispose`\n\t// callback registered inside `effectScope().run()` does NOT fire on\n\t// `scope.stop()`, while Vue's own does. The cause is the dual-package\n\t// hazard — vueuse resolves its own copy of Vue and asks a different\n\t// `getCurrentScope()` — so the registration silently goes nowhere.\n\t//\n\t// A departure that never fires is the exact silent no-op this whole\n\t// feature is built to avoid: the reader stays on everybody's list for a\n\t// full window after they navigate away. One fewer indirection also means\n\t// one fewer thing that can resolve to the wrong instance.\n\t//\n\t// Guarded, because a consumer may legitimately call this outside a scope;\n\t// there it simply has no automatic cleanup and relies on the beacon and\n\t// the server's window, which is what the guard makes explicit rather than\n\t// leaving to a warning nobody reads.\n\tif (getCurrentScope()) {\n\t\tonScopeDispose(() => {\n\t\t\twindow.removeEventListener('beforeunload', beaconDepart)\n\t\t\t// Fire and forget: the scope is gone, there is nobody to await it.\n\t\t\tdepart()\n\t\t})\n\t}\n\n\treturn { others, count, active, depart }\n}\n"],"names":["axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useObjectPresence.js","sources":["../../../src/composables/useObjectPresence.js"],"sourcesContent":["// SPDX-License-Identifier: EUPL-1.2\n// Copyright (C) 2026 Conduction B.V.\n\nimport { getCurrentUser, getRequestToken } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'\nimport { buildObjectKey } from '../store/liveUpdates/eventKeys.js'\nimport { getLiveUpdates } from '../store/liveUpdates/transport.js'\n\n/**\n * How long the server believes a reader after their last beat, in seconds.\n *\n * Mirrored from `PresenceService::WINDOW_SECONDS` and used ONLY as the\n * fallback pace when a server does not answer with its own. The server's\n * `beatSeconds` wins whenever it is present, because the window is its\n * decision and a client pacing itself to a stale copy of it drops off the\n * list while its user is still reading.\n */\nconst FALLBACK_BEAT_SECONDS = 30\n\n/**\n * useObjectPresence — who else has this object open.\n *\n * 🔴 A HEARTBEAT, NOT A CONNECTION. notify_push tells the server nothing about\n * who is looking at what: a socket can be open while the tab showing this\n * object closed ten minutes ago, and a socket can drop while the reader is\n * still there. So the client says \"still here\" on a timer and the server stops\n * believing it after its window. Missing two beats reads as gone, which\n * survives a lost socket where connection tracking does not.\n *\n * 🔑 THE LIST COMES FROM TWO PLACES, AND THAT IS DELIBERATE. Every beat answers\n * the current list, so the component is correct even on an instance with no\n * notify_push at all — it simply learns at beat pace. The push is the\n * OPTIMISATION: it carries the new list on the object's own channel the moment\n * somebody arrives or leaves. Relying on the push alone would make presence a\n * feature that silently does nothing wherever notify_push is not installed,\n * which is most development instances and some production ones.\n *\n * 🔑 DEPARTURE IS SENT TWICE, ON PURPOSE. `tryOnScopeDispose` covers a route\n * change and an unmount; `beforeunload` with `sendBeacon` covers a closed tab,\n * which fires no Vue hook at all. Without the beacon a closed tab lingers for a\n * whole window on everybody else's screen; without the dispose a route change\n * does. Both are cheap and the server is idempotent about it.\n *\n * @param {string|import('vue').Ref<string>} register OpenRegister register slug.\n * @param {string|import('vue').Ref<string>} schema OpenRegister schema slug.\n * @param {string|import('vue').Ref<string>} objectUuid The object being read.\n * @param {object} [options] Optional config.\n * @param {boolean|import('vue').Ref<boolean>|(() => boolean)} [options.enabled] Set false to make the\n * whole thing inert. A ref or getter is read on every use, so a consumer whose answer changes\n * after mount turns the subscription on when it flips.\n * @return {{\n * others: import('vue').ComputedRef<Array<object>>,\n * count: import('vue').ComputedRef<number>,\n * active: import('vue').Ref<boolean>,\n * depart: () => Promise<void>\n * }} The other readers, and a way to leave early.\n */\nexport function useObjectPresence(register, schema, objectUuid, options = {}) {\n\tconst present = ref([])\n\tconst active = ref(false)\n\tlet beatTimer = null\n\tlet subscription = null\n\tlet beatSeconds = FALLBACK_BEAT_SECONDS\n\n\t// A value, a ref or a getter, like the address parts below: a consumer whose\n\t// answer changes after mount (widgets that arrive late) has to be able to say so.\n\tconst enabled = () => unref(typeof options.enabled === 'function' ? options.enabled() : options.enabled) !== false\n\n\t/**\n\t * Read an address part that may be a value, a ref OR a getter.\n\t *\n\t * 🔑 GETTERS MATTER HERE. A detail page reuses one widget host across a\n\t * route change, so the record under a mounted consumer changes. A consumer\n\t * that could only pass a VALUE would leave this beating on the record the\n\t * reader left — the shape that puts a stranger's avatar on your page — and\n\t * `unref` alone does not call a function.\n\t *\n\t * @param {string|import('vue').Ref<string>|(() => string)} source A value, a ref or a getter.\n\t * @return {string} The part.\n\t */\n\tfunction read(source) {\n\t\treturn String((typeof source === 'function' ? source() : unref(source)) || '')\n\t}\n\n\tconst readUuid = () => read(objectUuid)\n\n\t/**\n\t * The presence endpoint of the object currently being read.\n\t *\n\t * Named `presenceUrl` rather than `endpoint` because\n\t * `tests/packaging/api-fetches-are-url-prefixed` recognises module-local\n\t * URL builders BY NAME, alongside `objectsUrl` and `agentsUrl`. A generic\n\t * `endpoint` would have to be exempted globally, which would blanket every\n\t * function of that name in the library.\n\t *\n\t * @return {string} The url, or '' when the object is not resolved yet.\n\t */\n\tfunction presenceUrl() {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tconst i = readUuid()\n\t\tif (!r || !s || !i) {\n\t\t\treturn ''\n\t\t}\n\t\treturn generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(i)}/presence`)\n\t}\n\n\t/**\n\t * Everybody present except the viewer.\n\t *\n\t * The server already leaves the caller out of the beat's answer, but a\n\t * PUSH carries the whole list — it is one payload for every recipient and\n\t * cannot be personalised. Filtering here is what stops a reader seeing\n\t * their own avatar appear the moment somebody else arrives.\n\t */\n\tconst others = computed(() => {\n\t\tconst me = getCurrentUser()?.uid\n\t\treturn present.value.filter((entry) => !me || entry?.user !== me)\n\t})\n\n\tconst count = computed(() => others.value.length)\n\n\t/**\n\t * Replace the list with whatever the server just said.\n\t *\n\t * @param {Array<object>} list The readers.\n\t * @return {void}\n\t */\n\tfunction adopt(list) {\n\t\tpresent.value = Array.isArray(list) ? list : []\n\t}\n\n\t/**\n\t * Say we are still here, and take the answer.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function beat() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !enabled()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst { data } = await axios.put(url)\n\t\t\tadopt(data?.present)\n\t\t\tactive.value = true\n\t\t\t// The server's pace, when it offers one. See FALLBACK_BEAT_SECONDS.\n\t\t\tif (Number.isFinite(data?.beatSeconds) && data.beatSeconds > 0) {\n\t\t\t\tbeatSeconds = data.beatSeconds\n\t\t\t}\n\t\t} catch {\n\t\t\t// A failed beat is NOT an empty list. The reader drops off other\n\t\t\t// people's screens in one window either way, and clearing our own\n\t\t\t// list here would tell this reader they are alone on a page two\n\t\t\t// colleagues are looking at.\n\t\t\tactive.value = false\n\t\t}\n\t}\n\n\t/**\n\t * Say we have gone.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function depart() {\n\t\tstop()\n\t\tawait departFrom(readUuid())\n\t\tpresent.value = []\n\t\tactive.value = false\n\t}\n\n\t/**\n\t * Leave one specific record, which may no longer be the current one.\n\t *\n\t * Takes the uuid rather than reading it, because the watcher calls this\n\t * for the record just LEFT, after the address has already moved on.\n\t *\n\t * @param {string} uuid The record to leave.\n\t * @return {Promise<void>}\n\t */\n\tasync function departFrom(uuid) {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tif (!r || !s || !uuid) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait axios.delete(generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(uuid)}/presence`))\n\t\t} catch {\n\t\t\t// Best effort: the window expires us anyway.\n\t\t}\n\t}\n\n\t/**\n\t * Tell the server we are gone from a tab that is closing.\n\t *\n\t * `sendBeacon` because `beforeunload` gives no time for a promise: an\n\t * axios call from here is cancelled with the page. A beacon is queued by\n\t * the browser and survives the navigation, which is the only mechanism\n\t * that does.\n\t *\n\t * 🔴 THE TOKEN RIDES IN THE BODY. A beacon cannot set headers, so the\n\t * `requesttoken` header axios adds is missing, and the departure route\n\t * keeps Nextcloud's CSRF check. Nextcloud also reads the token from a\n\t * POST field, so it goes in a form body. Without it the beacon answers\n\t * 412; before OpenRegister registered POST on `/presence` it answered 405\n\t * (seen on cloud.conduction.nl on every dossiq case page).\n\t *\n\t * @return {void}\n\t * @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/object-presence/spec.md#requirement-a-closing-tab-departs-through-a-route-that-exists\n\t */\n\tfunction beaconDepart() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !active.value) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\t// No DELETE from a beacon, it is always a POST, so the server's\n\t\t\t// POST departure route is used, marked `_method=DELETE`. Both reach\n\t\t\t// one implementation.\n\t\t\tconst body = new FormData()\n\t\t\tbody.append('requesttoken', getRequestToken() || '')\n\t\t\tnavigator.sendBeacon?.(url.replace(/\\/presence$/, '/presence?_method=DELETE'), body)\n\t\t} catch {\n\t\t\t// Best effort.\n\t\t}\n\t}\n\n\t/**\n\t * Begin beating and listen for other people's arrivals.\n\t *\n\t * @return {void}\n\t */\n\tfunction start() {\n\t\tif (!enabled() || beatTimer) {\n\t\t\treturn\n\t\t}\n\n\t\tbeat()\n\t\tbeatTimer = setInterval(beat, beatSeconds * 1000)\n\n\t\ttry {\n\t\t\tconst live = getLiveUpdates()\n\t\t\tsubscription = live.subscribe(\n\t\t\t\tbuildObjectKey(readUuid()),\n\t\t\t\t(event, body) => {\n\t\t\t\t\t// ONLY the presence event. The same channel carries create,\n\t\t\t\t\t// update and delete, and adopting a lifecycle payload's\n\t\t\t\t\t// absent `present` key would blank the list on every save.\n\t\t\t\t\tif (body?.action === 'presence') {\n\t\t\t\t\t\tadopt(body?.present)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ isObject: true },\n\t\t\t)\n\t\t} catch {\n\t\t\t// No transport is not an error: the beats still answer the list,\n\t\t\t// and presence simply updates at beat pace.\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t/**\n\t * Stop beating and listening.\n\t *\n\t * @return {void}\n\t */\n\tfunction stop() {\n\t\tif (beatTimer) {\n\t\t\tclearInterval(beatTimer)\n\t\t\tbeatTimer = null\n\t\t}\n\t\tif (subscription) {\n\t\t\ttry {\n\t\t\t\tgetLiveUpdates().unsubscribe(subscription)\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t// 🔴 STARTED HERE, NOT IN `onMounted`. `onMounted` only fires inside a\n\t// COMPONENT: called from a plain `effectScope`, or from a composable a\n\t// consumer wires up outside setup, it warns and never runs, and presence\n\t// would be a feature that silently does nothing in exactly the places\n\t// somebody would try it first. `tryOnScopeDispose` below works in both, so\n\t// the pairing is scope-based at both ends rather than half component and\n\t// half scope.\n\t//\n\t// Beating before mount is also the behaviour we want: a reader has the\n\t// object open the moment its page is being built, not one frame later.\n\twindow.addEventListener('beforeunload', beaconDepart)\n\tstart()\n\n\t// 🔴 THE RECORD CAN CHANGE UNDER A MOUNTED CONSUMER. A detail page reuses\n\t// one widget host across a route change, so without this the beats would go\n\t// on naming the record the reader has left: they would appear present on a\n\t// page they closed, and absent on the one they are reading. Depart the old\n\t// one first, so the people still on it are told, rather than waiting a whole\n\t// window to find out.\n\twatch(\n\t\t() => readUuid(),\n\t\t(next, previous) => {\n\t\t\tif (next === previous) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstop()\n\t\t\tif (previous) {\n\t\t\t\tdepartFrom(previous)\n\t\t\t}\n\t\t\tpresent.value = []\n\t\t\tactive.value = false\n\t\t\tstart()\n\t\t},\n\t)\n\n\t// A consumer may only learn it needs presence AFTER mount — a detail page\n\t// whose `widgets` arrive with the manifest reads `false` here on the first\n\t// pass. Without this the beat never starts for it, and the row it gates is\n\t// dropped permanently rather than until somebody arrives.\n\twatch(\n\t\t() => enabled(),\n\t\t(on, was) => {\n\t\t\tif (on === was) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (on) {\n\t\t\t\tstart()\n\t\t\t} else {\n\t\t\t\tdepart()\n\t\t\t}\n\t\t},\n\t)\n\n\t// 🔴 VUE'S OWN `onScopeDispose`, NOT `@vueuse/core`'s `tryOnScopeDispose`.\n\t// Measured in this repo's jest on 2026-09-18: a `tryOnScopeDispose`\n\t// callback registered inside `effectScope().run()` does NOT fire on\n\t// `scope.stop()`, while Vue's own does. The cause is the dual-package\n\t// hazard — vueuse resolves its own copy of Vue and asks a different\n\t// `getCurrentScope()` — so the registration silently goes nowhere.\n\t//\n\t// A departure that never fires is the exact silent no-op this whole\n\t// feature is built to avoid: the reader stays on everybody's list for a\n\t// full window after they navigate away. One fewer indirection also means\n\t// one fewer thing that can resolve to the wrong instance.\n\t//\n\t// Guarded, because a consumer may legitimately call this outside a scope;\n\t// there it simply has no automatic cleanup and relies on the beacon and\n\t// the server's window, which is what the guard makes explicit rather than\n\t// leaving to a warning nobody reads.\n\tif (getCurrentScope()) {\n\t\tonScopeDispose(() => {\n\t\t\twindow.removeEventListener('beforeunload', beaconDepart)\n\t\t\t// Fire and forget: the scope is gone, there is nobody to await it.\n\t\t\tdepart()\n\t\t})\n\t}\n\n\treturn { others, count, active, depart }\n}\n"],"names":["axios"],"mappings":"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"}
|
package/dist/esm/l10n/en.json.js
CHANGED
|
@@ -113,6 +113,7 @@ var translations = {
|
|
|
113
113
|
"Saved just now": "Saved just now",
|
|
114
114
|
"Saved {time}": "Saved {time}",
|
|
115
115
|
Saved: "Saved",
|
|
116
|
+
"Draft kept on this device": "Draft kept on this device",
|
|
116
117
|
Publish: "Publish",
|
|
117
118
|
"Edit {title} (draft)": "Edit {title} (draft)",
|
|
118
119
|
"Also search inside files": "Also search inside files",
|
|
@@ -452,6 +453,12 @@ var translations = {
|
|
|
452
453
|
"NC Activity is currently unavailable.": "NC Activity is currently unavailable.",
|
|
453
454
|
"Could not load activity.": "Could not load activity.",
|
|
454
455
|
Today: "Today",
|
|
456
|
+
"Last 8 hours": "Last 8 hours",
|
|
457
|
+
"Last 24 hours": "Last 24 hours",
|
|
458
|
+
"Last 7 days": "Last 7 days",
|
|
459
|
+
"Last 30 days": "Last 30 days",
|
|
460
|
+
"Last 90 days": "Last 90 days",
|
|
461
|
+
"Custom range": "Custom range",
|
|
455
462
|
Yesterday: "Yesterday",
|
|
456
463
|
Earlier: "Earlier",
|
|
457
464
|
System: "System",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/nl.json.js
CHANGED
|
@@ -113,6 +113,7 @@ var translations = {
|
|
|
113
113
|
"Saved just now": "Zojuist opgeslagen",
|
|
114
114
|
"Saved {time}": "Opgeslagen {time}",
|
|
115
115
|
Saved: "Opgeslagen",
|
|
116
|
+
"Draft kept on this device": "Concept bewaard op dit apparaat",
|
|
116
117
|
"Edit {title} (draft)": "{title} bewerken (concept)",
|
|
117
118
|
"Also search inside files": "Zoek ook in bestanden",
|
|
118
119
|
"File matches are limited to the best 50.": "Bestandstreffers zijn beperkt tot de beste 50.",
|
|
@@ -451,6 +452,12 @@ var translations = {
|
|
|
451
452
|
"NC Activity is currently unavailable.": "NC Activiteit is momenteel niet beschikbaar.",
|
|
452
453
|
"Could not load activity.": "Kon activiteit niet laden.",
|
|
453
454
|
Today: "Vandaag",
|
|
455
|
+
"Last 8 hours": "Laatste 8 uur",
|
|
456
|
+
"Last 24 hours": "Laatste 24 uur",
|
|
457
|
+
"Last 7 days": "Laatste 7 dagen",
|
|
458
|
+
"Last 30 days": "Laatste 30 dagen",
|
|
459
|
+
"Last 90 days": "Laatste 90 dagen",
|
|
460
|
+
"Custom range": "Aangepaste periode",
|
|
454
461
|
Yesterday: "Gisteren",
|
|
455
462
|
Earlier: "Eerder",
|
|
456
463
|
System: "Systeem",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -502,6 +502,14 @@
|
|
|
502
502
|
color: var(--color-main-text);
|
|
503
503
|
opacity: 1;
|
|
504
504
|
}
|
|
505
|
+
|
|
506
|
+
/* ---- Status pills (screens-cell-pill-parity) ---------------------------- */
|
|
507
|
+
/* The board draws a primary pill in the deep primary ink on the light primary
|
|
508
|
+
ground ("Verzoek", "Nieuw"); the default look keeps the brighter
|
|
509
|
+
primary-element text. The other tones already match the board. */
|
|
510
|
+
.cn-look-board .cn-status-badge--primary:not(.cn-status-badge--solid):not(.cn-look-nextcloud *) {
|
|
511
|
+
color: var(--color-primary-element-light-text);
|
|
512
|
+
}
|
|
505
513
|
/*
|
|
506
514
|
* The board dialog (screens-dialog-parity, canon section 5).
|
|
507
515
|
*
|
|
@@ -1192,6 +1200,42 @@
|
|
|
1192
1200
|
margin-inline-start: 0;
|
|
1193
1201
|
}
|
|
1194
1202
|
|
|
1203
|
+
/* The header buttons keep the board's button under a theme that styles every
|
|
1204
|
+
secondary or primary button with `!important` (the nldesign theme: a
|
|
1205
|
+
primary border, 8px 16px padding): Download and the Actions menu are 40px,
|
|
1206
|
+
a grey border on the main background (PqTickets, DqZaken), the primary
|
|
1207
|
+
button is the primary fill without a border. The buildiq square is not a
|
|
1208
|
+
direct child button and keeps its own rule. */
|
|
1209
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--secondary,
|
|
1210
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .action-item .button-vue {
|
|
1211
|
+
height: var(--cn-board-control-height) !important;
|
|
1212
|
+
min-height: var(--cn-board-control-height) !important;
|
|
1213
|
+
padding: 0 14px !important;
|
|
1214
|
+
border: 1px solid var(--color-border-dark) !important;
|
|
1215
|
+
border-radius: var(--cn-board-control-radius) !important;
|
|
1216
|
+
background: var(--color-main-background) !important;
|
|
1217
|
+
color: var(--color-main-text) !important;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--secondary:hover,
|
|
1221
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .action-item .button-vue:hover {
|
|
1222
|
+
background: var(--color-background-hover) !important;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--primary {
|
|
1226
|
+
height: var(--cn-board-control-height) !important;
|
|
1227
|
+
min-height: var(--cn-board-control-height) !important;
|
|
1228
|
+
padding: 0 16px !important;
|
|
1229
|
+
border: 0 !important;
|
|
1230
|
+
border-radius: var(--cn-board-control-radius) !important;
|
|
1231
|
+
background: var(--color-primary-element) !important;
|
|
1232
|
+
color: var(--color-primary-element-text) !important;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--primary:hover {
|
|
1236
|
+
background: var(--color-primary-element-hover) !important;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1195
1239
|
/* ----------------------------------------------------------------- toolbar */
|
|
1196
1240
|
|
|
1197
1241
|
/* No band, no padding, no radius. The DOM is already in reading order (row 1,
|
|
@@ -3346,6 +3390,57 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
3346
3390
|
gap: 6px;
|
|
3347
3391
|
}
|
|
3348
3392
|
|
|
3393
|
+
/* A linked board tile is one link: the label is not underlined link text, in
|
|
3394
|
+
any layout (screens-stat-tile-parity). The NL Design theme underlines every
|
|
3395
|
+
`a` with `!important`, so the opt-out needs it too. */
|
|
3396
|
+
a.cn-kpi-card--board,
|
|
3397
|
+
a.cn-kpi-card--board:hover,
|
|
3398
|
+
a.cn-kpi-card--board:focus-visible,
|
|
3399
|
+
a.cn-kpi-card--board * {
|
|
3400
|
+
text-decoration: none !important;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
/* `content.iconPlacement: "end"` (DqDashboard): label left, the icon in a
|
|
3404
|
+
32px circle right, tinted by the tile's state. */
|
|
3405
|
+
.cn-kpi-card--board .cn-kpi-card__title--badge-end {
|
|
3406
|
+
justify-content: space-between;
|
|
3407
|
+
gap: 8px;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
.cn-kpi-card__badge {
|
|
3411
|
+
display: inline-flex;
|
|
3412
|
+
flex: 0 0 auto;
|
|
3413
|
+
align-items: center;
|
|
3414
|
+
justify-content: center;
|
|
3415
|
+
width: 32px;
|
|
3416
|
+
height: 32px;
|
|
3417
|
+
margin-inline-start: auto;
|
|
3418
|
+
border-radius: 50%;
|
|
3419
|
+
color: var(--color-primary-element-light-text, var(--color-primary-element));
|
|
3420
|
+
background: var(--color-primary-element-light, color-mix(in srgb, var(--color-primary-element) 12%, transparent));
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
.cn-kpi-card__badge svg {
|
|
3424
|
+
width: 18px;
|
|
3425
|
+
height: 18px;
|
|
3426
|
+
fill: currentcolor;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
.cn-kpi-card__badge--success {
|
|
3430
|
+
color: var(--color-text-success, var(--color-success-text));
|
|
3431
|
+
background: color-mix(in srgb, var(--color-text-success, var(--color-success-text)) 12%, var(--color-main-background));
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
.cn-kpi-card__badge--warning {
|
|
3435
|
+
color: var(--color-element-warning, var(--color-warning-text));
|
|
3436
|
+
background: color-mix(in srgb, var(--color-element-warning, var(--color-warning-text)) 12%, var(--color-main-background));
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
.cn-kpi-card__badge--error {
|
|
3440
|
+
color: var(--color-text-error, var(--color-error-text));
|
|
3441
|
+
background: color-mix(in srgb, var(--color-text-error, var(--color-error-text)) 10%, var(--color-main-background));
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3349
3444
|
.cn-kpi-card__glyph {
|
|
3350
3445
|
display: inline-flex;
|
|
3351
3446
|
flex: 0 0 auto;
|
|
@@ -3592,6 +3687,15 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
3592
3687
|
@conduction/nextcloud-vue — Status Badge
|
|
3593
3688
|
======================================== */
|
|
3594
3689
|
|
|
3690
|
+
/* The two board tones without a Nextcloud colour role (screens-cell-pill-parity).
|
|
3691
|
+
A theme may redefine them; the six role variants below use Nextcloud's own. */
|
|
3692
|
+
:root {
|
|
3693
|
+
--cn-status-purple-bg: #e8e6f6;
|
|
3694
|
+
--cn-status-purple-text: #4a3f8f;
|
|
3695
|
+
--cn-status-teal-bg: #e3f1f3;
|
|
3696
|
+
--cn-status-teal-text: #1d5e66;
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3595
3699
|
.cn-status-badge {
|
|
3596
3700
|
display: inline-flex;
|
|
3597
3701
|
align-items: center;
|
|
@@ -3641,6 +3745,18 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
3641
3745
|
color: var(--color-info-text, var(--color-info, #0082c9));
|
|
3642
3746
|
}
|
|
3643
3747
|
|
|
3748
|
+
/* Work in progress ("In behandeling"), a board tone. */
|
|
3749
|
+
.cn-status-badge--purple {
|
|
3750
|
+
background-color: var(--cn-status-purple-bg);
|
|
3751
|
+
color: var(--cn-status-purple-text);
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
/* Handed on ("Omgezet naar een zaak"), a board tone. */
|
|
3755
|
+
.cn-status-badge--teal {
|
|
3756
|
+
background-color: var(--cn-status-teal-bg);
|
|
3757
|
+
color: var(--cn-status-teal-text);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3644
3760
|
/* Solid variants — solid background with white text for use on colored backgrounds */
|
|
3645
3761
|
.cn-status-badge--solid.cn-status-badge--default {
|
|
3646
3762
|
background-color: var(--color-background-dark);
|
|
@@ -3671,6 +3787,16 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
3671
3787
|
background-color: var(--color-info, #0082c9);
|
|
3672
3788
|
color: white;
|
|
3673
3789
|
}
|
|
3790
|
+
|
|
3791
|
+
.cn-status-badge--solid.cn-status-badge--purple {
|
|
3792
|
+
background-color: var(--cn-status-purple-text);
|
|
3793
|
+
color: white;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
.cn-status-badge--solid.cn-status-badge--teal {
|
|
3797
|
+
background-color: var(--cn-status-teal-text);
|
|
3798
|
+
color: white;
|
|
3799
|
+
}
|
|
3674
3800
|
/* ========================================
|
|
3675
3801
|
@conduction/nextcloud-vue — Layout & Filter Utility Styles
|
|
3676
3802
|
======================================== */
|
|
@@ -9122,8 +9248,11 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
|
|
9122
9248
|
.cn-cell-renderer__date--error[data-v-37d7b88f] {
|
|
9123
9249
|
color: var(--color-text-error, var(--color-error-text));
|
|
9124
9250
|
}
|
|
9251
|
+
|
|
9252
|
+
/* The text variant, not the fill: from Nextcloud 32 `--color-success` is a
|
|
9253
|
+
background colour and too pale for a 16px glyph on a light theme. */
|
|
9125
9254
|
.cn-cell-renderer__icon--success[data-v-37d7b88f] {
|
|
9126
|
-
color: var(--color-success);
|
|
9255
|
+
color: var(--color-text-success, var(--color-success-text, var(--color-success)));
|
|
9127
9256
|
}
|
|
9128
9257
|
.cn-cell-renderer__swatch-wrap[data-v-37d7b88f] {
|
|
9129
9258
|
display: inline-flex;
|
|
@@ -20833,6 +20962,57 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
20833
20962
|
gap: 6px;
|
|
20834
20963
|
}
|
|
20835
20964
|
|
|
20965
|
+
/* A linked board tile is one link: the label is not underlined link text, in
|
|
20966
|
+
any layout (screens-stat-tile-parity). The NL Design theme underlines every
|
|
20967
|
+
`a` with `!important`, so the opt-out needs it too. */
|
|
20968
|
+
a.cn-kpi-card--board,
|
|
20969
|
+
a.cn-kpi-card--board:hover,
|
|
20970
|
+
a.cn-kpi-card--board:focus-visible,
|
|
20971
|
+
a.cn-kpi-card--board * {
|
|
20972
|
+
text-decoration: none !important;
|
|
20973
|
+
}
|
|
20974
|
+
|
|
20975
|
+
/* `content.iconPlacement: "end"` (DqDashboard): label left, the icon in a
|
|
20976
|
+
32px circle right, tinted by the tile's state. */
|
|
20977
|
+
.cn-kpi-card--board .cn-kpi-card__title--badge-end {
|
|
20978
|
+
justify-content: space-between;
|
|
20979
|
+
gap: 8px;
|
|
20980
|
+
}
|
|
20981
|
+
|
|
20982
|
+
.cn-kpi-card__badge {
|
|
20983
|
+
display: inline-flex;
|
|
20984
|
+
flex: 0 0 auto;
|
|
20985
|
+
align-items: center;
|
|
20986
|
+
justify-content: center;
|
|
20987
|
+
width: 32px;
|
|
20988
|
+
height: 32px;
|
|
20989
|
+
margin-inline-start: auto;
|
|
20990
|
+
border-radius: 50%;
|
|
20991
|
+
color: var(--color-primary-element-light-text, var(--color-primary-element));
|
|
20992
|
+
background: var(--color-primary-element-light, color-mix(in srgb, var(--color-primary-element) 12%, transparent));
|
|
20993
|
+
}
|
|
20994
|
+
|
|
20995
|
+
.cn-kpi-card__badge svg {
|
|
20996
|
+
width: 18px;
|
|
20997
|
+
height: 18px;
|
|
20998
|
+
fill: currentcolor;
|
|
20999
|
+
}
|
|
21000
|
+
|
|
21001
|
+
.cn-kpi-card__badge--success {
|
|
21002
|
+
color: var(--color-text-success, var(--color-success-text));
|
|
21003
|
+
background: color-mix(in srgb, var(--color-text-success, var(--color-success-text)) 12%, var(--color-main-background));
|
|
21004
|
+
}
|
|
21005
|
+
|
|
21006
|
+
.cn-kpi-card__badge--warning {
|
|
21007
|
+
color: var(--color-element-warning, var(--color-warning-text));
|
|
21008
|
+
background: color-mix(in srgb, var(--color-element-warning, var(--color-warning-text)) 12%, var(--color-main-background));
|
|
21009
|
+
}
|
|
21010
|
+
|
|
21011
|
+
.cn-kpi-card__badge--error {
|
|
21012
|
+
color: var(--color-text-error, var(--color-error-text));
|
|
21013
|
+
background: color-mix(in srgb, var(--color-text-error, var(--color-error-text)) 10%, var(--color-main-background));
|
|
21014
|
+
}
|
|
21015
|
+
|
|
20836
21016
|
.cn-kpi-card__glyph {
|
|
20837
21017
|
display: inline-flex;
|
|
20838
21018
|
flex: 0 0 auto;
|
|
@@ -26550,6 +26730,15 @@ tr.file-picker__row td.row-name[data-v-1f96131b] {
|
|
|
26550
26730
|
@conduction/nextcloud-vue — Status Badge
|
|
26551
26731
|
======================================== */
|
|
26552
26732
|
|
|
26733
|
+
/* The two board tones without a Nextcloud colour role (screens-cell-pill-parity).
|
|
26734
|
+
A theme may redefine them; the six role variants below use Nextcloud's own. */
|
|
26735
|
+
:root {
|
|
26736
|
+
--cn-status-purple-bg: #e8e6f6;
|
|
26737
|
+
--cn-status-purple-text: #4a3f8f;
|
|
26738
|
+
--cn-status-teal-bg: #e3f1f3;
|
|
26739
|
+
--cn-status-teal-text: #1d5e66;
|
|
26740
|
+
}
|
|
26741
|
+
|
|
26553
26742
|
.cn-status-badge {
|
|
26554
26743
|
display: inline-flex;
|
|
26555
26744
|
align-items: center;
|
|
@@ -26599,6 +26788,18 @@ tr.file-picker__row td.row-name[data-v-1f96131b] {
|
|
|
26599
26788
|
color: var(--color-info-text, var(--color-info, #0082c9));
|
|
26600
26789
|
}
|
|
26601
26790
|
|
|
26791
|
+
/* Work in progress ("In behandeling"), a board tone. */
|
|
26792
|
+
.cn-status-badge--purple {
|
|
26793
|
+
background-color: var(--cn-status-purple-bg);
|
|
26794
|
+
color: var(--cn-status-purple-text);
|
|
26795
|
+
}
|
|
26796
|
+
|
|
26797
|
+
/* Handed on ("Omgezet naar een zaak"), a board tone. */
|
|
26798
|
+
.cn-status-badge--teal {
|
|
26799
|
+
background-color: var(--cn-status-teal-bg);
|
|
26800
|
+
color: var(--cn-status-teal-text);
|
|
26801
|
+
}
|
|
26802
|
+
|
|
26602
26803
|
/* Solid variants — solid background with white text for use on colored backgrounds */
|
|
26603
26804
|
.cn-status-badge--solid.cn-status-badge--default {
|
|
26604
26805
|
background-color: var(--color-background-dark);
|
|
@@ -26630,6 +26831,16 @@ tr.file-picker__row td.row-name[data-v-1f96131b] {
|
|
|
26630
26831
|
color: white;
|
|
26631
26832
|
}
|
|
26632
26833
|
|
|
26834
|
+
.cn-status-badge--solid.cn-status-badge--purple {
|
|
26835
|
+
background-color: var(--cn-status-purple-text);
|
|
26836
|
+
color: white;
|
|
26837
|
+
}
|
|
26838
|
+
|
|
26839
|
+
.cn-status-badge--solid.cn-status-badge--teal {
|
|
26840
|
+
background-color: var(--cn-status-teal-text);
|
|
26841
|
+
color: white;
|
|
26842
|
+
}
|
|
26843
|
+
|
|
26633
26844
|
|
|
26634
26845
|
/*
|
|
26635
26846
|
* The card itself — its box, body stack, icon circle, title and value
|