@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
|
@@ -1646,19 +1646,24 @@ export default {
|
|
|
1646
1646
|
* handed a `:schema` for at all) still get type-aware rendering.
|
|
1647
1647
|
* `enumLabels` rides with `enum` rather than getting its own `if`
|
|
1648
1648
|
* branch: a column can't usefully declare labels without also
|
|
1649
|
-
* declaring the codes they label.
|
|
1649
|
+
* declaring the codes they label. A column `colorMap` (raw value to
|
|
1650
|
+
* badge variant) colours the enum pill from the manifest and wins over
|
|
1651
|
+
* the schema property's own `colorMap` / `x-color-map`.
|
|
1650
1652
|
*
|
|
1651
1653
|
* @param {object} col Column definition.
|
|
1652
1654
|
* @return {object} Property definition for CnCellRenderer.
|
|
1655
|
+
* @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-manifest-column-colours-its-enum-pills
|
|
1653
1656
|
*/
|
|
1654
1657
|
columnProperty(col) {
|
|
1655
1658
|
const base = this.getSchemaProperty(col.key)
|
|
1656
|
-
|
|
1659
|
+
const colorMap = col && col.colorMap && typeof col.colorMap === 'object' && !Array.isArray(col.colorMap) ? col.colorMap : null
|
|
1660
|
+
if (col && (col.format || col.type || col.enum || colorMap)) {
|
|
1657
1661
|
return {
|
|
1658
1662
|
...base,
|
|
1659
1663
|
...(col.type ? { type: col.type } : {}),
|
|
1660
1664
|
...(col.format ? { format: col.format } : {}),
|
|
1661
1665
|
...(col.enum ? { enum: col.enum, enumLabels: col.enumLabels || base.enumLabels } : {}),
|
|
1666
|
+
...(colorMap ? { colorMap } : {}),
|
|
1662
1667
|
}
|
|
1663
1668
|
}
|
|
1664
1669
|
return base
|
|
@@ -69,6 +69,44 @@ export const DEFAULT_DATE_RANGE_PRESETS = Object.freeze([
|
|
|
69
69
|
{ id: 'custom', label: 'Custom range', days: null },
|
|
70
70
|
])
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The library's own translations of the default preset labels. Literal
|
|
74
|
+
* `t()` calls, so the strings are extracted into the `nextcloud-vue`
|
|
75
|
+
* catalogue.
|
|
76
|
+
*
|
|
77
|
+
* @type {{[label: string]: function(): string}}
|
|
78
|
+
*/
|
|
79
|
+
const LIBRARY_PRESET_LABELS = Object.freeze({
|
|
80
|
+
'Last 8 hours': () => t('nextcloud-vue', 'Last 8 hours'),
|
|
81
|
+
'Last 24 hours': () => t('nextcloud-vue', 'Last 24 hours'),
|
|
82
|
+
Today: () => t('nextcloud-vue', 'Today'),
|
|
83
|
+
'Last 7 days': () => t('nextcloud-vue', 'Last 7 days'),
|
|
84
|
+
'Last 30 days': () => t('nextcloud-vue', 'Last 30 days'),
|
|
85
|
+
'Last 90 days': () => t('nextcloud-vue', 'Last 90 days'),
|
|
86
|
+
'Custom range': () => t('nextcloud-vue', 'Custom range'),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A preset label in the user's language: the host translation when the app's
|
|
91
|
+
* catalogue has the key, else the library's own translation of a default
|
|
92
|
+
* label ("Last 30 days"), else the label as written.
|
|
93
|
+
*
|
|
94
|
+
* @spec openspec/changes/screens-dashboard-i18n/specs/dashboard-page/spec.md#requirement-date-range-preset-labels-are-translated
|
|
95
|
+
* @param {string} label The preset label (an i18n key).
|
|
96
|
+
* @param {function(string): string} [translate] The host translate function.
|
|
97
|
+
* @return {string} The translated label.
|
|
98
|
+
*/
|
|
99
|
+
export function translatePresetLabel(label, translate) {
|
|
100
|
+
if (typeof label !== 'string' || label === '') {
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
const own = typeof translate === 'function' ? translate(label) : label
|
|
104
|
+
if (typeof own === 'string' && own !== '' && own !== label) {
|
|
105
|
+
return own
|
|
106
|
+
}
|
|
107
|
+
return Object.hasOwn(LIBRARY_PRESET_LABELS, label) ? LIBRARY_PRESET_LABELS[label]() : label
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
/**
|
|
73
111
|
* Resolve a `period` preset into its calendar-aligned `{ from, to }`
|
|
74
112
|
* window — the CURRENT week / month / quarter / year to date, not a
|
|
@@ -196,6 +234,11 @@ export default {
|
|
|
196
234
|
NcSelect,
|
|
197
235
|
},
|
|
198
236
|
|
|
237
|
+
inject: {
|
|
238
|
+
/** Host translate function provided by CnAppRoot, for app-authored preset labels. */
|
|
239
|
+
cnTranslate: { default: () => (key) => key },
|
|
240
|
+
},
|
|
241
|
+
|
|
199
242
|
props: {
|
|
200
243
|
/**
|
|
201
244
|
* Current range value. `from` / `to` are ISO-8601 UTC strings;
|
|
@@ -327,7 +370,7 @@ export default {
|
|
|
327
370
|
},
|
|
328
371
|
|
|
329
372
|
presetOptions() {
|
|
330
|
-
return this.presets.map((p) => ({ id: p.id, label: p.label, days: p.days }))
|
|
373
|
+
return this.presets.map((p) => ({ id: p.id, label: translatePresetLabel(p.label, this.cnTranslate), days: p.days }))
|
|
331
374
|
},
|
|
332
375
|
|
|
333
376
|
selectedPresetOption() {
|
|
@@ -1466,11 +1466,17 @@ export default {
|
|
|
1466
1466
|
|
|
1467
1467
|
/**
|
|
1468
1468
|
* What the draft indicator announces, or '' when it says nothing.
|
|
1469
|
+
* While the dialog shows a failed save it never says Saved: it says
|
|
1470
|
+
* the draft is kept on this device, so it cannot be read as the
|
|
1471
|
+
* server save that just failed.
|
|
1469
1472
|
*
|
|
1470
1473
|
* @return {string} The text.
|
|
1474
|
+
* @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
|
|
1471
1475
|
*/
|
|
1472
1476
|
draftIndicatorLabel() {
|
|
1473
|
-
|
|
1477
|
+
const saveFailed = this.result !== null && typeof this.result === 'object'
|
|
1478
|
+
&& (Boolean(this.result.error) || this.result.success === false)
|
|
1479
|
+
return draftIndicatorText(this.draftState, this.draftSavedAt, Date.now(), { saveFailed })
|
|
1474
1480
|
},
|
|
1475
1481
|
|
|
1476
1482
|
isCreateMode() {
|
|
@@ -954,11 +954,14 @@ export default {
|
|
|
954
954
|
|
|
955
955
|
/**
|
|
956
956
|
* What the draft indicator announces: Saving, then Saved with a relative time.
|
|
957
|
+
* After a failed submit it says the draft is kept on this device instead,
|
|
958
|
+
* so it cannot be read as the server save that just failed.
|
|
957
959
|
*
|
|
958
960
|
* @return {string} The text, or '' before anything is typed.
|
|
961
|
+
* @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
|
|
959
962
|
*/
|
|
960
963
|
draftIndicatorLabel() {
|
|
961
|
-
return draftIndicatorText(this.draftState, this.draftSavedAt)
|
|
964
|
+
return draftIndicatorText(this.draftState, this.draftSavedAt, Date.now(), { saveFailed: Boolean(this.lastError) })
|
|
962
965
|
},
|
|
963
966
|
},
|
|
964
967
|
|
|
@@ -5156,6 +5156,7 @@ export default {
|
|
|
5156
5156
|
* stable key and a variant.
|
|
5157
5157
|
*
|
|
5158
5158
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
5159
|
+
* @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
|
|
5159
5160
|
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
|
|
5160
5161
|
*/
|
|
5161
5162
|
resolvedHeaderButtons() {
|
|
@@ -5168,6 +5169,11 @@ export default {
|
|
|
5168
5169
|
if (label === '' && button.action === 'add') {
|
|
5169
5170
|
label = this.resolvedAddLabel
|
|
5170
5171
|
}
|
|
5172
|
+
// The board's primary button reads "+ New request": the Add
|
|
5173
|
+
// button takes the plus unless the manifest names an icon.
|
|
5174
|
+
if (this.isBoardLook && button.action === 'add' && icon === '') {
|
|
5175
|
+
icon = 'Plus'
|
|
5176
|
+
}
|
|
5171
5177
|
// The board's header: Export reads "Download" with its icon and
|
|
5172
5178
|
// the Actions menu reads "Actions", without the manifest saying so.
|
|
5173
5179
|
if (this.isBoardLook && label === '' && button.action === 'export') {
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
<div class="cn-kpi-card__body cn-stat-widget__body">
|
|
36
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
v-if="content.label || rangePresets.length"
|
|
38
|
+
class="cn-kpi-card__title cn-stat-widget__label"
|
|
39
|
+
:class="{ 'cn-kpi-card__title--badge-end': hasEndBadge }">
|
|
37
40
|
<!-- Board look: the icon is an 18px glyph before the label, and
|
|
38
41
|
only when the tile links to a filtered list. -->
|
|
39
42
|
<span
|
|
40
|
-
v-if="isBoardLook && isLinked && resolvedIcon"
|
|
43
|
+
v-if="isBoardLook && isLinked && resolvedIcon && !hasEndBadge"
|
|
41
44
|
class="cn-kpi-card__glyph cn-stat-widget__glyph"
|
|
42
45
|
data-testid="cn-stat-widget-glyph"
|
|
43
46
|
aria-hidden="true">
|
|
@@ -66,6 +69,17 @@
|
|
|
66
69
|
{{ effectiveTranslate(preset.label || preset.id) }}
|
|
67
70
|
</option>
|
|
68
71
|
</select>
|
|
72
|
+
<!-- Board look, `content.iconPlacement: "end"`: the icon sits in a
|
|
73
|
+
32px circle at the right of the label row, tinted by the
|
|
74
|
+
tile's state (DqDashboard). -->
|
|
75
|
+
<span
|
|
76
|
+
v-if="hasEndBadge"
|
|
77
|
+
class="cn-kpi-card__badge cn-stat-widget__badge-icon"
|
|
78
|
+
:class="'cn-kpi-card__badge--' + stateTone"
|
|
79
|
+
data-testid="cn-stat-widget-icon-badge"
|
|
80
|
+
aria-hidden="true">
|
|
81
|
+
<CnWidgetIcon :name="resolvedIcon" :size="18" />
|
|
82
|
+
</span>
|
|
69
83
|
</div>
|
|
70
84
|
|
|
71
85
|
<div class="cn-kpi-card__value-row cn-stat-widget__value-row">
|
|
@@ -1330,9 +1344,45 @@ export default {
|
|
|
1330
1344
|
if (layout === 'vertical' || layout === 'stacked') {
|
|
1331
1345
|
return layout
|
|
1332
1346
|
}
|
|
1347
|
+
// Board look: every KPI tile on the screens is stacked, so a tile
|
|
1348
|
+
// that names no layout takes it. An explicit `horizontal` stays.
|
|
1349
|
+
if (this.isBoardLook && layout === undefined) {
|
|
1350
|
+
return 'stacked'
|
|
1351
|
+
}
|
|
1333
1352
|
return 'horizontal'
|
|
1334
1353
|
},
|
|
1335
1354
|
|
|
1355
|
+
/**
|
|
1356
|
+
* Whether the board look draws the icon in a tinted circle at the
|
|
1357
|
+
* right of the label row (`content.iconPlacement: "end"`), as the
|
|
1358
|
+
* DqDashboard tiles. Off without the board look and without an icon.
|
|
1359
|
+
*
|
|
1360
|
+
* @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
|
|
1361
|
+
* @return {boolean}
|
|
1362
|
+
*/
|
|
1363
|
+
hasEndBadge() {
|
|
1364
|
+
return this.isBoardLook
|
|
1365
|
+
&& (this.content || {}).iconPlacement === 'end'
|
|
1366
|
+
&& Boolean(this.resolvedIcon)
|
|
1367
|
+
},
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* The tile's state as a tone name for the end badge: the variant the
|
|
1371
|
+
* value colour comes from (`success`, `warning`, `error`), else
|
|
1372
|
+
* `primary`. A custom `valueColor` has no tone and reads as primary.
|
|
1373
|
+
*
|
|
1374
|
+
* @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
|
|
1375
|
+
* @return {'primary'|'success'|'warning'|'error'}
|
|
1376
|
+
*/
|
|
1377
|
+
stateTone() {
|
|
1378
|
+
const color = this.variantColor
|
|
1379
|
+
if (!color) {
|
|
1380
|
+
return 'primary'
|
|
1381
|
+
}
|
|
1382
|
+
const tone = ['success', 'warning', 'error'].find((name) => VARIANT_COLORS[name] === color)
|
|
1383
|
+
return tone || 'primary'
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1336
1386
|
/**
|
|
1337
1387
|
* Whether the tile takes the stacked board look (`content.layout:
|
|
1338
1388
|
* "stacked"`): a plain muted label, the number at 34px in the text
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
|
|
12
12
|
<script>
|
|
13
|
+
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* CnStatusBadge — Color-coded pill badge for status, priority, or category display.
|
|
15
17
|
*
|
|
@@ -39,12 +41,14 @@ export default {
|
|
|
39
41
|
},
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Color variant: 'default', 'primary', 'success', 'warning', 'error',
|
|
44
|
+
* Color variant: 'default', 'primary', 'success', 'warning', 'error',
|
|
45
|
+
* 'info', or one of the two board tones without a Nextcloud role,
|
|
46
|
+
* 'purple' (work in progress) and 'teal' (handed on).
|
|
43
47
|
*/
|
|
44
48
|
variant: {
|
|
45
49
|
type: String,
|
|
46
50
|
default: 'default',
|
|
47
|
-
validator: (v) =>
|
|
51
|
+
validator: (v) => isBadgeVariant(v),
|
|
48
52
|
},
|
|
49
53
|
|
|
50
54
|
/** Size: 'small' or 'medium' */
|
|
@@ -40,6 +40,7 @@ jest.mock('@nextcloud/router', () => ({
|
|
|
40
40
|
jest.mock('@nextcloud/auth', () => ({
|
|
41
41
|
__esModule: true,
|
|
42
42
|
getCurrentUser: () => ({ uid: 'anna' }),
|
|
43
|
+
getRequestToken: () => 'token-123',
|
|
43
44
|
}))
|
|
44
45
|
|
|
45
46
|
/** The subscription the composable made, so a test can push into it. */
|
|
@@ -246,6 +247,29 @@ describe('useObjectPresence', () => {
|
|
|
246
247
|
scope.stop()
|
|
247
248
|
})
|
|
248
249
|
|
|
250
|
+
it('🔴 a closing tab departs with a beacon that carries the request token', async () => {
|
|
251
|
+
// A beacon cannot set headers, and the departure route keeps the CSRF
|
|
252
|
+
// check, so the token has to travel in the form body. The old beacon
|
|
253
|
+
// sent no body at all and could never pass that check.
|
|
254
|
+
const sendBeacon = jest.fn(() => true)
|
|
255
|
+
const original = navigator.sendBeacon
|
|
256
|
+
Object.defineProperty(navigator, 'sendBeacon', { value: sendBeacon, configurable: true, writable: true })
|
|
257
|
+
try {
|
|
258
|
+
await withPresence(async () => {
|
|
259
|
+
await nextTick()
|
|
260
|
+
await Promise.resolve()
|
|
261
|
+
window.dispatchEvent(new Event('beforeunload'))
|
|
262
|
+
})
|
|
263
|
+
} finally {
|
|
264
|
+
Object.defineProperty(navigator, 'sendBeacon', { value: original, configurable: true, writable: true })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const call = sendBeacon.mock.calls.find(([url]) => url === '/apps/openregister/api/objects/dossiq/case/case-1/presence?_method=DELETE')
|
|
268
|
+
expect(call).toBeDefined()
|
|
269
|
+
expect(call[1]).toBeInstanceOf(FormData)
|
|
270
|
+
expect(call[1].get('requesttoken')).toBe('token-123')
|
|
271
|
+
})
|
|
272
|
+
|
|
249
273
|
it('does nothing at all when disabled', async () => {
|
|
250
274
|
const scope = effectScope()
|
|
251
275
|
scope.run(() => {
|
|
@@ -127,12 +127,22 @@ export function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {
|
|
|
127
127
|
* What the draft indicator says: "Saving" while a local write is pending,
|
|
128
128
|
* "Saved just now" / "Saved 3 minutes ago" once it landed, nothing before that.
|
|
129
129
|
*
|
|
130
|
+
* After a failed save (`options.saveFailed`) it never says Saving or Saved:
|
|
131
|
+
* next to a server error those words read as if the record had been saved.
|
|
132
|
+
* It then says "Draft kept on this device" when a local copy is stored, and
|
|
133
|
+
* nothing otherwise.
|
|
134
|
+
*
|
|
130
135
|
* @param {string} state `idle`, `saving` or `saved`.
|
|
131
136
|
* @param {number} [savedAt] When the last write landed, in epoch ms.
|
|
132
137
|
* @param {number} [now] The current epoch ms, for testing.
|
|
138
|
+
* @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.
|
|
133
139
|
* @return {string} The text, or '' when it says nothing.
|
|
140
|
+
* @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
|
|
134
141
|
*/
|
|
135
|
-
export function draftIndicatorText(state, savedAt = 0, now = Date.now()) {
|
|
142
|
+
export function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {
|
|
143
|
+
if (options && options.saveFailed === true) {
|
|
144
|
+
return state === 'saved' ? t('nextcloud-vue', 'Draft kept on this device') : ''
|
|
145
|
+
}
|
|
136
146
|
if (state === 'saving') {
|
|
137
147
|
return t('nextcloud-vue', 'Saving')
|
|
138
148
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
// Copyright (C) 2026 Conduction B.V.
|
|
3
3
|
|
|
4
|
-
import { getCurrentUser } from '@nextcloud/auth'
|
|
4
|
+
import { getCurrentUser, getRequestToken } from '@nextcloud/auth'
|
|
5
5
|
import axios from '@nextcloud/axios'
|
|
6
6
|
import { generateUrl } from '@nextcloud/router'
|
|
7
7
|
import { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'
|
|
@@ -201,7 +201,15 @@ export function useObjectPresence(register, schema, objectUuid, options = {}) {
|
|
|
201
201
|
* the browser and survives the navigation, which is the only mechanism
|
|
202
202
|
* that does.
|
|
203
203
|
*
|
|
204
|
+
* 🔴 THE TOKEN RIDES IN THE BODY. A beacon cannot set headers, so the
|
|
205
|
+
* `requesttoken` header axios adds is missing, and the departure route
|
|
206
|
+
* keeps Nextcloud's CSRF check. Nextcloud also reads the token from a
|
|
207
|
+
* POST field, so it goes in a form body. Without it the beacon answers
|
|
208
|
+
* 412; before OpenRegister registered POST on `/presence` it answered 405
|
|
209
|
+
* (seen on cloud.conduction.nl on every dossiq case page).
|
|
210
|
+
*
|
|
204
211
|
* @return {void}
|
|
212
|
+
* @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
|
|
205
213
|
*/
|
|
206
214
|
function beaconDepart() {
|
|
207
215
|
const url = presenceUrl()
|
|
@@ -209,9 +217,12 @@ export function useObjectPresence(register, schema, objectUuid, options = {}) {
|
|
|
209
217
|
return
|
|
210
218
|
}
|
|
211
219
|
try {
|
|
212
|
-
// No DELETE from a beacon
|
|
213
|
-
//
|
|
214
|
-
|
|
220
|
+
// No DELETE from a beacon, it is always a POST, so the server's
|
|
221
|
+
// POST departure route is used, marked `_method=DELETE`. Both reach
|
|
222
|
+
// one implementation.
|
|
223
|
+
const body = new FormData()
|
|
224
|
+
body.append('requesttoken', getRequestToken() || '')
|
|
225
|
+
navigator.sendBeacon?.(url.replace(/\/presence$/, '/presence?_method=DELETE'), body)
|
|
215
226
|
} catch {
|
|
216
227
|
// Best effort.
|
|
217
228
|
}
|
package/src/css/badge.css
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
@conduction/nextcloud-vue — Status Badge
|
|
3
3
|
======================================== */
|
|
4
4
|
|
|
5
|
+
/* The two board tones without a Nextcloud colour role (screens-cell-pill-parity).
|
|
6
|
+
A theme may redefine them; the six role variants below use Nextcloud's own. */
|
|
7
|
+
:root {
|
|
8
|
+
--cn-status-purple-bg: #e8e6f6;
|
|
9
|
+
--cn-status-purple-text: #4a3f8f;
|
|
10
|
+
--cn-status-teal-bg: #e3f1f3;
|
|
11
|
+
--cn-status-teal-text: #1d5e66;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
.cn-status-badge {
|
|
6
15
|
display: inline-flex;
|
|
7
16
|
align-items: center;
|
|
@@ -51,6 +60,18 @@
|
|
|
51
60
|
color: var(--color-info-text, var(--color-info, #0082c9));
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
/* Work in progress ("In behandeling"), a board tone. */
|
|
64
|
+
.cn-status-badge--purple {
|
|
65
|
+
background-color: var(--cn-status-purple-bg);
|
|
66
|
+
color: var(--cn-status-purple-text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Handed on ("Omgezet naar een zaak"), a board tone. */
|
|
70
|
+
.cn-status-badge--teal {
|
|
71
|
+
background-color: var(--cn-status-teal-bg);
|
|
72
|
+
color: var(--cn-status-teal-text);
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
/* Solid variants — solid background with white text for use on colored backgrounds */
|
|
55
76
|
.cn-status-badge--solid.cn-status-badge--default {
|
|
56
77
|
background-color: var(--color-background-dark);
|
|
@@ -81,3 +102,13 @@
|
|
|
81
102
|
background-color: var(--color-info, #0082c9);
|
|
82
103
|
color: white;
|
|
83
104
|
}
|
|
105
|
+
|
|
106
|
+
.cn-status-badge--solid.cn-status-badge--purple {
|
|
107
|
+
background-color: var(--cn-status-purple-text);
|
|
108
|
+
color: white;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.cn-status-badge--solid.cn-status-badge--teal {
|
|
112
|
+
background-color: var(--cn-status-teal-text);
|
|
113
|
+
color: white;
|
|
114
|
+
}
|
package/src/css/kpi-card.css
CHANGED
|
@@ -463,6 +463,57 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
463
463
|
gap: 6px;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
/* A linked board tile is one link: the label is not underlined link text, in
|
|
467
|
+
any layout (screens-stat-tile-parity). The NL Design theme underlines every
|
|
468
|
+
`a` with `!important`, so the opt-out needs it too. */
|
|
469
|
+
a.cn-kpi-card--board,
|
|
470
|
+
a.cn-kpi-card--board:hover,
|
|
471
|
+
a.cn-kpi-card--board:focus-visible,
|
|
472
|
+
a.cn-kpi-card--board * {
|
|
473
|
+
text-decoration: none !important;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* `content.iconPlacement: "end"` (DqDashboard): label left, the icon in a
|
|
477
|
+
32px circle right, tinted by the tile's state. */
|
|
478
|
+
.cn-kpi-card--board .cn-kpi-card__title--badge-end {
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
gap: 8px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.cn-kpi-card__badge {
|
|
484
|
+
display: inline-flex;
|
|
485
|
+
flex: 0 0 auto;
|
|
486
|
+
align-items: center;
|
|
487
|
+
justify-content: center;
|
|
488
|
+
width: 32px;
|
|
489
|
+
height: 32px;
|
|
490
|
+
margin-inline-start: auto;
|
|
491
|
+
border-radius: 50%;
|
|
492
|
+
color: var(--color-primary-element-light-text, var(--color-primary-element));
|
|
493
|
+
background: var(--color-primary-element-light, color-mix(in srgb, var(--color-primary-element) 12%, transparent));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.cn-kpi-card__badge svg {
|
|
497
|
+
width: 18px;
|
|
498
|
+
height: 18px;
|
|
499
|
+
fill: currentcolor;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.cn-kpi-card__badge--success {
|
|
503
|
+
color: var(--color-text-success, var(--color-success-text));
|
|
504
|
+
background: color-mix(in srgb, var(--color-text-success, var(--color-success-text)) 12%, var(--color-main-background));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.cn-kpi-card__badge--warning {
|
|
508
|
+
color: var(--color-element-warning, var(--color-warning-text));
|
|
509
|
+
background: color-mix(in srgb, var(--color-element-warning, var(--color-warning-text)) 12%, var(--color-main-background));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.cn-kpi-card__badge--error {
|
|
513
|
+
color: var(--color-text-error, var(--color-error-text));
|
|
514
|
+
background: color-mix(in srgb, var(--color-text-error, var(--color-error-text)) 10%, var(--color-main-background));
|
|
515
|
+
}
|
|
516
|
+
|
|
466
517
|
.cn-kpi-card__glyph {
|
|
467
518
|
display: inline-flex;
|
|
468
519
|
flex: 0 0 auto;
|
|
@@ -45,6 +45,42 @@
|
|
|
45
45
|
margin-inline-start: 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/* The header buttons keep the board's button under a theme that styles every
|
|
49
|
+
secondary or primary button with `!important` (the nldesign theme: a
|
|
50
|
+
primary border, 8px 16px padding): Download and the Actions menu are 40px,
|
|
51
|
+
a grey border on the main background (PqTickets, DqZaken), the primary
|
|
52
|
+
button is the primary fill without a border. The buildiq square is not a
|
|
53
|
+
direct child button and keeps its own rule. */
|
|
54
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--secondary,
|
|
55
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .action-item .button-vue {
|
|
56
|
+
height: var(--cn-board-control-height) !important;
|
|
57
|
+
min-height: var(--cn-board-control-height) !important;
|
|
58
|
+
padding: 0 14px !important;
|
|
59
|
+
border: 1px solid var(--color-border-dark) !important;
|
|
60
|
+
border-radius: var(--cn-board-control-radius) !important;
|
|
61
|
+
background: var(--color-main-background) !important;
|
|
62
|
+
color: var(--color-main-text) !important;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--secondary:hover,
|
|
66
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .action-item .button-vue:hover {
|
|
67
|
+
background: var(--color-background-hover) !important;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--primary {
|
|
71
|
+
height: var(--cn-board-control-height) !important;
|
|
72
|
+
min-height: var(--cn-board-control-height) !important;
|
|
73
|
+
padding: 0 16px !important;
|
|
74
|
+
border: 0 !important;
|
|
75
|
+
border-radius: var(--cn-board-control-radius) !important;
|
|
76
|
+
background: var(--color-primary-element) !important;
|
|
77
|
+
color: var(--color-primary-element-text) !important;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--primary:hover {
|
|
81
|
+
background: var(--color-primary-element-hover) !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
48
84
|
/* ----------------------------------------------------------------- toolbar */
|
|
49
85
|
|
|
50
86
|
/* No band, no padding, no radius. The DOM is already in reading order (row 1,
|
package/src/css/look-board.css
CHANGED
|
@@ -435,3 +435,11 @@
|
|
|
435
435
|
color: var(--color-main-text);
|
|
436
436
|
opacity: 1;
|
|
437
437
|
}
|
|
438
|
+
|
|
439
|
+
/* ---- Status pills (screens-cell-pill-parity) ---------------------------- */
|
|
440
|
+
/* The board draws a primary pill in the deep primary ink on the light primary
|
|
441
|
+
ground ("Verzoek", "Nieuw"); the default look keeps the brighter
|
|
442
|
+
primary-element text. The other tones already match the board. */
|
|
443
|
+
.cn-look-board .cn-status-badge--primary:not(.cn-status-badge--solid):not(.cn-look-nextcloud *) {
|
|
444
|
+
color: var(--color-primary-element-light-text);
|
|
445
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.76.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1094,7 +1094,7 @@
|
|
|
1094
1094
|
"pillColors": {
|
|
1095
1095
|
"type": "object",
|
|
1096
1096
|
"description": "Pill value to badge variant. A value without an entry takes the default variant.",
|
|
1097
|
-
"additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
|
|
1097
|
+
"additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
|
|
1098
1098
|
},
|
|
1099
1099
|
"due": { "type": "string", "description": "The date field of the footer's due text, marked by `dueRule`." },
|
|
1100
1100
|
"owner": { "type": "string", "description": "The field of the owner shown as an avatar (initials) at the footer's end." }
|
|
@@ -1143,7 +1143,7 @@
|
|
|
1143
1143
|
"colorMap": {
|
|
1144
1144
|
"type": "object",
|
|
1145
1145
|
"description": "Raw value to badge variant. Keys are matched ignoring case.",
|
|
1146
|
-
"additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
|
|
1146
|
+
"additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
|
|
1147
1147
|
},
|
|
1148
1148
|
"labels": {
|
|
1149
1149
|
"type": "object",
|
|
@@ -1152,7 +1152,7 @@
|
|
|
1152
1152
|
},
|
|
1153
1153
|
"variant": {
|
|
1154
1154
|
"type": "string",
|
|
1155
|
-
"enum": ["default", "primary", "success", "warning", "error", "info"],
|
|
1155
|
+
"enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"],
|
|
1156
1156
|
"default": "default",
|
|
1157
1157
|
"description": "Badge variant for a value `colorMap` does not name."
|
|
1158
1158
|
}
|
|
@@ -3586,7 +3586,7 @@
|
|
|
3586
3586
|
},
|
|
3587
3587
|
"columns": {
|
|
3588
3588
|
"type": "array",
|
|
3589
|
-
"description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate? }). `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
|
|
3589
|
+
"description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate?, colorMap? }). `colorMap` maps an enum column's raw values to a pill tone (default, primary, success, warning, error, info, purple, teal) and wins over the schema property's own `colorMap` / `x-color-map`. `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
|
|
3590
3590
|
"items": {
|
|
3591
3591
|
"oneOf": [
|
|
3592
3592
|
{ "type": "string" },
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* badgeVariants: the colour variants a CnStatusBadge (a status pill) may take.
|
|
6
|
+
*
|
|
7
|
+
* The first six follow the Nextcloud colour roles. `purple` and `teal` are the
|
|
8
|
+
* two tones the drawn boards use for a status that has no Nextcloud role, such
|
|
9
|
+
* as "In behandeling" (work in progress) and "Omgezet naar een zaak" (handed
|
|
10
|
+
* on). One list, so the badge, the board card pill and the manifest schema
|
|
11
|
+
* accept the same names.
|
|
12
|
+
*
|
|
13
|
+
* @module utils/badgeVariants
|
|
14
|
+
* @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-status-pill-has-eight-tones
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Every variant a status pill accepts, in the order the docs list them. */
|
|
18
|
+
export const BADGE_VARIANTS = Object.freeze(['default', 'primary', 'success', 'warning', 'error', 'info', 'purple', 'teal'])
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether a value names a badge variant.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown} variant The candidate variant.
|
|
24
|
+
* @return {boolean} True for one of `BADGE_VARIANTS`.
|
|
25
|
+
* @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-status-pill-has-eight-tones
|
|
26
|
+
*/
|
|
27
|
+
export function isBadgeVariant(variant) {
|
|
28
|
+
return typeof variant === 'string' && BADGE_VARIANTS.includes(variant)
|
|
29
|
+
}
|