@conduction/nextcloud-vue 2.132.0 → 2.133.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.
Files changed (86) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +14 -0
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +5 -4
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js +9 -0
  7. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js.map +1 -0
  8. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js +237 -0
  9. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js.map +1 -0
  10. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js +103 -0
  11. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js.map +1 -0
  12. package/dist/esm/components/CnConditionListEditor/conditionOperators.js +121 -0
  13. package/dist/esm/components/CnConditionListEditor/conditionOperators.js.map +1 -0
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +12 -0
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  22. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
  23. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  25. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  27. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +59 -3
  28. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +197 -127
  30. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  32. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -2
  33. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  36. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
  37. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  39. package/dist/esm/index.js +2 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/l10n/en.json.js +17 -1
  42. package/dist/esm/l10n/en.json.js.map +1 -1
  43. package/dist/esm/l10n/nl.json.js +17 -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 +277 -166
  47. package/dist/esm/store/plugins/liveUpdates.js +13 -4
  48. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  49. package/dist/esm/store/useObjectStore.js +28 -5
  50. package/dist/esm/store/useObjectStore.js.map +1 -1
  51. package/dist/esm/utils/detailActionModel.js +6 -0
  52. package/dist/esm/utils/detailActionModel.js.map +1 -1
  53. package/dist/esm/utils/listLenses.js +20 -1
  54. package/dist/esm/utils/listLenses.js.map +1 -1
  55. package/dist/esm/utils/objectRows.js +59 -0
  56. package/dist/esm/utils/objectRows.js.map +1 -0
  57. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  58. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  59. package/dist/nextcloud-vue.cjs.js +5961 -5127
  60. package/dist/nextcloud-vue.cjs.js.map +2 -2
  61. package/dist/nextcloud-vue.css +277 -166
  62. package/l10n/en.json +17 -1
  63. package/l10n/nl.json +17 -1
  64. package/package.json +1 -1
  65. package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
  66. package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
  67. package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
  68. package/src/components/CnConditionListEditor/index.js +17 -0
  69. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  70. package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
  71. package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
  72. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
  73. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +139 -2
  74. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
  75. package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
  76. package/src/components/index.js +1 -0
  77. package/src/css/look-board-index.css +6 -0
  78. package/src/css/look-board.css +35 -7
  79. package/src/index.js +2 -0
  80. package/src/schemas/app-manifest-v2.schema.json +17 -1
  81. package/src/store/plugins/liveUpdates.js +13 -4
  82. package/src/store/useObjectStore.js +28 -5
  83. package/src/utils/detailActionModel.js +6 -0
  84. package/src/utils/listLenses.js +20 -1
  85. package/src/utils/objectRows.js +55 -0
  86. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -55,7 +55,7 @@
55
55
  v-for="(item, idx) in shownItems"
56
56
  :key="item.sinceId || item.id || idx"
57
57
  class="cn-nc-widget-widget__item"
58
- :class="itemClass"
58
+ :class="[itemClass, toneClass(item)]"
59
59
  :href="item.link || '#'"
60
60
  :title="item.title">
61
61
  <span class="cn-nc-widget-widget__item-icon-wrap">
@@ -93,6 +93,9 @@ import axios from '@nextcloud/axios'
93
93
  import { translate as t } from '@nextcloud/l10n'
94
94
  import { generateOcsUrl } from '@nextcloud/router'
95
95
 
96
+ /** The tones `content.itemTones` may give an item. */
97
+ const ITEM_TONES = ['success', 'info', 'neutral', 'warning', 'error']
98
+
96
99
  const API_ITEM_LIMIT = 7
97
100
 
98
101
  /**
@@ -137,7 +140,7 @@ export default {
137
140
 
138
141
  props: {
139
142
  /**
140
- * Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?}`.
143
+ * Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?, itemTones?}`.
141
144
  * `excludeApps` (`["systemtags"]`) and `excludeTypes` leave out the
142
145
  * items of those apps or types (an item's own `app` / `type`, else the
143
146
  * app in its link or icon URL); the list then comes from the items API.
@@ -365,18 +368,54 @@ export default {
365
368
  return false
366
369
  }
367
370
  const { apps, types } = this.exclusions
371
+ const kinds = this.kindsOf(item)
372
+ if (apps.length > 0 && kinds.apps.some((app) => apps.includes(app))) {
373
+ return true
374
+ }
375
+ return types.length > 0 && kinds.types.some((type) => types.includes(type))
376
+ },
377
+
378
+ /**
379
+ * What an item is, lower case: its own `type` / `objectType`, and its
380
+ * own `app` or the app in an `/apps/<app>/` part of its icons or link.
381
+ *
382
+ * @param {object} item A widget item.
383
+ * @return {{apps: string[], types: string[]}}
384
+ */
385
+ kindsOf(item) {
368
386
  const own = (value) => (typeof value === 'string' ? value.toLowerCase() : '')
369
- const appsOf = [own(item.app)]
370
- for (const url of [item.link, item.iconUrl, item.overlayIconUrl]) {
387
+ // The icons first: the activity app sets them from the activity's own app.
388
+ const apps = [own(item.app)]
389
+ for (const url of [item.iconUrl, item.overlayIconUrl, item.link]) {
371
390
  const match = typeof url === 'string' ? url.match(/\/apps\/([a-z0-9_-]+)\//i) : null
372
391
  if (match) {
373
- appsOf.push(match[1].toLowerCase())
392
+ apps.push(match[1].toLowerCase())
374
393
  }
375
394
  }
376
- if (apps.length > 0 && appsOf.some((app) => app !== '' && apps.includes(app))) {
377
- return true
395
+ return { apps: apps.filter((app) => app !== ''), types: [own(item.type), own(item.objectType)].filter((type) => type !== '') }
396
+ },
397
+
398
+ /**
399
+ * The item's class for its tone (`content.itemTones`), keyed by its
400
+ * type first, then its app; '' without a match.
401
+ *
402
+ * @spec openspec/changes/nc-widget-item-tones/specs/nc-widget-item-tones/spec.md#requirement-a-nextcloud-widget-item-can-take-a-tone-from-its-app-or-type
403
+ * @param {object} item A widget item.
404
+ * @return {string}
405
+ */
406
+ toneClass(item) {
407
+ const map = this.content && this.content.itemTones
408
+ if (!map || typeof map !== 'object' || !item || typeof item !== 'object') {
409
+ return ''
410
+ }
411
+ const kinds = this.kindsOf(item)
412
+ for (const key of [...kinds.types, ...kinds.apps]) {
413
+ const tone = Object.keys(map).find((name) => name.toLowerCase() === key)
414
+ if (tone && ITEM_TONES.includes(map[tone])) {
415
+ return `cn-nc-widget-widget__item--tone-${map[tone]}`
416
+ }
378
417
  }
379
- return types.length > 0 && [own(item.type), own(item.objectType)].some((type) => type !== '' && types.includes(type))
418
+ return ''
380
419
  },
381
420
 
382
421
  t,
@@ -13,6 +13,35 @@
13
13
  :description="label('failedDetail')"
14
14
  data-testid="cn-object-parties-failed" />
15
15
 
16
+ <div
17
+ v-else-if="isPrimaryPresentation"
18
+ class="cn-object-parties__main-card"
19
+ data-testid="cn-object-parties-main-card">
20
+ <div
21
+ v-if="mainParty"
22
+ class="cn-object-parties__main"
23
+ :data-party="mainParty.partyUuid"
24
+ data-testid="cn-object-parties-main">
25
+ <span class="cn-object-parties__main-initials" aria-hidden="true" data-testid="cn-object-parties-main-initials">{{ initialsOf(nameOf(mainParty)) }}</span>
26
+ <span class="cn-object-parties__main-text">
27
+ <strong class="cn-object-parties__main-name" data-testid="cn-object-parties-main-name">{{ nameOf(mainParty) }}</strong>
28
+ <span v-if="mainRole" class="cn-object-parties__muted cn-object-parties__main-role" data-testid="cn-object-parties-main-role">{{ mainRole }}</span>
29
+ </span>
30
+ </div>
31
+ <span v-else class="cn-object-parties__muted" data-testid="cn-object-parties-main-none">{{ label('noPrimary') }}</span>
32
+ <a
33
+ v-if="mainParty && mainParty.email"
34
+ class="cn-object-parties__main-email"
35
+ :href="`mailto:${mainParty.email}`"
36
+ data-testid="cn-object-parties-main-email">
37
+ <EmailOutline :size="16" aria-hidden="true" />{{ mainParty.email }}</a>
38
+ <a
39
+ v-if="footLink"
40
+ class="cn-object-parties__main-link"
41
+ :href="footLink.href"
42
+ data-testid="cn-object-parties-main-link">{{ footLink.label }}</a>
43
+ </div>
44
+
16
45
  <NcEmptyContent
17
46
  v-else-if="groups.length === 0"
18
47
  :name="label('empty')"
@@ -111,15 +140,18 @@ import axios from '@nextcloud/axios'
111
140
  import { translatePlural as n, translate as t } from '@nextcloud/l10n'
112
141
  import { generateUrl } from '@nextcloud/router'
113
142
  import { NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
143
+ import EmailOutline from 'vue-material-design-icons/EmailOutline.vue'
114
144
  import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
115
145
  import innerHeading from '../../mixins/innerHeading.js'
116
146
  import { conditionalLine } from '../../utils/conditionalLine.js'
147
+ import { initialsOf } from '../../utils/initials.js'
117
148
  import {
118
149
  documentsOfParty,
119
150
  indicatorsOf,
120
151
  indicatorVerdict,
121
152
  kindCode,
122
153
  representedByMap,
154
+ roleLabel,
123
155
  rolesInOrder,
124
156
  } from '../../utils/objectParties.js'
125
157
  import { readPath } from '../../utils/readPath.js'
@@ -141,6 +173,7 @@ function defaultLabels() {
141
173
  primary: t('nextcloud-vue', 'Primary party'),
142
174
  notAccepted: t('nextcloud-vue', 'Kind not accepted here'),
143
175
  representedBy: t('nextcloud-vue', 'Represented by'),
176
+ noPrimary: t('nextcloud-vue', 'No main contact yet'),
144
177
  }
145
178
  }
146
179
 
@@ -164,6 +197,12 @@ function defaultLabels() {
164
197
  * }
165
198
  * ```
166
199
  *
200
+ * `presentation: "primary"` draws only the primary party, as a side card
201
+ * does: initials, name and role label, its correspondence e-mail, and
202
+ * `link` (`{ label, href? | tab? }`, `{total}` = the number of parties) at
203
+ * the foot; `tab` opens that tab of the page through the route hash. It
204
+ * reads the listing only.
205
+ *
167
206
  * A failed read says so and is never shown as "no parties".
168
207
  *
169
208
  * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-record-shows-its-parties-by-role
@@ -171,7 +210,7 @@ function defaultLabels() {
171
210
  export default {
172
211
  name: 'CnObjectPartiesWidget',
173
212
 
174
- components: { NcEmptyContent, NcLoadingIcon, NcNoteCard },
213
+ components: { EmailOutline, NcEmptyContent, NcLoadingIcon, NcNoteCard },
175
214
 
176
215
  mixins: [innerHeading],
177
216
 
@@ -188,7 +227,9 @@ export default {
188
227
  * `kindsSource` (`{ url, enforce?, sourceText? }`), `extraLines`
189
228
  * (`[{ endpoint, path?, valueLabels?, lines }]`, one request per
190
229
  * party, `{party}` filled in), `correspondence` (`{ endpoint, path?,
191
- * senderField?, recipientsField? }`) and `labels`.
230
+ * senderField?, recipientsField? }`), `labels`, `presentation`
231
+ * (`"primary"`: only the primary party) and `link` (`{ label,
232
+ * href? | tab? }`, the primary presentation's foot link).
192
233
  */
193
234
  content: {
194
235
  type: Object,
@@ -235,6 +276,46 @@ export default {
235
276
  return rolesInOrder(this.listing, this.translatedMap(this.content?.roleLabels))
236
277
  },
237
278
 
279
+ /** @return {boolean} Whether only the primary party is drawn. */
280
+ isPrimaryPresentation() {
281
+ return this.content?.presentation === 'primary'
282
+ },
283
+
284
+ /** @return {object|null} The primary party's listing row. */
285
+ mainParty() {
286
+ if (!this.primary) {
287
+ return null
288
+ }
289
+ return (this.listing?.results || []).find((row) => row && row.partyUuid === this.primary) || null
290
+ },
291
+
292
+ /** @return {string} The primary party's role, in the app's words. */
293
+ mainRole() {
294
+ const key = this.mainParty?.role
295
+ if (!key) {
296
+ return ''
297
+ }
298
+ const own = this.translatedMap(this.content?.roleLabels)
299
+ if (typeof own[key] === 'string' && own[key] !== '') {
300
+ return own[key]
301
+ }
302
+ return this.cnTranslate(roleLabel(key, this.listing?.roles, {}))
303
+ },
304
+
305
+ /** @return {{label: string, href: string}|null} The primary presentation's foot link; none without parties. */
306
+ footLink() {
307
+ const link = this.content?.link
308
+ const total = Number(this.listing?.total ?? (this.listing?.results || []).length)
309
+ if (!link || typeof link !== 'object' || !link.label || !(total > 0)) {
310
+ return null
311
+ }
312
+ const href = link.tab ? `#${encodeURIComponent(link.tab)}` : (link.href || '')
313
+ if (href === '') {
314
+ return null
315
+ }
316
+ return { label: this.cnTranslate(link.label).replaceAll('{total}', String(total)), href }
317
+ },
318
+
238
319
  /** @return {string|null} The primary party's uuid. */
239
320
  primary() {
240
321
  return this.listing?.primary || null
@@ -301,6 +382,7 @@ export default {
301
382
  },
302
383
 
303
384
  methods: {
385
+ initialsOf,
304
386
  kindCode,
305
387
 
306
388
  /**
@@ -383,6 +465,10 @@ export default {
383
465
  return
384
466
  }
385
467
  this.listing = listing
468
+ if (this.isPrimaryPresentation) {
469
+ this.loading = false
470
+ return
471
+ }
386
472
  const uuids = [...new Set((listing.results || []).map((row) => row && row.partyUuid).filter(Boolean))]
387
473
  const partyUrl = this.content?.partySource || '/apps/openregister/api/parties/{uuid}'
388
474
  const records = await Promise.all(uuids.map((uuid) => this.read(this.urlOf(partyUrl, uuid))))
@@ -657,6 +743,57 @@ export default {
657
743
  color: var(--color-text-maxcontrast);
658
744
  }
659
745
 
746
+ .cn-object-parties__main-card {
747
+ display: flex;
748
+ flex-direction: column;
749
+ gap: calc(var(--default-grid-baseline, 4px) * 3);
750
+ }
751
+
752
+ .cn-object-parties__main {
753
+ display: flex;
754
+ align-items: center;
755
+ gap: calc(var(--default-grid-baseline, 4px) * 3);
756
+ }
757
+
758
+ .cn-object-parties__main-initials {
759
+ display: flex;
760
+ flex: none;
761
+ align-items: center;
762
+ justify-content: center;
763
+ width: 40px;
764
+ height: 40px;
765
+ border-radius: 50%;
766
+ background-color: var(--color-background-dark);
767
+ color: var(--color-main-text);
768
+ font-weight: 700;
769
+ }
770
+
771
+ .cn-object-parties__main-text {
772
+ display: flex;
773
+ flex-direction: column;
774
+ }
775
+
776
+ .cn-object-parties__main-name {
777
+ font-size: 15px;
778
+ }
779
+
780
+ .cn-object-parties__main-role {
781
+ font-size: 13px;
782
+ }
783
+
784
+ .cn-object-parties__main-email {
785
+ display: flex;
786
+ align-items: center;
787
+ gap: calc(var(--default-grid-baseline, 4px) * 2.5);
788
+ color: var(--color-main-text);
789
+ overflow-wrap: anywhere;
790
+ }
791
+
792
+ .cn-object-parties__main-link {
793
+ color: var(--color-primary-element);
794
+ font-weight: 600;
795
+ }
796
+
660
797
  .cn-object-parties__accepted {
661
798
  display: flex;
662
799
  flex-wrap: wrap;
@@ -542,6 +542,7 @@ import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
542
542
  import { unwrapAppConfig } from '../../utils/formatMetric.js'
543
543
  import { initialsOf } from '../../utils/initials.js'
544
544
  import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
545
+ import { isObjectRowsSpec, objectRowsOf } from '../../utils/objectRows.js'
545
546
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
546
547
  import { readPath } from '../../utils/readPath.js'
547
548
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveDeepTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
@@ -607,7 +608,8 @@ function unwrap(value) {
607
608
  * `object-list` (`register`, `schema`, `filter` with the `@objectId` /
608
609
  * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
609
610
  * `rowRoute`, `pageControls`), or `endpointSource` (an app endpoint whose
610
- * payload, after `responsePath`, is the array of rows, as on the object table); `avatar` puts a circle with the initials of a name
611
+ * payload, after `responsePath`, is the array of rows, as on the object table), or `objectRows`
612
+ * (`{ path, where?, valuePath? }`: rows the page record holds, such as one answer of a case); `avatar` puts a circle with the initials of a name
611
613
  * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
612
614
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
613
615
  * `"Termijn {deadline|date}"`). `cards` draws a framed card per row in a
@@ -647,7 +649,8 @@ export default {
647
649
  * with `overVariant: "error"`), or without `maxField` against the
648
650
  * largest row. `endpointSource` (`{ url, method?,
649
651
  * params?, responsePath? }`) reads the rows from an app endpoint instead
650
- * of `register` / `schema`. `valueLabels` maps a field's stored values to the labels
652
+ * of `register` / `schema`. `objectRows` (`{ path, where?, valuePath? }`)
653
+ * lists rows the page record holds, with no request. `valueLabels` maps a field's stored values to the labels
651
654
  * the templates show. `headerNote` and `headerLink.label` fill `{total}`
652
655
  * and `{count:<field>=<value>}`, the loaded rows with that value.
653
656
  * A row that is a plain string or number (an endpoint
@@ -960,6 +963,30 @@ export default {
960
963
  return Boolean(source && typeof source === 'object' && source.url)
961
964
  },
962
965
 
966
+ /**
967
+ * Whether the rows come from the page's own record (`objectRows`).
968
+ *
969
+ * @return {boolean}
970
+ * @spec openspec/changes/record-list-object-rows/specs/record-list/spec.md#requirement-a-record-list-can-list-rows-the-page-record-holds
971
+ */
972
+ objectRowsActive() {
973
+ return isObjectRowsSpec(this.content?.objectRows)
974
+ },
975
+
976
+ /**
977
+ * The rows `objectRows` names on the page record (see utils/objectRows.js).
978
+ *
979
+ * @return {Array<object>}
980
+ */
981
+ objectRowsList() {
982
+ return this.objectRowsActive ? objectRowsOf(this.objectData, this.content.objectRows) : []
983
+ },
984
+
985
+ /** @return {string} The page record's rows, to copy into the list when they change. */
986
+ objectRowsKey() {
987
+ return this.objectRowsActive ? JSON.stringify(this.objectRowsList) : ''
988
+ },
989
+
963
990
  /** @return {string} The endpoint read's state, to copy into the list when it changes. */
964
991
  endpointKey() {
965
992
  return this.endpointActive ? JSON.stringify([this.epData, this.epLoading, this.epError]) : ''
@@ -1464,6 +1491,10 @@ export default {
1464
1491
  this.fetchRows()
1465
1492
  },
1466
1493
 
1494
+ objectRowsKey() {
1495
+ this.fetchRows()
1496
+ },
1497
+
1467
1498
  linkIdsKey() {
1468
1499
  this.loadLinkLabels()
1469
1500
  },
@@ -2609,6 +2640,15 @@ export default {
2609
2640
  this.takeEndpointRows()
2610
2641
  return
2611
2642
  }
2643
+ if (this.objectRowsActive) {
2644
+ this.rows = this.flattenRows(this.objectRowsList.map((row, index) => (row.id === undefined ? { id: `row-${index}`, ...row } : row)))
2645
+ this.total = this.rows.length
2646
+ this.loading = false
2647
+ this.error = false
2648
+ this.lenses = {}
2649
+ this.emitHeader()
2650
+ return
2651
+ }
2612
2652
  if (!c.register || !c.schema || this.waiting) {
2613
2653
  this.rows = []
2614
2654
  this.total = 0
@@ -1056,7 +1056,7 @@ export default {
1056
1056
  * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-opening-a-tab-marks-its-part-read
1057
1057
  */
1058
1058
  unreadLabelOf(tab, widgetId) {
1059
- if (!this.readStateBadges || (tab && typeof tab === 'object' && (typeof tab.count === 'number' || tab.countField))) {
1059
+ if (!this.readStateBadges || (tab && typeof tab === 'object' && (typeof tab.count === 'number' || tab.countField || tab.countObjectRows))) {
1060
1060
  return ''
1061
1061
  }
1062
1062
  const key = this.unreadKeyOf(tab, widgetId)
@@ -7,6 +7,7 @@ export { CnBulkJobProgress } from './CnBulkJobProgress/index.js'
7
7
  export { CnDataTable } from './CnDataTable/index.js'
8
8
  export { CnDataMatrix } from './CnDataMatrix/index.js'
9
9
  export { CnFilterBar } from './CnFilterBar/index.js'
10
+ export { CnConditionListEditor } from './CnConditionListEditor/index.js'
10
11
  export { CnCronField } from './CnCronField/index.js'
11
12
  export { CnFileField } from './CnFileField/index.js'
12
13
  export { CnOfflineQueue } from './CnOfflineQueue/index.js'
@@ -799,6 +799,12 @@
799
799
  padding-inline: 16px;
800
800
  }
801
801
 
802
+ /* The row's actions menu (a 34px toggle) adds no height to a card table's
803
+ row: the cell has no block padding, so the row stays at its text's 40px. */
804
+ .cn-look-board .cn-table-container--board:not(.cn-index-page__main--table > *) .cn-data-table .cn-table-col--actions {
805
+ padding-block: 0;
806
+ }
807
+
802
808
  .cn-look-board .cn-index-page__main--table .cn-table-container--board .cn-data-table td > .cn-cell-renderer {
803
809
  display: block;
804
810
  min-width: 0;
@@ -35,7 +35,7 @@
35
35
  --cn-board-card-inline-padding: 22px;
36
36
  --cn-board-card-padding: 20px var(--cn-board-card-inline-padding);
37
37
  /* The side inset of a dashboard card: its title and its rows. */
38
- --cn-board-dashboard-card-inset: 24px;
38
+ --cn-board-dashboard-card-inset: 22px;
39
39
  /* Hairline inside a card, and the soft secondary text: the boards' #3d4047,
40
40
  between the main text and the muted text, so it follows a theme's pair. */
41
41
  --cn-board-hairline: var(--color-border);
@@ -622,6 +622,15 @@
622
622
  --button-size: 34px;
623
623
  }
624
624
 
625
+ /* A detail page marked `cardButtons: "regular"` draws them at the 40px
626
+ control height: the case page's tab cards (DqZaakContact "Live gesprek",
627
+ DqZaakHistorie, DqZaakBetrokkenen), where setting and case-type pages keep 34.
628
+ @spec openspec/changes/detail-card-buttons-regular/specs/detail-card-buttons/spec.md#requirement-a-detail-page-can-draw-its-card-header-buttons-at-the-control-height */
629
+ .cn-look-board.cn-detail-page--card-buttons-regular .cn-widget-wrapper__actions .button-vue:not(.action-item__menutoggle):has(> .button-vue__wrapper > .button-vue__text:not(:empty)):where(:not(.cn-look-nextcloud *)),
630
+ .cn-look-board .cn-detail-page--card-buttons-regular .cn-widget-wrapper__actions .button-vue:not(.action-item__menutoggle):has(> .button-vue__wrapper > .button-vue__text:not(:empty)):where(:not(.cn-look-nextcloud *)) {
631
+ --button-size: var(--cn-board-control-height);
632
+ }
633
+
625
634
  /* The same beside a CnDetailCard's title (DqTaak "Notitie toevoegen" on the
626
635
  notes card, a detail card's declared header actions).
627
636
  @spec openspec/changes/board-detail-card-header-button-height/specs/board-detail-card-header-button-height/spec.md#requirement-a-labelled-detail-card-header-button-is-40px */
@@ -2244,14 +2253,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2244
2253
 
2245
2254
  /* ---- Record list on a dashboard card (PtDashboard) ---------------------- */
2246
2255
  /* The dashboard's card body is flush, so the rows carry their own inset: the
2247
- board's card pads 24px at the sides and each row 12px (the Dq dashboards;
2256
+ board's card pads 22px at the sides and each row 12px (the Dq dashboards;
2248
2257
  the pipelinq board's 14px gives way), a soft hairline
2249
2258
  between rows, the title 15px weight 600 and the meta 13px muted. A row is a
2250
2259
  link, but only the hover says so: no underline on any text run, however the
2251
2260
  host styles its links (`!important`, because a host rule such as
2252
2261
  `#content a` outranks any class). The pill and the date sit at the right. */
2253
2262
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list {
2254
- padding: 0 24px 10px;
2263
+ padding: 0 var(--cn-board-dashboard-card-inset) 10px;
2255
2264
  }
2256
2265
 
2257
2266
  /* Inside a plain or grid sections widget the sections body already takes
@@ -2357,7 +2366,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2357
2366
  }
2358
2367
 
2359
2368
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-widget-card-grid--in-cell {
2360
- padding: 16px 24px 20px;
2369
+ padding: 16px var(--cn-board-dashboard-card-inset) 20px;
2361
2370
  }
2362
2371
 
2363
2372
  /* A record list drawn as cards (ShBank's bank accounts) keeps the rows' 24px
@@ -2366,7 +2375,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2366
2375
  draws them. A card is a link only by its hover: no underline on any text run.
2367
2376
  @spec openspec/changes/record-list-cards/specs/record-list-cards/spec.md#requirement-a-record-list-can-draw-its-rows-as-cards */
2368
2377
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list--cards {
2369
- padding: 16px 24px 20px;
2378
+ padding: 16px var(--cn-board-dashboard-card-inset) 20px;
2370
2379
  }
2371
2380
 
2372
2381
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper--borderless .cn-record-list--cards {
@@ -2380,9 +2389,9 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2380
2389
 
2381
2390
  /* ---- Saved views on a dashboard card (PqMijnWerk) ------------------------ */
2382
2391
  /* The card body is flush, so the list and its empty line carry the card's
2383
- inset themselves, the same 24px at the sides as a record list. */
2392
+ inset themselves, the same card inset at the sides as a record list. */
2384
2393
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-saved-views-widget {
2385
- padding: 4px 24px 20px;
2394
+ padding: 4px var(--cn-board-dashboard-card-inset) 20px;
2386
2395
  }
2387
2396
 
2388
2397
  /* The board draws no chevron after the pill and date. */
@@ -2529,6 +2538,25 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2529
2538
  background: var(--color-primary-element);
2530
2539
  }
2531
2540
 
2541
+ /* An item's tone (`content.itemTones`) colours its dot (DqMijnWerk: a
2542
+ reaction blue, a publication green, a handover grey).
2543
+ @spec openspec/changes/nc-widget-item-tones/specs/nc-widget-item-tones/spec.md#requirement-a-nextcloud-widget-item-can-take-a-tone-from-its-app-or-type */
2544
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-nc-widget-widget__item--tone-success .cn-nc-widget-widget__item-icon-wrap {
2545
+ background: var(--color-success);
2546
+ }
2547
+
2548
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-nc-widget-widget__item--tone-neutral .cn-nc-widget-widget__item-icon-wrap {
2549
+ background: var(--color-text-maxcontrast);
2550
+ }
2551
+
2552
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-nc-widget-widget__item--tone-warning .cn-nc-widget-widget__item-icon-wrap {
2553
+ background: var(--color-warning);
2554
+ }
2555
+
2556
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-nc-widget-widget__item--tone-error .cn-nc-widget-widget__item-icon-wrap {
2557
+ background: var(--color-error);
2558
+ }
2559
+
2532
2560
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-nc-widget-widget__item--vertical .cn-nc-widget-widget__item-icon-wrap img {
2533
2561
  display: none;
2534
2562
  }
package/src/index.js CHANGED
@@ -74,6 +74,7 @@ export {
74
74
  CnChoicePickerWidget,
75
75
  CnColorPicker,
76
76
  CnCommandPalette,
77
+ CnConditionListEditor,
77
78
  CnConfigurationCard,
78
79
  CnContactCreate,
79
80
  CnContactPicker,
@@ -599,6 +600,7 @@ export { ROADMAP_LABEL_BLOCKLIST } from './utils/roadmapLabelBlocklist.js'
599
600
  // The words a person reads for a notification rule key (never the raw key).
600
601
  export { humaniseRuleKey, notificationRuleLabel } from './utils/notificationRuleLabel.js'
601
602
  export { readTransitionInputs } from './utils/transitionInputs.js'
603
+ export { conditionSentence, formatConditionValue, operatorLabel, operatorsForType, operatorTakesValue } from './components/CnConditionListEditor/conditionOperators.js'
602
604
  // An identifier-shaped key made readable (`shortDescription` → "Short description").
603
605
  export { humaniseKey } from './utils/humaniseKey.js'
604
606
 
@@ -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.152.0",
6
+ "version": "2.153.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -3249,6 +3249,22 @@
3249
3249
  "default": false,
3250
3250
  "description": "Confirm-gating (object-op + #91 Wave 3 api-call / open-form + agent): when true, the action presents a CnConfirmDialog before running (the dispatcher / CnActionButtons runs AFTER confirmation). object-op delete ALWAYS confirms regardless of this flag."
3251
3251
  },
3252
+ "confirmTitle": {
3253
+ "type": "string",
3254
+ "description": "confirm: true: the confirm dialog's title, through the host translate. Absent: the action's label."
3255
+ },
3256
+ "confirmMessage": {
3257
+ "type": "string",
3258
+ "description": "confirm: true: the question the confirm dialog asks, through the host translate. Absent: 'Are you sure you want to continue?'."
3259
+ },
3260
+ "confirmLabel": {
3261
+ "type": "string",
3262
+ "description": "confirm: true: the confirm button's label, through the host translate, e.g. 'Record it'. Absent: 'Confirm'."
3263
+ },
3264
+ "confirmIcon": {
3265
+ "type": "string",
3266
+ "description": "confirm: true: a CnIcon name drawn on the confirm button, e.g. 'Check'. Absent: no icon."
3267
+ },
3252
3268
  "target": {
3253
3269
  "type": "string",
3254
3270
  "description": "Destination for open-modal (modal id), open-page (route name), or navigate (URL). Ignored for handler type."
@@ -401,12 +401,15 @@ export function liveUpdatesPlugin(opts = {}) {
401
401
 
402
402
  // Detached: the store outlives the component whose setup first
403
403
  // used it, and an attached observer is removed with that scope.
404
- store.$onAction(({ name, args, after }) => {
404
+ // Recorded when the fetch starts, so a hint arriving while it is in
405
+ // flight repeats it rather than the query before. An append page
406
+ // (the board's "Load more") is not a query of its own.
407
+ store.$onAction(({ name, args }) => {
405
408
  if (name === 'fetchCollection') {
406
- const [type, params = {}] = args
407
- after(() => {
409
+ const [type, params = {}, options = {}] = args
410
+ if (!(options && options.append === true)) {
408
411
  store.__lastCollectionParams.set(type, params)
409
- })
412
+ }
410
413
  }
411
414
  }, true)
412
415
 
@@ -440,6 +443,12 @@ export function liveUpdatesPlugin(opts = {}) {
440
443
  }
441
444
  const key = collectionDedupKey(type, params)
442
445
  let entry = collectionInFlight.get(key)
446
+ if (entry && !(options && options.append === true)) {
447
+ // Joining makes the shared request's query the newest again.
448
+ const currentQuery = store.__currentCollectionQuery || (store.__currentCollectionQuery = {})
449
+ currentQuery[type] = JSON.stringify(params)
450
+ store.__lastCollectionParams.set(type, params)
451
+ }
443
452
  if (!entry) {
444
453
  // The shared request reports into its own sink, copied to
445
454
  // each caller's `options.outcome` below, so a caller that