@conduction/nextcloud-vue 2.56.0-beta.48 → 2.56.0-beta.49

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.
Files changed (72) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +46 -28
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
  7. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
  8. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
  11. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
  12. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
  13. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
  14. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
  15. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
  16. package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
  17. package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
  20. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
  24. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
  25. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
  26. package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
  27. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  28. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +48 -1
  29. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  33. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +230 -25
  34. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  35. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
  36. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  37. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  38. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  39. package/dist/esm/index.js +1 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/l10n/en.json.js +18 -1
  42. package/dist/esm/l10n/en.json.js.map +1 -1
  43. package/dist/esm/l10n/nl.json.js +18 -1
  44. package/dist/esm/l10n/nl.json.js.map +1 -1
  45. package/dist/esm/library-style.js +1 -1
  46. package/dist/esm/nextcloud-vue.css +356 -114
  47. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  48. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  49. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  50. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  51. package/dist/nextcloud-vue.cjs.js +6964 -5943
  52. package/dist/nextcloud-vue.cjs.js.map +2 -2
  53. package/dist/nextcloud-vue.css +356 -114
  54. package/l10n/en.json +18 -1
  55. package/l10n/nl.json +18 -1
  56. package/package.json +1 -1
  57. package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
  58. package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
  59. package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
  60. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
  61. package/src/components/CnDeviceFramesWidget/index.js +29 -0
  62. package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
  63. package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
  64. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +81 -1
  65. package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
  66. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
  67. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  68. package/src/components/index.js +1 -0
  69. package/src/index.js +1 -0
  70. package/src/schemas/app-manifest-v2.schema.json +6 -1
  71. package/src/utils/libraryWidgetKeys.js +1 -0
  72. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -18,7 +18,8 @@
18
18
  <template v-for="block in blocks" :key="block.key">
19
19
  <dl
20
20
  v-if="block.kind === 'rows'"
21
- class="cn-facts-widget__rows">
21
+ class="cn-facts-widget__rows"
22
+ :class="{ 'cn-facts-widget__rows--columns': twoColumns }">
22
23
  <div
23
24
  v-for="row in block.rows"
24
25
  :key="row.key"
@@ -268,6 +269,7 @@ export default {
268
269
  * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
269
270
  * - `link` — `{ label, route? | href? }` at the foot.
270
271
  * - `hideEmpty` — drop an entry whose value is empty (default true).
272
+ * - `columns` — `2` lays the rows out in two columns with the label above the value (board DqContact); a row with `parts` joins them like a line (an address on one row).
271
273
  * - `emptyText` — shown when no entry has a value.
272
274
  *
273
275
  * @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}
@@ -310,6 +312,16 @@ export default {
310
312
  },
311
313
 
312
314
  computed: {
315
+ /**
316
+ * Whether the rows sit in two columns, label above value.
317
+ *
318
+ * @spec openspec/changes/facts-columns/specs/facts-widget/spec.md#requirement-facts-rows-can-sit-in-two-columns
319
+ * @return {boolean}
320
+ */
321
+ twoColumns() {
322
+ return Number(this.content?.columns) === 2
323
+ },
324
+
313
325
  /**
314
326
  * The token context: the detail object (prop, else the injected context)
315
327
  * and the page workspace.
@@ -546,7 +558,7 @@ export default {
546
558
  if (kind === 'lead') {
547
559
  return this.partsValue(record, field.title)
548
560
  }
549
- if ((kind === 'line' || kind === 'text') && Array.isArray(field.parts)) {
561
+ if ((kind === 'line' || kind === 'text' || kind === 'row') && Array.isArray(field.parts)) {
550
562
  return this.lineValue(record, field)
551
563
  }
552
564
  if ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {
@@ -924,6 +936,11 @@ export default {
924
936
  if (!spec.path && typeof spec.text === 'string') {
925
937
  return this.tr(spec.text)
926
938
  }
939
+ // A group of parts with its own separator ("Parkstraat 24" inside
940
+ // "Parkstraat 24, 3421 AB Zuiddrecht").
941
+ if (!spec.path && Array.isArray(spec.parts)) {
942
+ return this.lineValue(record, spec)
943
+ }
927
944
  const value = this.format(readPath(record, spec.path), spec, record)
928
945
  if (value === '') {
929
946
  return ''
@@ -1240,4 +1257,31 @@ export default {
1240
1257
  color: var(--color-primary-element);
1241
1258
  text-decoration: underline;
1242
1259
  }
1260
+
1261
+ /* `columns: 2` (board DqContact): two columns, the label above the value, a
1262
+ hairline under each fact; one column on a phone. */
1263
+ .cn-facts-widget__rows--columns {
1264
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1265
+ gap: 14px 24px;
1266
+ font-size: 15px;
1267
+ }
1268
+
1269
+ .cn-facts-widget__rows--columns .cn-facts-widget__row {
1270
+ display: flex;
1271
+ flex-direction: column;
1272
+ gap: 3px;
1273
+ min-width: 0;
1274
+ padding-bottom: 10px;
1275
+ border-bottom: 1px solid var(--color-border);
1276
+ }
1277
+
1278
+ .cn-facts-widget__rows--columns .cn-facts-widget__label.cn-facts-widget__label {
1279
+ font-size: 13px;
1280
+ }
1281
+
1282
+ @media (max-width: 640px) {
1283
+ .cn-facts-widget__rows--columns {
1284
+ grid-template-columns: minmax(0, 1fr);
1285
+ }
1286
+ }
1243
1287
  </style>
@@ -0,0 +1,107 @@
1
+ <!--
2
+ CnHeaderSearchLabel — the app's own words on the search field in Nextcloud's
3
+ header (screens-board-top-bar-search). Renders nothing of its own.
4
+
5
+ Under the board look the screens give the top bar's search field a line that
6
+ names what the app holds ("Zoek zaken, documenten of contacten"), where
7
+ Nextcloud shows its global "Search apps, files, tags, messages …". Nextcloud
8
+ offers no way for an app to set that line, so this component replaces the
9
+ TEXT of the field's label and nothing else:
10
+ - it changes only the text of `.unified-search-input__label` (which is also
11
+ the search button's accessible name); it never adds, moves or removes a node;
12
+ - it writes the text again when Nextcloud re-renders the field, and only
13
+ when the text differs, so its own write never loops;
14
+ - on unmount it puts Nextcloud's text back;
15
+ - with no header or no such label it does nothing.
16
+
17
+ The search Nextcloud opens is unchanged: this is the resting field's line.
18
+ -->
19
+ <script>
20
+ const LABEL_SELECTOR = '.unified-search-input__label'
21
+
22
+ /**
23
+ * Put the app's own line on the search field in Nextcloud's header.
24
+ *
25
+ * @spec openspec/changes/screens-board-top-bar-search/specs/layout-components/spec.md#requirement-the-board-look-names-what-the-header-search-finds
26
+ */
27
+ export default {
28
+ name: 'CnHeaderSearchLabel',
29
+
30
+ props: {
31
+ /**
32
+ * The line to show, already translated.
33
+ *
34
+ * @type {string}
35
+ */
36
+ label: {
37
+ type: String,
38
+ required: true,
39
+ },
40
+ },
41
+
42
+ data() {
43
+ return { observer: null, original: null, labelNode: null }
44
+ },
45
+
46
+ watch: {
47
+ label() {
48
+ this.apply()
49
+ },
50
+ },
51
+
52
+ mounted() {
53
+ const header = document.getElementById('header')
54
+ if (!header) {
55
+ return
56
+ }
57
+ this.apply()
58
+ if (typeof MutationObserver === 'function') {
59
+ this.observer = new MutationObserver(() => this.apply())
60
+ this.observer.observe(header, { childList: true, subtree: true, characterData: true })
61
+ }
62
+ },
63
+
64
+ beforeUnmount() {
65
+ this.observer?.disconnect()
66
+ this.observer = null
67
+ if (this.labelNode && this.original !== null && this.labelNode.isConnected) {
68
+ this.labelNode.textContent = this.original
69
+ }
70
+ this.labelNode = null
71
+ this.original = null
72
+ },
73
+
74
+ methods: {
75
+ /**
76
+ * Write the line into the label when it shows something else.
77
+ * Remembers the first text Nextcloud drew in each label node, to put back.
78
+ *
79
+ * @return {void}
80
+ */
81
+ apply() {
82
+ const node = document.querySelector('#header ' + LABEL_SELECTOR)
83
+ if (!node || !this.label) {
84
+ return
85
+ }
86
+ if (node !== this.labelNode) {
87
+ this.labelNode = node
88
+ this.original = node.textContent
89
+ }
90
+ if (node.textContent.trim() === this.label) {
91
+ return
92
+ }
93
+ // Keep Nextcloud's own text node where there is one, so its renderer still holds the live node.
94
+ const text = node.childNodes.length === 1 && node.firstChild.nodeType === 3 ? node.firstChild : null
95
+ if (text) {
96
+ text.data = this.label
97
+ } else {
98
+ node.textContent = this.label
99
+ }
100
+ },
101
+ },
102
+
103
+ render() {
104
+ return null
105
+ },
106
+ }
107
+ </script>
@@ -169,6 +169,20 @@
169
169
  {{ item.title }}
170
170
  </component>
171
171
  </div>
172
+ <!-- `bar`: a thin track under the row, filled to the row's value
173
+ against its own maximum or against the largest row. -->
174
+ <span
175
+ v-if="presentation === 'rows' && item.bar"
176
+ class="cn-record-list__bar"
177
+ :class="{ 'cn-record-list__bar--over': item.bar.over }"
178
+ role="meter"
179
+ :aria-label="item.title || null"
180
+ :aria-valuenow="item.bar.value"
181
+ aria-valuemin="0"
182
+ :aria-valuemax="item.bar.max"
183
+ data-testid="cn-record-list-bar">
184
+ <span class="cn-record-list__bar-fill" :style="{ width: `${item.bar.share}%` }" />
185
+ </span>
172
186
  </li>
173
187
  </ul>
174
188
  </div>
@@ -254,7 +268,11 @@ export default {
254
268
  * The list config: `{ register, schema, filter?, sort?, limit?, extend?,
255
269
  * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
256
270
  * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
257
- * valueLabels?, endpointSource? }`. `endpointSource` (`{ url, method?,
271
+ * valueLabels?, endpointSource?, bar? }`. `bar` (`{ valueField, maxField?,
272
+ * overVariant? }`) draws a thin track under each row: filled to
273
+ * `valueField` against `maxField` (drawn in the error colour past it
274
+ * with `overVariant: "error"`), or without `maxField` against the
275
+ * largest row. `endpointSource` (`{ url, method?,
258
276
  * params?, responsePath? }`) reads the rows from an app endpoint instead
259
277
  * of `register` / `schema`. `valueLabels` maps a field's stored values to the labels
260
278
  * the templates show.
@@ -402,6 +420,7 @@ export default {
402
420
  /** @return {Array<object>} The rows ready to draw. */
403
421
  items() {
404
422
  const c = this.content || {}
423
+ const barScale = this.barScale
405
424
  return this.rows.slice(0, this.limit).map((row, index) => {
406
425
  const link = this.rowLink(row)
407
426
  return {
@@ -420,10 +439,26 @@ export default {
420
439
  badges: this.badgesOf(row),
421
440
  due: this.dueOf(row),
422
441
  icon: this.iconOf(row),
442
+ bar: this.barOf(row, barScale),
423
443
  }
424
444
  })
425
445
  },
426
446
 
447
+ /**
448
+ * The largest `bar.valueField` over the rows shown, the scale of a
449
+ * `bar` without `maxField`. 0 without a `bar`.
450
+ *
451
+ * @return {number} The largest value.
452
+ * @spec openspec/changes/record-list-row-bar/specs/record-list-row-bar/spec.md#requirement-a-record-list-row-can-carry-a-bar
453
+ */
454
+ barScale() {
455
+ const field = this.content?.bar?.valueField
456
+ if (!field) {
457
+ return 0
458
+ }
459
+ return Math.max(0, ...this.rows.slice(0, this.limit).map((row) => Number(readPath(row, field)) || 0))
460
+ },
461
+
427
462
  /** @return {object|null} The header link with `{total}` filled and tokens resolved. */
428
463
  headerLink() {
429
464
  const link = this.content?.headerLink
@@ -518,6 +553,32 @@ export default {
518
553
  return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
519
554
  },
520
555
 
556
+ /**
557
+ * A row's `bar`: its value, the maximum it is measured against (the
558
+ * row's `maxField`, else the largest row), the filled share and
559
+ * whether it is over that maximum. Null without `bar` or a value.
560
+ *
561
+ * @param {object} row The row.
562
+ * @param {number} scale The largest value over the rows shown.
563
+ * @return {{value: number, max: number, share: number, over: boolean}|null} The bar.
564
+ * @spec openspec/changes/record-list-row-bar/specs/record-list-row-bar/spec.md#requirement-a-record-list-row-can-carry-a-bar
565
+ */
566
+ barOf(row, scale) {
567
+ const bar = this.content?.bar
568
+ if (!bar || typeof bar !== 'object' || !bar.valueField) {
569
+ return null
570
+ }
571
+ const value = Number(readPath(row, bar.valueField))
572
+ if (!Number.isFinite(value)) {
573
+ return null
574
+ }
575
+ const ownMax = bar.maxField ? Number(readPath(row, bar.maxField)) : NaN
576
+ const max = bar.maxField ? (Number.isFinite(ownMax) && ownMax > 0 ? ownMax : 0) : scale
577
+ const share = max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0
578
+ const over = Boolean(bar.maxField) && bar.overVariant === 'error' && max > 0 && value > max
579
+ return { value, max, share, over }
580
+ },
581
+
521
582
  /**
522
583
  * The initials of the row's avatar (`avatar`: a name template such as
523
584
  * `"{client.name}"`, or `{ text }`), or '' without one.
@@ -1075,6 +1136,25 @@ export default {
1075
1136
  border-top: 1px solid var(--color-border);
1076
1137
  }
1077
1138
 
1139
+ .cn-record-list__bar {
1140
+ background: var(--color-border);
1141
+ border-radius: 3px;
1142
+ display: block;
1143
+ height: 6px;
1144
+ margin: -6px 0 12px;
1145
+ overflow: hidden;
1146
+ }
1147
+
1148
+ .cn-record-list__bar-fill {
1149
+ background: var(--color-primary-element);
1150
+ display: block;
1151
+ height: 100%;
1152
+ }
1153
+
1154
+ .cn-record-list__bar--over .cn-record-list__bar-fill {
1155
+ background: var(--color-error);
1156
+ }
1157
+
1078
1158
  .cn-record-list__row {
1079
1159
  align-items: center;
1080
1160
  color: var(--color-main-text);
@@ -115,6 +115,14 @@ export default {
115
115
 
116
116
  With more than one group each group starts closed: its heading is a button that shows the group name and how many of its types are configured ("2/8 configured"), and opens the register select and type list on a click or Enter. A page with several registers is a short card until the administrator opens the group they need.
117
117
 
118
+ ## Board look
119
+
120
+ In the board look (`look="board"`, or the `cnLook` that `CnAppRoot` or `CnAdminSettingsShell` provides) one group is open: its name, a status chip, the register select and a type/schema table with a check icon on each configured type. It lists `boardRowLimit` types and counts the rest in a line with a button that lists them all. The other groups are rows with their type count and a status chip; activating a row opens that group. The save button closes the section instead of sitting in its header.
121
+
122
+ ## Loading
123
+
124
+ The register select lists every register without its schemas. Only the registers the configuration maps have their schemas fetched (`registers/{id}/schemas`), and a register chosen in the select is fetched when chosen.
125
+
118
126
  ## Additional props
119
127
 
120
128
  | Prop | Type | Default | Description |
@@ -127,5 +135,7 @@ With more than one group each group starts closed: its heading is a button that
127
135
  | `saveButtonText` | String | `'Save configuration'` | Label for the save button |
128
136
  | `reimportButtonText` | String | `'Re-import configuration'` | Label for the reimport button |
129
137
  | `collapseGroups` | Boolean | `null` | Draw each group as a heading button that opens and closes it, with its configured count shown while closed; the groups start closed. `null` does this when there is more than one group, `true` or `false` forces it |
138
+ | `look` | String | `''` | `board` or `nextcloud`; empty follows the provided `cnLook` |
139
+ | `boardRowLimit` | Number | `4` | Board look: how many types the open group lists before the "show all" line; `0` lists every type |
130
140
  | `autoMatch` | Boolean | `true` | Automatically match schema titles to type slugs when a register is selected |
131
141
  | `labels` | Object | *(see below)* | UI label overrides for all user-visible strings (register, schema, notConfigured, noSchemas, selectRegister, selectSchema, allConfigured, partiallyConfigured) |