@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.
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +14 -0
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +5 -4
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js +9 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js +237 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js +103 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/conditionOperators.js +121 -0
- package/dist/esm/components/CnConditionListEditor/conditionOperators.js.map +1 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +12 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +85 -10
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +60 -36
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
- 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/CnIntegrationWidget/CnIntegrationWidget.vue.js +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js +10 -0
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +258 -6
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +283 -131
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnPartyAddDialog.vue.js +9 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue.js.map +1 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue2.js +184 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue2.js.map +1 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue3.js +168 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue3.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +27 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +27 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +353 -217
- package/dist/esm/store/plugins/liveUpdates.js +13 -4
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +28 -5
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +6 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/listLenses.js +20 -1
- package/dist/esm/utils/listLenses.js.map +1 -1
- package/dist/esm/utils/objectParties.js +79 -1
- package/dist/esm/utils/objectParties.js.map +1 -1
- package/dist/esm/utils/objectRows.js +59 -0
- package/dist/esm/utils/objectRows.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +7460 -5803
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +353 -217
- package/l10n/en.json +27 -1
- package/l10n/nl.json +27 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
- package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
- package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
- package/src/components/CnConditionListEditor/index.js +17 -0
- package/src/components/CnDetailPage/CnDetailPage.md +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +105 -12
- package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
- package/src/components/CnIntegrationWidget/CnIntegrationWidget.vue +10 -0
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +401 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
- package/src/components/index.js +1 -0
- package/src/css/look-board-index.css +6 -0
- package/src/css/look-board.css +35 -7
- package/src/dialogs/CnPartyAddDialog.vue +309 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +17 -1
- package/src/store/plugins/liveUpdates.js +13 -4
- package/src/store/useObjectStore.js +28 -5
- package/src/utils/detailActionModel.js +6 -0
- package/src/utils/listLenses.js +20 -1
- package/src/utils/objectParties.js +78 -0
- package/src/utils/objectRows.js +55 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/src/css/look-board.css
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/utils/listLenses.js
CHANGED
|
@@ -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
|
*
|