@conduction/nextcloud-vue 2.133.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/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/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/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +202 -6
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +137 -55
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.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/l10n/en.json.js +11 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +11 -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 +95 -70
- package/dist/esm/utils/objectParties.js +79 -1
- package/dist/esm/utils/objectParties.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +5683 -4860
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +95 -70
- package/l10n/en.json +11 -1
- package/l10n/nl.json +11 -1
- package/package.json +1 -1
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +105 -12
- package/src/components/CnIntegrationWidget/CnIntegrationWidget.vue +10 -0
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +264 -3
- package/src/dialogs/CnPartyAddDialog.vue +309 -0
- package/src/utils/objectParties.js +78 -0
|
@@ -305,6 +305,15 @@ export default {
|
|
|
305
305
|
*/
|
|
306
306
|
objectLabel: { type: String, default: '' },
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Extra props for the leaf, set by the placement (a manifest widget's
|
|
310
|
+
* `props.placementProps`), for example the kinds a finance leaf shows. The
|
|
311
|
+
* record's own identity always wins over a key of the same name.
|
|
312
|
+
*
|
|
313
|
+
* @type {object}
|
|
314
|
+
*/
|
|
315
|
+
placementProps: { type: Object, default: () => ({}) },
|
|
316
|
+
|
|
308
317
|
/**
|
|
309
318
|
* The surface the leaf itself is told it renders on, when it differs
|
|
310
319
|
* from `surface` (a leaf placed in a dialog that draws its sidebar-tab
|
|
@@ -580,6 +589,7 @@ export default {
|
|
|
580
589
|
*/
|
|
581
590
|
leafProps(provider) {
|
|
582
591
|
return {
|
|
592
|
+
...this.placementProps,
|
|
583
593
|
integrationId: provider.id,
|
|
584
594
|
register: this.resolvedRegister,
|
|
585
595
|
schema: this.resolvedSchema,
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<div class="cn-object-parties" data-testid="cn-object-parties">
|
|
8
|
+
<div v-if="canAdd" class="cn-object-parties__manage">
|
|
9
|
+
<NcButton data-testid="cn-object-parties-add" @click="adding = true">
|
|
10
|
+
<template #icon>
|
|
11
|
+
<Plus :size="20" />
|
|
12
|
+
</template>
|
|
13
|
+
{{ manageWord('add', 'label', t('nextcloud-vue', 'Add a party')) }}
|
|
14
|
+
</NcButton>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<NcNoteCard
|
|
18
|
+
v-if="manageError"
|
|
19
|
+
type="error"
|
|
20
|
+
role="alert"
|
|
21
|
+
data-testid="cn-object-parties-manage-error">
|
|
22
|
+
{{ manageError }}
|
|
23
|
+
</NcNoteCard>
|
|
24
|
+
|
|
8
25
|
<NcLoadingIcon v-if="loading" :size="24" />
|
|
9
26
|
|
|
10
27
|
<NcEmptyContent
|
|
@@ -128,19 +145,60 @@
|
|
|
128
145
|
:data-effect="indicator.effect"
|
|
129
146
|
:title="indicator.verdict"
|
|
130
147
|
data-testid="cn-object-parties-indicator">{{ indicator.label }} · {{ indicator.verdict }}</span>
|
|
148
|
+
<NcButton
|
|
149
|
+
v-if="canRemove && party.partyUuid"
|
|
150
|
+
class="cn-object-parties__remove"
|
|
151
|
+
variant="tertiary"
|
|
152
|
+
:aria-label="removeLabel(party, group)"
|
|
153
|
+
:title="removeLabel(party, group)"
|
|
154
|
+
data-testid="cn-object-parties-remove"
|
|
155
|
+
@click="removing = { party, group }">
|
|
156
|
+
<template #icon>
|
|
157
|
+
<TrashCanOutline :size="20" />
|
|
158
|
+
</template>
|
|
159
|
+
</NcButton>
|
|
131
160
|
</li>
|
|
132
161
|
</ul>
|
|
133
162
|
</section>
|
|
134
163
|
</template>
|
|
164
|
+
|
|
165
|
+
<CnPartyAddDialog
|
|
166
|
+
v-if="adding"
|
|
167
|
+
ref="addDialog"
|
|
168
|
+
:dialogTitle="manageWord('add', 'dialogTitle', t('nextcloud-vue', 'Add a party'))"
|
|
169
|
+
:roles="addRoles"
|
|
170
|
+
:sources="addSources"
|
|
171
|
+
:createFields="createFields"
|
|
172
|
+
:createLabel="manageWord('add', 'createLabel', t('nextcloud-vue', 'Someone new'))"
|
|
173
|
+
:allowPrimary="content.manage.add.primary === true"
|
|
174
|
+
:primaryLabel="label('primary')"
|
|
175
|
+
:confirmLabel="manageWord('add', 'confirmLabel', t('nextcloud-vue', 'Add'))"
|
|
176
|
+
@close="adding = false"
|
|
177
|
+
@confirm="addParty" />
|
|
178
|
+
|
|
179
|
+
<CnConfirmDialog
|
|
180
|
+
v-if="removing"
|
|
181
|
+
ref="removeDialog"
|
|
182
|
+
:dialogTitle="manageWord('remove', 'title', t('nextcloud-vue', 'Remove party'))"
|
|
183
|
+
:message="removeMessage"
|
|
184
|
+
:confirmLabel="manageWord('remove', 'confirmLabel', t('nextcloud-vue', 'Remove'))"
|
|
185
|
+
variant="error"
|
|
186
|
+
@close="removing = null"
|
|
187
|
+
@confirm="removeParty" />
|
|
135
188
|
</div>
|
|
136
189
|
</template>
|
|
137
190
|
|
|
138
191
|
<script>
|
|
139
192
|
import axios from '@nextcloud/axios'
|
|
193
|
+
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
140
194
|
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
141
195
|
import { generateUrl } from '@nextcloud/router'
|
|
142
|
-
import { NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
196
|
+
import { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
143
197
|
import EmailOutline from 'vue-material-design-icons/EmailOutline.vue'
|
|
198
|
+
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
199
|
+
import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
|
|
200
|
+
import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
201
|
+
import CnPartyAddDialog from '../../dialogs/CnPartyAddDialog.vue'
|
|
144
202
|
import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
|
|
145
203
|
import innerHeading from '../../mixins/innerHeading.js'
|
|
146
204
|
import { conditionalLine } from '../../utils/conditionalLine.js'
|
|
@@ -150,10 +208,13 @@ import {
|
|
|
150
208
|
indicatorsOf,
|
|
151
209
|
indicatorVerdict,
|
|
152
210
|
kindCode,
|
|
211
|
+
newPartyRecord,
|
|
153
212
|
representedByMap,
|
|
154
213
|
roleLabel,
|
|
155
214
|
rolesInOrder,
|
|
215
|
+
savedUuid,
|
|
156
216
|
} from '../../utils/objectParties.js'
|
|
217
|
+
import { fetchCallerScope } from '../../utils/objectPermissions.js'
|
|
157
218
|
import { readPath } from '../../utils/readPath.js'
|
|
158
219
|
|
|
159
220
|
let uid = 0
|
|
@@ -197,6 +258,16 @@ function defaultLabels() {
|
|
|
197
258
|
* }
|
|
198
259
|
* ```
|
|
199
260
|
*
|
|
261
|
+
* `manage` lets a reader change the parties through OpenRegister's party
|
|
262
|
+
* API, which asks the record's update right on every write: `add` (`{ label?,
|
|
263
|
+
* dialogTitle?, kind?, sources, create?, primary?, confirmLabel? }`) opens a
|
|
264
|
+
* dialog to pick somebody known or describe somebody new (`create: {
|
|
265
|
+
* register, schema, label?, fields, compose? }`, see `newPartyRecord`), choose
|
|
266
|
+
* a role and optionally make them the primary party; `remove` (`{ title?,
|
|
267
|
+
* message?, confirmLabel? }`, `{name}` and `{role}` in the message) takes a
|
|
268
|
+
* party off in one role. Both are hidden when OpenRegister says the reader
|
|
269
|
+
* may not update the schema; a refusal is shown, never swallowed.
|
|
270
|
+
*
|
|
200
271
|
* `presentation: "primary"` draws only the primary party, as a side card
|
|
201
272
|
* does: initials, name and role label, its correspondence e-mail, and
|
|
202
273
|
* `link` (`{ label, href? | tab? }`, `{total}` = the number of parties) at
|
|
@@ -210,7 +281,7 @@ function defaultLabels() {
|
|
|
210
281
|
export default {
|
|
211
282
|
name: 'CnObjectPartiesWidget',
|
|
212
283
|
|
|
213
|
-
components: { EmailOutline, NcEmptyContent, NcLoadingIcon, NcNoteCard },
|
|
284
|
+
components: { CnConfirmDialog, CnPartyAddDialog, EmailOutline, NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard, Plus, TrashCanOutline },
|
|
214
285
|
|
|
215
286
|
mixins: [innerHeading],
|
|
216
287
|
|
|
@@ -229,7 +300,8 @@ export default {
|
|
|
229
300
|
* party, `{party}` filled in), `correspondence` (`{ endpoint, path?,
|
|
230
301
|
* senderField?, recipientsField? }`), `labels`, `presentation`
|
|
231
302
|
* (`"primary"`: only the primary party) and `link` (`{ label,
|
|
232
|
-
* href? | tab? }`, the primary presentation's foot link)
|
|
303
|
+
* href? | tab? }`, the primary presentation's foot link) and
|
|
304
|
+
* `manage` (`{ add?, remove? }`, adding and removing parties).
|
|
233
305
|
*/
|
|
234
306
|
content: {
|
|
235
307
|
type: Object,
|
|
@@ -267,6 +339,10 @@ export default {
|
|
|
267
339
|
kinds: null,
|
|
268
340
|
extra: {},
|
|
269
341
|
documents: [],
|
|
342
|
+
mayUpdate: true,
|
|
343
|
+
adding: false,
|
|
344
|
+
removing: null,
|
|
345
|
+
manageError: '',
|
|
270
346
|
}
|
|
271
347
|
},
|
|
272
348
|
|
|
@@ -276,6 +352,58 @@ export default {
|
|
|
276
352
|
return rolesInOrder(this.listing, this.translatedMap(this.content?.roleLabels))
|
|
277
353
|
},
|
|
278
354
|
|
|
355
|
+
/** @return {object} The `manage` declaration, or an empty one. */
|
|
356
|
+
manage() {
|
|
357
|
+
const manage = this.content?.manage
|
|
358
|
+
return manage && typeof manage === 'object' ? manage : {}
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
/** @return {boolean} Whether the add button shows. */
|
|
362
|
+
canAdd() {
|
|
363
|
+
return Boolean(this.manage.add) && this.mayUpdate && !this.isPrimaryPresentation && !this.loading && !this.failed
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/** @return {boolean} Whether each party offers its remove button. */
|
|
367
|
+
canRemove() {
|
|
368
|
+
return Boolean(this.manage.remove) && this.mayUpdate && !this.isPrimaryPresentation
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/** @return {Array<{key: string, label: string}>} The roles a new party may hold, in the reader's words. */
|
|
372
|
+
addRoles() {
|
|
373
|
+
const declared = Array.isArray(this.listing?.roles) ? this.listing.roles : []
|
|
374
|
+
const kind = this.manage.add?.kind
|
|
375
|
+
const accepted = kind ? (this.listing?.kinds || []).find((entry) => kindCode(entry) === kind)?.roles : null
|
|
376
|
+
const keys = Array.isArray(accepted) && accepted.length > 0 ? accepted : declared.map((role) => role?.key).filter(Boolean)
|
|
377
|
+
const own = this.translatedMap(this.content?.roleLabels)
|
|
378
|
+
return keys.map((key) => ({ key, label: own[key] || this.cnTranslate(roleLabel(key, declared, {})) }))
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
/** @return {Array<object>} The picker sources, their words translated by the picker. */
|
|
382
|
+
addSources() {
|
|
383
|
+
return Array.isArray(this.manage.add?.sources) ? this.manage.add.sources : []
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/** @return {Array<object>} The new-person fields, labels translated. */
|
|
387
|
+
createFields() {
|
|
388
|
+
const fields = this.manage.add?.create?.fields
|
|
389
|
+
return (Array.isArray(fields) ? fields : [])
|
|
390
|
+
.filter((field) => field && field.key)
|
|
391
|
+
.map((field) => ({ ...field, label: this.cnTranslate(field.label || field.key) }))
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
/** @return {string} The question the remove dialog asks. */
|
|
395
|
+
removeMessage() {
|
|
396
|
+
if (!this.removing) {
|
|
397
|
+
return ''
|
|
398
|
+
}
|
|
399
|
+
const name = this.nameOf(this.removing.party)
|
|
400
|
+
const role = this.removing.group.label
|
|
401
|
+
const own = this.manage.remove?.message
|
|
402
|
+
return own
|
|
403
|
+
? this.cnTranslate(own).replaceAll('{name}', name).replaceAll('{role}', role)
|
|
404
|
+
: t('nextcloud-vue', 'Remove {name} as {role}?', { name, role }, undefined, { escape: false })
|
|
405
|
+
},
|
|
406
|
+
|
|
279
407
|
/** @return {boolean} Whether only the primary party is drawn. */
|
|
280
408
|
isPrimaryPresentation() {
|
|
281
409
|
return this.content?.presentation === 'primary'
|
|
@@ -381,7 +509,21 @@ export default {
|
|
|
381
509
|
},
|
|
382
510
|
},
|
|
383
511
|
|
|
512
|
+
mounted() {
|
|
513
|
+
this.onPageRefresh = (payload) => {
|
|
514
|
+
if (payload?.source !== this.idPrefix) {
|
|
515
|
+
this.load()
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
subscribe('cn:page:refresh', this.onPageRefresh)
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
beforeUnmount() {
|
|
522
|
+
unsubscribe('cn:page:refresh', this.onPageRefresh)
|
|
523
|
+
},
|
|
524
|
+
|
|
384
525
|
methods: {
|
|
526
|
+
t,
|
|
385
527
|
initialsOf,
|
|
386
528
|
kindCode,
|
|
387
529
|
|
|
@@ -396,6 +538,115 @@ export default {
|
|
|
396
538
|
return own !== '' ? this.cnTranslate(own) : defaultLabels()[key]
|
|
397
539
|
},
|
|
398
540
|
|
|
541
|
+
/**
|
|
542
|
+
* A word of the `manage` declaration, translated, else the default.
|
|
543
|
+
*
|
|
544
|
+
* @param {string} part `add` or `remove`.
|
|
545
|
+
* @param {string} key The word's key.
|
|
546
|
+
* @param {string} fallback The default.
|
|
547
|
+
* @return {string}
|
|
548
|
+
*/
|
|
549
|
+
manageWord(part, key, fallback) {
|
|
550
|
+
const own = this.manage[part]?.[key]
|
|
551
|
+
return typeof own === 'string' && own !== '' ? this.cnTranslate(own) : fallback
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The remove button's accessible name.
|
|
556
|
+
*
|
|
557
|
+
* @param {object} party The listing row.
|
|
558
|
+
* @param {object} group The role group.
|
|
559
|
+
* @return {string}
|
|
560
|
+
*/
|
|
561
|
+
removeLabel(party, group) {
|
|
562
|
+
return t('nextcloud-vue', 'Remove {name} as {role}', { name: this.nameOf(party), role: group.label }, undefined, { escape: false })
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Why a write failed, in the reader's words.
|
|
567
|
+
*
|
|
568
|
+
* @param {unknown} error The axios error.
|
|
569
|
+
* @return {string}
|
|
570
|
+
*/
|
|
571
|
+
refusalOf(error) {
|
|
572
|
+
if (error?.response?.status === 403) {
|
|
573
|
+
return t('nextcloud-vue', 'You may not change who is on this record.')
|
|
574
|
+
}
|
|
575
|
+
const said = error?.response?.data?.error
|
|
576
|
+
return typeof said === 'string' && said !== '' ? said : t('nextcloud-vue', 'The change could not be saved. Nothing was changed.')
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
/** @return {string} The record's parties URL. */
|
|
580
|
+
partiesUrl() {
|
|
581
|
+
return this.urlOf(this.content?.rolesSource || '/apps/openregister/api/objects/@register/@schema/@objectId/parties')
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
/** @return {Promise<void>} Whether OpenRegister lets the reader update this schema; unknown counts as yes, the server decides. */
|
|
585
|
+
async loadMayUpdate() {
|
|
586
|
+
if (!this.manage.add && !this.manage.remove) {
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
const slug = (value) => (value && typeof value === 'object' ? String(value.slug || value.id || '') : String(value || ''))
|
|
590
|
+
const scope = await fetchCallerScope({ register: slug(this.register), schema: slug(this.schema) })
|
|
591
|
+
this.mayUpdate = !scope || !Array.isArray(scope.actions) || scope.actions.includes('update')
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Create the person when they are new, then give them the role; a
|
|
596
|
+
* primary party is set through the primary route, which records the
|
|
597
|
+
* replacement.
|
|
598
|
+
*
|
|
599
|
+
* @param {{mode: string, partyUuid: string|null, values: object, role: string, primary: boolean}} choice The dialog's choice.
|
|
600
|
+
* @return {Promise<void>}
|
|
601
|
+
*/
|
|
602
|
+
async addParty(choice) {
|
|
603
|
+
try {
|
|
604
|
+
let partyUuid = choice.partyUuid
|
|
605
|
+
if (choice.mode === 'new') {
|
|
606
|
+
const create = this.manage.add?.create || {}
|
|
607
|
+
const { data } = await axios.post(
|
|
608
|
+
generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(create.register || '')}/${encodeURIComponent(create.schema || '')}`),
|
|
609
|
+
newPartyRecord(create, choice.values),
|
|
610
|
+
)
|
|
611
|
+
partyUuid = savedUuid(data)
|
|
612
|
+
if (partyUuid === '') {
|
|
613
|
+
throw new Error(t('nextcloud-vue', 'The new person was not saved.'))
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (choice.primary) {
|
|
617
|
+
await axios.put(generateUrl(`${this.partiesUrl()}/primary`), { partyUuid, role: choice.role })
|
|
618
|
+
} else {
|
|
619
|
+
await axios.post(generateUrl(this.partiesUrl()), { partyUuid, role: choice.role })
|
|
620
|
+
}
|
|
621
|
+
this.adding = false
|
|
622
|
+
this.changed()
|
|
623
|
+
} catch (error) {
|
|
624
|
+
this.$refs.addDialog?.fail(error?.response ? this.refusalOf(error) : (error?.message || this.refusalOf(error)))
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
/** @return {Promise<void>} Take the chosen party off in the chosen role. */
|
|
629
|
+
async removeParty() {
|
|
630
|
+
const { party, group } = this.removing || {}
|
|
631
|
+
if (!party) {
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
await axios.delete(generateUrl(`${this.partiesUrl()}/${encodeURIComponent(party.partyUuid)}`), { params: { role: group.key } })
|
|
636
|
+
this.removing = null
|
|
637
|
+
this.changed()
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.$refs.removeDialog?.setResult({ error: this.refusalOf(error) })
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
/** @return {void} Read the parties again and tell the page (tab counts, the side card). */
|
|
644
|
+
changed() {
|
|
645
|
+
this.manageError = ''
|
|
646
|
+
this.load()
|
|
647
|
+
emit('cn:page:refresh', { source: this.idPrefix })
|
|
648
|
+
},
|
|
649
|
+
|
|
399
650
|
/**
|
|
400
651
|
* A map of manifest words, translated.
|
|
401
652
|
*
|
|
@@ -465,6 +716,7 @@ export default {
|
|
|
465
716
|
return
|
|
466
717
|
}
|
|
467
718
|
this.listing = listing
|
|
719
|
+
await this.loadMayUpdate()
|
|
468
720
|
if (this.isPrimaryPresentation) {
|
|
469
721
|
this.loading = false
|
|
470
722
|
return
|
|
@@ -743,6 +995,15 @@ export default {
|
|
|
743
995
|
color: var(--color-text-maxcontrast);
|
|
744
996
|
}
|
|
745
997
|
|
|
998
|
+
.cn-object-parties__manage {
|
|
999
|
+
display: flex;
|
|
1000
|
+
justify-content: flex-end;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.cn-object-parties__remove {
|
|
1004
|
+
margin-inline-start: auto;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
746
1007
|
.cn-object-parties__main-card {
|
|
747
1008
|
display: flex;
|
|
748
1009
|
flex-direction: column;
|
|
@@ -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>
|