@conduction/nextcloud-vue 2.132.0 → 2.134.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 (107) 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/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  19. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +85 -10
  20. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +60 -36
  22. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue.js +1 -1
  27. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js +10 -0
  28. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  31. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
  32. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  34. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  36. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +258 -6
  37. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +283 -131
  39. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  41. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -2
  42. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
  46. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  48. package/dist/esm/dialogs/CnPartyAddDialog.vue.js +9 -0
  49. package/dist/esm/dialogs/CnPartyAddDialog.vue.js.map +1 -0
  50. package/dist/esm/dialogs/CnPartyAddDialog.vue2.js +184 -0
  51. package/dist/esm/dialogs/CnPartyAddDialog.vue2.js.map +1 -0
  52. package/dist/esm/dialogs/CnPartyAddDialog.vue3.js +168 -0
  53. package/dist/esm/dialogs/CnPartyAddDialog.vue3.js.map +1 -0
  54. package/dist/esm/index.js +2 -0
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/l10n/en.json.js +27 -1
  57. package/dist/esm/l10n/en.json.js.map +1 -1
  58. package/dist/esm/l10n/nl.json.js +27 -1
  59. package/dist/esm/l10n/nl.json.js.map +1 -1
  60. package/dist/esm/library-style.js +1 -1
  61. package/dist/esm/nextcloud-vue.css +353 -217
  62. package/dist/esm/store/plugins/liveUpdates.js +13 -4
  63. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  64. package/dist/esm/store/useObjectStore.js +28 -5
  65. package/dist/esm/store/useObjectStore.js.map +1 -1
  66. package/dist/esm/utils/detailActionModel.js +6 -0
  67. package/dist/esm/utils/detailActionModel.js.map +1 -1
  68. package/dist/esm/utils/listLenses.js +20 -1
  69. package/dist/esm/utils/listLenses.js.map +1 -1
  70. package/dist/esm/utils/objectParties.js +79 -1
  71. package/dist/esm/utils/objectParties.js.map +1 -1
  72. package/dist/esm/utils/objectRows.js +59 -0
  73. package/dist/esm/utils/objectRows.js.map +1 -0
  74. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  75. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  76. package/dist/nextcloud-vue.cjs.js +7460 -5803
  77. package/dist/nextcloud-vue.cjs.js.map +2 -2
  78. package/dist/nextcloud-vue.css +353 -217
  79. package/l10n/en.json +27 -1
  80. package/l10n/nl.json +27 -1
  81. package/package.json +1 -1
  82. package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
  83. package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
  84. package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
  85. package/src/components/CnConditionListEditor/index.js +17 -0
  86. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  87. package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
  88. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +105 -12
  89. package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
  90. package/src/components/CnIntegrationWidget/CnIntegrationWidget.vue +10 -0
  91. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
  92. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +401 -3
  93. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
  94. package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
  95. package/src/components/index.js +1 -0
  96. package/src/css/look-board-index.css +6 -0
  97. package/src/css/look-board.css +35 -7
  98. package/src/dialogs/CnPartyAddDialog.vue +309 -0
  99. package/src/index.js +2 -0
  100. package/src/schemas/app-manifest-v2.schema.json +17 -1
  101. package/src/store/plugins/liveUpdates.js +13 -4
  102. package/src/store/useObjectStore.js +28 -5
  103. package/src/utils/detailActionModel.js +6 -0
  104. package/src/utils/listLenses.js +20 -1
  105. package/src/utils/objectParties.js +78 -0
  106. package/src/utils/objectRows.js +55 -0
  107. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -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
  }
@@ -0,0 +1,309 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <CnDialog
8
+ :look="look"
9
+ :width="width"
10
+ defaultWidth="form"
11
+ :eyebrow="eyebrow"
12
+ :subtitle="subtitle"
13
+ :name="dialogTitle"
14
+ size="normal"
15
+ :noClose="loading"
16
+ @closing="$emit('close')">
17
+ <div class="cn-party-add" data-testid="cn-party-add-dialog">
18
+ <fieldset v-if="canCreate" class="cn-party-add__mode">
19
+ <legend class="cn-party-add__legend">
20
+ {{ t('nextcloud-vue', 'Who') }}
21
+ </legend>
22
+ <NcCheckboxRadioSwitch
23
+ v-model="mode"
24
+ type="radio"
25
+ :name="`${idPrefix}-mode`"
26
+ value="existing"
27
+ :disabled="loading"
28
+ data-testid="cn-party-add-mode-existing">
29
+ {{ t('nextcloud-vue', 'Someone already known') }}
30
+ </NcCheckboxRadioSwitch>
31
+ <NcCheckboxRadioSwitch
32
+ v-model="mode"
33
+ type="radio"
34
+ :name="`${idPrefix}-mode`"
35
+ value="new"
36
+ :disabled="loading"
37
+ data-testid="cn-party-add-mode-new">
38
+ {{ createLabel }}
39
+ </NcCheckboxRadioSwitch>
40
+ </fieldset>
41
+
42
+ <CnPartyPicker
43
+ v-if="mode === 'existing'"
44
+ v-model="partyUuid"
45
+ :sources="sources"
46
+ :inputLabel="t('nextcloud-vue', 'Person')"
47
+ labelOutside
48
+ :disabled="loading"
49
+ data-testid="cn-party-add-picker" />
50
+
51
+ <template v-else>
52
+ <NcTextField
53
+ v-for="field in createFields"
54
+ :key="field.key"
55
+ :modelValue="values[field.key] || ''"
56
+ :label="field.label"
57
+ :type="inputType(field)"
58
+ :required="field.required === true"
59
+ :disabled="loading"
60
+ :data-testid="`cn-party-add-field-${field.key}`"
61
+ @update:modelValue="setValue(field.key, $event)" />
62
+ </template>
63
+
64
+ <NcSelect
65
+ :modelValue="roleOption"
66
+ :options="roleOptions"
67
+ label="label"
68
+ :clearable="false"
69
+ :inputLabel="t('nextcloud-vue', 'Role')"
70
+ :disabled="loading"
71
+ data-testid="cn-party-add-role"
72
+ @update:modelValue="role = $event ? $event.key : ''" />
73
+
74
+ <NcCheckboxRadioSwitch
75
+ v-if="allowPrimary"
76
+ v-model="primary"
77
+ type="checkbox"
78
+ :disabled="loading"
79
+ data-testid="cn-party-add-primary">
80
+ {{ primaryLabel }}
81
+ </NcCheckboxRadioSwitch>
82
+
83
+ <NcNoteCard v-if="error" type="error" data-testid="cn-party-add-error">
84
+ {{ error }}
85
+ </NcNoteCard>
86
+ </div>
87
+
88
+ <template #actions>
89
+ <NcButton :disabled="loading" data-testid="cn-party-add-cancel" @click="$emit('close')">
90
+ {{ t('nextcloud-vue', 'Cancel') }}
91
+ </NcButton>
92
+ <NcButton
93
+ variant="primary"
94
+ :disabled="loading || !complete"
95
+ data-testid="cn-party-add-confirm"
96
+ @click="confirm">
97
+ <template #icon>
98
+ <NcLoadingIcon v-if="loading" :size="20" :name="t('nextcloud-vue', 'Loading …')" />
99
+ </template>
100
+ {{ confirmLabel }}
101
+ </NcButton>
102
+ </template>
103
+ </CnDialog>
104
+ </template>
105
+
106
+ <script>
107
+ import { translate as t } from '@nextcloud/l10n'
108
+ import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
109
+ import CnDialog from '../components/CnDialog/CnDialog.vue'
110
+ import CnPartyPicker from '../components/CnPartyPicker/CnPartyPicker.vue'
111
+ import { dialogBoardMixin } from '../mixins/dialogBoard.js'
112
+
113
+ let uid = 0
114
+
115
+ /**
116
+ * CnPartyAddDialog — give a party a role on a record: pick somebody already
117
+ * known (CnPartyPicker over `sources`), or describe a new person with the
118
+ * `create.fields` the host declares (no identifier needed: a name and an
119
+ * address are enough). Then a role from `roles`, and optionally "make this the
120
+ * primary party".
121
+ *
122
+ * The dialog writes nothing. `confirm` emits `{ mode, partyUuid, values, role,
123
+ * primary }`; the host creates the person when `mode` is `new`, links the
124
+ * party, then closes the dialog or calls `fail(message)` to show the refusal
125
+ * (a 403 when the reader may not update the record) and let them try again.
126
+ *
127
+ * @spec openspec/changes/object-parties-manage/specs/record-widgets/spec.md#requirement-the-parties-widget-can-add-and-remove-parties
128
+ */
129
+ export default {
130
+ name: 'CnPartyAddDialog',
131
+
132
+ components: { CnDialog, CnPartyPicker, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField },
133
+
134
+ mixins: [dialogBoardMixin],
135
+
136
+ props: {
137
+ /** The dialog's title ("Add contact person"). */
138
+ dialogTitle: {
139
+ type: String,
140
+ default: () => t('nextcloud-vue', 'Add a party'),
141
+ },
142
+
143
+ /** The roles to choose from, `[{ key, label }]`, already in the reader's words. */
144
+ roles: {
145
+ type: Array,
146
+ default: () => [],
147
+ },
148
+
149
+ /** CnPartyPicker sources for somebody already known. */
150
+ sources: {
151
+ type: Array,
152
+ default: () => [],
153
+ },
154
+
155
+ /**
156
+ * The fields of a new person, `[{ key, label, type? (text, email, integer), required? }]`,
157
+ * labels already translated. None: only existing parties can be picked.
158
+ */
159
+ createFields: {
160
+ type: Array,
161
+ default: () => [],
162
+ },
163
+
164
+ /** The choice that opens the new-person fields. */
165
+ createLabel: {
166
+ type: String,
167
+ default: () => t('nextcloud-vue', 'Someone new'),
168
+ },
169
+
170
+ /** Offer "make this the primary party". */
171
+ allowPrimary: {
172
+ type: Boolean,
173
+ default: false,
174
+ },
175
+
176
+ /** The primary choice's words ("Main contact"). */
177
+ primaryLabel: {
178
+ type: String,
179
+ default: () => t('nextcloud-vue', 'Primary party'),
180
+ },
181
+
182
+ /** The confirm button's words. */
183
+ confirmLabel: {
184
+ type: String,
185
+ default: () => t('nextcloud-vue', 'Add'),
186
+ },
187
+ },
188
+
189
+ emits: ['close', 'confirm'],
190
+
191
+ data() {
192
+ uid += 1
193
+ return {
194
+ idPrefix: `cn-party-add-${uid}`,
195
+ mode: 'existing',
196
+ partyUuid: null,
197
+ values: {},
198
+ role: this.roles.length === 1 ? this.roles[0].key : '',
199
+ primary: false,
200
+ loading: false,
201
+ error: '',
202
+ }
203
+ },
204
+
205
+ computed: {
206
+ /** @return {boolean} Whether a new person can be described here. */
207
+ canCreate() {
208
+ return this.createFields.length > 0
209
+ },
210
+
211
+ /** @return {Array<{key: string, label: string}>} The role choices. */
212
+ roleOptions() {
213
+ return this.roles.filter((role) => role && role.key).map((role) => ({ key: role.key, label: role.label || role.key }))
214
+ },
215
+
216
+ /** @return {object|null} The chosen role's option. */
217
+ roleOption() {
218
+ return this.roleOptions.find((option) => option.key === this.role) || null
219
+ },
220
+
221
+ /** @return {boolean} Whether the choice is complete enough to send. */
222
+ complete() {
223
+ if (this.role === '') {
224
+ return false
225
+ }
226
+ if (this.mode === 'existing') {
227
+ return Boolean(this.partyUuid)
228
+ }
229
+ return this.createFields.every((field) => field.required !== true || String(this.values[field.key] || '').trim() !== '')
230
+ },
231
+ },
232
+
233
+ methods: {
234
+ t,
235
+
236
+ /**
237
+ * The input type of a new-person field.
238
+ *
239
+ * @param {{type?: string}} field The field.
240
+ * @return {string} `email`, `number` or `text`.
241
+ */
242
+ inputType(field) {
243
+ if (field.type === 'email') {
244
+ return 'email'
245
+ }
246
+ return field.type === 'integer' ? 'number' : 'text'
247
+ },
248
+
249
+ /**
250
+ * Set one new-person field.
251
+ *
252
+ * @param {string} key The field key.
253
+ * @param {string} value The value.
254
+ * @return {void}
255
+ */
256
+ setValue(key, value) {
257
+ this.values = { ...this.values, [key]: value }
258
+ },
259
+
260
+ /** @return {void} Hand the choice to the host. */
261
+ confirm() {
262
+ if (!this.complete) {
263
+ return
264
+ }
265
+ this.loading = true
266
+ this.error = ''
267
+ this.$emit('confirm', {
268
+ mode: this.mode,
269
+ partyUuid: this.mode === 'existing' ? String(this.partyUuid) : null,
270
+ values: this.mode === 'new' ? { ...this.values } : {},
271
+ role: this.role,
272
+ primary: this.allowPrimary && this.primary === true,
273
+ })
274
+ },
275
+
276
+ /**
277
+ * The host could not write: show why and let the reader try again.
278
+ *
279
+ * @param {string} message The refusal.
280
+ * @return {void}
281
+ */
282
+ fail(message) {
283
+ this.loading = false
284
+ this.error = message || t('nextcloud-vue', 'The party could not be added.')
285
+ },
286
+ },
287
+ }
288
+ </script>
289
+
290
+ <style scoped>
291
+ .cn-party-add {
292
+ display: flex;
293
+ flex-direction: column;
294
+ gap: calc(var(--default-grid-baseline, 4px) * 3);
295
+ }
296
+
297
+ .cn-party-add__mode {
298
+ display: flex;
299
+ flex-direction: column;
300
+ margin: 0;
301
+ padding: 0;
302
+ border: 0;
303
+ }
304
+
305
+ .cn-party-add__legend {
306
+ margin-bottom: var(--default-grid-baseline, 4px);
307
+ font-weight: 600;
308
+ }
309
+ </style>
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
@@ -714,10 +714,20 @@ const baseActions = {
714
714
  * @param {{error?: object|null}} [options.outcome] Opt-in sink for THIS call's own result: `error` is set to the error it records on `errors[type]`, or `null` on success. Unlike `errors[type]`, which every call for the type shares, a later call cannot overwrite it.
715
715
  * @param {boolean} [options.append] Add the fetched page after the rows already stored instead of replacing them. Dropped when the stored rows are no longer the page before it, or when `options.isCurrent()` returns false (the query changed meanwhile).
716
716
  * @param {() => boolean} [options.isCurrent] With `append`: asked once the page arrives; false drops it.
717
- * @return {Promise<Array>} The fetched collection (also stored in state)
717
+ * @return {Promise<Array>} The fetched collection (also stored in state, unless a newer non-append fetch for the type started meanwhile)
718
+ * @spec openspec/changes/list-keeps-its-newest-query/specs/list-keeps-its-newest-query/spec.md#requirement-the-newest-list-query-decides-what-the-list-shows
718
719
  */
719
720
  async fetchCollection(type, params = {}, options = {}) {
720
721
  const outcome = (options && typeof options.outcome === 'object' && options.outcome) || null
722
+ const append = options?.append === true
723
+ // Responses can arrive out of order; only the newest query for the
724
+ // type may write the list. An append page belongs to the query before it.
725
+ const currentQuery = this.__currentCollectionQuery || (this.__currentCollectionQuery = {})
726
+ if (!append) {
727
+ currentQuery[type] = JSON.stringify(params)
728
+ }
729
+ const query = currentQuery[type]
730
+ const superseded = () => currentQuery[type] !== query
721
731
  this.loading = { ...this.loading, [type]: true }
722
732
  this.errors = { ...this.errors, [type]: null }
723
733
 
@@ -746,10 +756,13 @@ const baseActions = {
746
756
 
747
757
  if (!response.ok) {
748
758
  const failure = await parseResponseError(response, type)
749
- this.errors = { ...this.errors, [type]: failure }
750
759
  if (outcome) {
751
760
  outcome.error = failure
752
761
  }
762
+ if (superseded()) {
763
+ return []
764
+ }
765
+ this.errors = { ...this.errors, [type]: failure }
753
766
  // eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from
754
767
  console.error(`Error fetching ${type} collection:`, failure)
755
768
  return []
@@ -757,8 +770,13 @@ const baseActions = {
757
770
 
758
771
  const data = await response.json()
759
772
  const results = data.results || data
760
- const append = options?.append === true
761
773
 
774
+ if (superseded()) {
775
+ if (outcome) {
776
+ outcome.error = null
777
+ }
778
+ return append ? [] : results
779
+ }
762
780
  const stale = append && (
763
781
  Number(this.pagination[type]?.page || 1) !== Number(params._page || 1) - 1
764
782
  || (typeof options.isCurrent === 'function' && !options.isCurrent())
@@ -797,15 +815,20 @@ const baseActions = {
797
815
  const failure = error.name === 'TypeError'
798
816
  ? networkError(error)
799
817
  : genericError(error)
800
- this.errors = { ...this.errors, [type]: failure }
801
818
  if (outcome) {
802
819
  outcome.error = failure
803
820
  }
821
+ if (superseded()) {
822
+ return []
823
+ }
824
+ this.errors = { ...this.errors, [type]: failure }
804
825
  // eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from
805
826
  console.error(`Error fetching ${type} collection:`, error)
806
827
  return []
807
828
  } finally {
808
- this.loading = { ...this.loading, [type]: false }
829
+ if (!superseded()) {
830
+ this.loading = { ...this.loading, [type]: false }
831
+ }
809
832
  }
810
833
  },
811
834
 
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { variantForColor } from './headerFieldChips.js'
20
+ import { isObjectRowsSpec, objectRowsOf } from './objectRows.js'
20
21
  import { readPath } from './readPath.js'
21
22
  import { evaluateVisibleWhenLocal } from './visibleWhen.js'
22
23
 
@@ -335,6 +336,7 @@ export function pillLook(look) {
335
336
  * @param {object|null} object The record.
336
337
  * @return {number|null} The count, or null when the tab declares none.
337
338
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
339
+ * @spec openspec/changes/record-list-object-rows/specs/record-list/spec.md#requirement-a-tab-can-count-the-rows-its-record-holds
338
340
  */
339
341
  export function resolveTabCount(tab, object) {
340
342
  if (!tab || typeof tab !== 'object') {
@@ -343,6 +345,10 @@ export function resolveTabCount(tab, object) {
343
345
  if (typeof tab.count === 'number' && Number.isFinite(tab.count)) {
344
346
  return tab.count
345
347
  }
348
+ // `countObjectRows`: the rows the record holds where it points, as a record list's `objectRows`.
349
+ if (isObjectRowsSpec(tab.countObjectRows)) {
350
+ return objectRowsOf(object, tab.countObjectRows).length
351
+ }
346
352
  if (typeof tab.countField !== 'string' || tab.countField === '') {
347
353
  const counts = object && typeof object === 'object' && object['@self'] && object['@self'].unreadCounts
348
354
  const key = tab.unreadKey || tab.id || tab.widgetId
@@ -74,6 +74,12 @@ export function splitViewsIntoTabs(views, tabIds) {
74
74
  * One saved view as a tab for CnQuickFilterBar, so a lens renders through the
75
75
  * tab strip the page already has rather than a second one beside it.
76
76
  *
77
+ * Its filter is the view's own `filters`/`filter`, else what an OpenRegister
78
+ * view (and a default view) keeps in its query: `query.filters` with the
79
+ * locked conditions of `query.overrides.lockedFilters` under them. Reading
80
+ * only the first left a stored view with no filter, so every view in the
81
+ * side panel counted the whole list.
82
+ *
77
83
  * @param {object} view A saved view.
78
84
  * @return {{id: string, label: string, filter: object}}
79
85
  */
@@ -81,10 +87,23 @@ export function viewAsTab(view) {
81
87
  return {
82
88
  id: viewIdOf(view),
83
89
  label: (view && (view.title || view.name)) || viewIdOf(view),
84
- filter: (view && (view.filters || view.filter)) || {},
90
+ filter: (view && (view.filters || view.filter)) || queryFilterOf(view),
85
91
  }
86
92
  }
87
93
 
94
+ /**
95
+ * The conditions a stored view's query applies: its locked filters, then its filters.
96
+ *
97
+ * @param {object} view A saved view.
98
+ * @return {object}
99
+ */
100
+ function queryFilterOf(view) {
101
+ const query = view && view.query && typeof view.query === 'object' ? view.query : {}
102
+ const plain = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {})
103
+ const locked = plain(query.overrides && query.overrides.lockedFilters)
104
+ return { ...locked, ...plain(query.filters) }
105
+ }
106
+
88
107
  /**
89
108
  * The teams this person is treated as having claimed.
90
109
  *