@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
|
@@ -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
|
|
@@ -13,6 +30,35 @@
|
|
|
13
30
|
:description="label('failedDetail')"
|
|
14
31
|
data-testid="cn-object-parties-failed" />
|
|
15
32
|
|
|
33
|
+
<div
|
|
34
|
+
v-else-if="isPrimaryPresentation"
|
|
35
|
+
class="cn-object-parties__main-card"
|
|
36
|
+
data-testid="cn-object-parties-main-card">
|
|
37
|
+
<div
|
|
38
|
+
v-if="mainParty"
|
|
39
|
+
class="cn-object-parties__main"
|
|
40
|
+
:data-party="mainParty.partyUuid"
|
|
41
|
+
data-testid="cn-object-parties-main">
|
|
42
|
+
<span class="cn-object-parties__main-initials" aria-hidden="true" data-testid="cn-object-parties-main-initials">{{ initialsOf(nameOf(mainParty)) }}</span>
|
|
43
|
+
<span class="cn-object-parties__main-text">
|
|
44
|
+
<strong class="cn-object-parties__main-name" data-testid="cn-object-parties-main-name">{{ nameOf(mainParty) }}</strong>
|
|
45
|
+
<span v-if="mainRole" class="cn-object-parties__muted cn-object-parties__main-role" data-testid="cn-object-parties-main-role">{{ mainRole }}</span>
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
<span v-else class="cn-object-parties__muted" data-testid="cn-object-parties-main-none">{{ label('noPrimary') }}</span>
|
|
49
|
+
<a
|
|
50
|
+
v-if="mainParty && mainParty.email"
|
|
51
|
+
class="cn-object-parties__main-email"
|
|
52
|
+
:href="`mailto:${mainParty.email}`"
|
|
53
|
+
data-testid="cn-object-parties-main-email">
|
|
54
|
+
<EmailOutline :size="16" aria-hidden="true" />{{ mainParty.email }}</a>
|
|
55
|
+
<a
|
|
56
|
+
v-if="footLink"
|
|
57
|
+
class="cn-object-parties__main-link"
|
|
58
|
+
:href="footLink.href"
|
|
59
|
+
data-testid="cn-object-parties-main-link">{{ footLink.label }}</a>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
16
62
|
<NcEmptyContent
|
|
17
63
|
v-else-if="groups.length === 0"
|
|
18
64
|
:name="label('empty')"
|
|
@@ -99,29 +145,76 @@
|
|
|
99
145
|
:data-effect="indicator.effect"
|
|
100
146
|
:title="indicator.verdict"
|
|
101
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>
|
|
102
160
|
</li>
|
|
103
161
|
</ul>
|
|
104
162
|
</section>
|
|
105
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" />
|
|
106
188
|
</div>
|
|
107
189
|
</template>
|
|
108
190
|
|
|
109
191
|
<script>
|
|
110
192
|
import axios from '@nextcloud/axios'
|
|
193
|
+
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
111
194
|
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
112
195
|
import { generateUrl } from '@nextcloud/router'
|
|
113
|
-
import { NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
196
|
+
import { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
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'
|
|
114
202
|
import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
|
|
115
203
|
import innerHeading from '../../mixins/innerHeading.js'
|
|
116
204
|
import { conditionalLine } from '../../utils/conditionalLine.js'
|
|
205
|
+
import { initialsOf } from '../../utils/initials.js'
|
|
117
206
|
import {
|
|
118
207
|
documentsOfParty,
|
|
119
208
|
indicatorsOf,
|
|
120
209
|
indicatorVerdict,
|
|
121
210
|
kindCode,
|
|
211
|
+
newPartyRecord,
|
|
122
212
|
representedByMap,
|
|
213
|
+
roleLabel,
|
|
123
214
|
rolesInOrder,
|
|
215
|
+
savedUuid,
|
|
124
216
|
} from '../../utils/objectParties.js'
|
|
217
|
+
import { fetchCallerScope } from '../../utils/objectPermissions.js'
|
|
125
218
|
import { readPath } from '../../utils/readPath.js'
|
|
126
219
|
|
|
127
220
|
let uid = 0
|
|
@@ -141,6 +234,7 @@ function defaultLabels() {
|
|
|
141
234
|
primary: t('nextcloud-vue', 'Primary party'),
|
|
142
235
|
notAccepted: t('nextcloud-vue', 'Kind not accepted here'),
|
|
143
236
|
representedBy: t('nextcloud-vue', 'Represented by'),
|
|
237
|
+
noPrimary: t('nextcloud-vue', 'No main contact yet'),
|
|
144
238
|
}
|
|
145
239
|
}
|
|
146
240
|
|
|
@@ -164,6 +258,22 @@ function defaultLabels() {
|
|
|
164
258
|
* }
|
|
165
259
|
* ```
|
|
166
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
|
+
*
|
|
271
|
+
* `presentation: "primary"` draws only the primary party, as a side card
|
|
272
|
+
* does: initials, name and role label, its correspondence e-mail, and
|
|
273
|
+
* `link` (`{ label, href? | tab? }`, `{total}` = the number of parties) at
|
|
274
|
+
* the foot; `tab` opens that tab of the page through the route hash. It
|
|
275
|
+
* reads the listing only.
|
|
276
|
+
*
|
|
167
277
|
* A failed read says so and is never shown as "no parties".
|
|
168
278
|
*
|
|
169
279
|
* @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-record-shows-its-parties-by-role
|
|
@@ -171,7 +281,7 @@ function defaultLabels() {
|
|
|
171
281
|
export default {
|
|
172
282
|
name: 'CnObjectPartiesWidget',
|
|
173
283
|
|
|
174
|
-
components: { NcEmptyContent, NcLoadingIcon, NcNoteCard },
|
|
284
|
+
components: { CnConfirmDialog, CnPartyAddDialog, EmailOutline, NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard, Plus, TrashCanOutline },
|
|
175
285
|
|
|
176
286
|
mixins: [innerHeading],
|
|
177
287
|
|
|
@@ -188,7 +298,10 @@ export default {
|
|
|
188
298
|
* `kindsSource` (`{ url, enforce?, sourceText? }`), `extraLines`
|
|
189
299
|
* (`[{ endpoint, path?, valueLabels?, lines }]`, one request per
|
|
190
300
|
* party, `{party}` filled in), `correspondence` (`{ endpoint, path?,
|
|
191
|
-
* senderField?, recipientsField? }`)
|
|
301
|
+
* senderField?, recipientsField? }`), `labels`, `presentation`
|
|
302
|
+
* (`"primary"`: only the primary party) and `link` (`{ label,
|
|
303
|
+
* href? | tab? }`, the primary presentation's foot link) and
|
|
304
|
+
* `manage` (`{ add?, remove? }`, adding and removing parties).
|
|
192
305
|
*/
|
|
193
306
|
content: {
|
|
194
307
|
type: Object,
|
|
@@ -226,6 +339,10 @@ export default {
|
|
|
226
339
|
kinds: null,
|
|
227
340
|
extra: {},
|
|
228
341
|
documents: [],
|
|
342
|
+
mayUpdate: true,
|
|
343
|
+
adding: false,
|
|
344
|
+
removing: null,
|
|
345
|
+
manageError: '',
|
|
229
346
|
}
|
|
230
347
|
},
|
|
231
348
|
|
|
@@ -235,6 +352,98 @@ export default {
|
|
|
235
352
|
return rolesInOrder(this.listing, this.translatedMap(this.content?.roleLabels))
|
|
236
353
|
},
|
|
237
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
|
+
|
|
407
|
+
/** @return {boolean} Whether only the primary party is drawn. */
|
|
408
|
+
isPrimaryPresentation() {
|
|
409
|
+
return this.content?.presentation === 'primary'
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/** @return {object|null} The primary party's listing row. */
|
|
413
|
+
mainParty() {
|
|
414
|
+
if (!this.primary) {
|
|
415
|
+
return null
|
|
416
|
+
}
|
|
417
|
+
return (this.listing?.results || []).find((row) => row && row.partyUuid === this.primary) || null
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/** @return {string} The primary party's role, in the app's words. */
|
|
421
|
+
mainRole() {
|
|
422
|
+
const key = this.mainParty?.role
|
|
423
|
+
if (!key) {
|
|
424
|
+
return ''
|
|
425
|
+
}
|
|
426
|
+
const own = this.translatedMap(this.content?.roleLabels)
|
|
427
|
+
if (typeof own[key] === 'string' && own[key] !== '') {
|
|
428
|
+
return own[key]
|
|
429
|
+
}
|
|
430
|
+
return this.cnTranslate(roleLabel(key, this.listing?.roles, {}))
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
/** @return {{label: string, href: string}|null} The primary presentation's foot link; none without parties. */
|
|
434
|
+
footLink() {
|
|
435
|
+
const link = this.content?.link
|
|
436
|
+
const total = Number(this.listing?.total ?? (this.listing?.results || []).length)
|
|
437
|
+
if (!link || typeof link !== 'object' || !link.label || !(total > 0)) {
|
|
438
|
+
return null
|
|
439
|
+
}
|
|
440
|
+
const href = link.tab ? `#${encodeURIComponent(link.tab)}` : (link.href || '')
|
|
441
|
+
if (href === '') {
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
return { label: this.cnTranslate(link.label).replaceAll('{total}', String(total)), href }
|
|
445
|
+
},
|
|
446
|
+
|
|
238
447
|
/** @return {string|null} The primary party's uuid. */
|
|
239
448
|
primary() {
|
|
240
449
|
return this.listing?.primary || null
|
|
@@ -300,7 +509,22 @@ export default {
|
|
|
300
509
|
},
|
|
301
510
|
},
|
|
302
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
|
+
|
|
303
525
|
methods: {
|
|
526
|
+
t,
|
|
527
|
+
initialsOf,
|
|
304
528
|
kindCode,
|
|
305
529
|
|
|
306
530
|
/**
|
|
@@ -314,6 +538,115 @@ export default {
|
|
|
314
538
|
return own !== '' ? this.cnTranslate(own) : defaultLabels()[key]
|
|
315
539
|
},
|
|
316
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
|
+
|
|
317
650
|
/**
|
|
318
651
|
* A map of manifest words, translated.
|
|
319
652
|
*
|
|
@@ -383,6 +716,11 @@ export default {
|
|
|
383
716
|
return
|
|
384
717
|
}
|
|
385
718
|
this.listing = listing
|
|
719
|
+
await this.loadMayUpdate()
|
|
720
|
+
if (this.isPrimaryPresentation) {
|
|
721
|
+
this.loading = false
|
|
722
|
+
return
|
|
723
|
+
}
|
|
386
724
|
const uuids = [...new Set((listing.results || []).map((row) => row && row.partyUuid).filter(Boolean))]
|
|
387
725
|
const partyUrl = this.content?.partySource || '/apps/openregister/api/parties/{uuid}'
|
|
388
726
|
const records = await Promise.all(uuids.map((uuid) => this.read(this.urlOf(partyUrl, uuid))))
|
|
@@ -657,6 +995,66 @@ export default {
|
|
|
657
995
|
color: var(--color-text-maxcontrast);
|
|
658
996
|
}
|
|
659
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
|
+
|
|
1007
|
+
.cn-object-parties__main-card {
|
|
1008
|
+
display: flex;
|
|
1009
|
+
flex-direction: column;
|
|
1010
|
+
gap: calc(var(--default-grid-baseline, 4px) * 3);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.cn-object-parties__main {
|
|
1014
|
+
display: flex;
|
|
1015
|
+
align-items: center;
|
|
1016
|
+
gap: calc(var(--default-grid-baseline, 4px) * 3);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.cn-object-parties__main-initials {
|
|
1020
|
+
display: flex;
|
|
1021
|
+
flex: none;
|
|
1022
|
+
align-items: center;
|
|
1023
|
+
justify-content: center;
|
|
1024
|
+
width: 40px;
|
|
1025
|
+
height: 40px;
|
|
1026
|
+
border-radius: 50%;
|
|
1027
|
+
background-color: var(--color-background-dark);
|
|
1028
|
+
color: var(--color-main-text);
|
|
1029
|
+
font-weight: 700;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.cn-object-parties__main-text {
|
|
1033
|
+
display: flex;
|
|
1034
|
+
flex-direction: column;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.cn-object-parties__main-name {
|
|
1038
|
+
font-size: 15px;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.cn-object-parties__main-role {
|
|
1042
|
+
font-size: 13px;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.cn-object-parties__main-email {
|
|
1046
|
+
display: flex;
|
|
1047
|
+
align-items: center;
|
|
1048
|
+
gap: calc(var(--default-grid-baseline, 4px) * 2.5);
|
|
1049
|
+
color: var(--color-main-text);
|
|
1050
|
+
overflow-wrap: anywhere;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.cn-object-parties__main-link {
|
|
1054
|
+
color: var(--color-primary-element);
|
|
1055
|
+
font-weight: 600;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
660
1058
|
.cn-object-parties__accepted {
|
|
661
1059
|
display: flex;
|
|
662
1060
|
flex-wrap: wrap;
|
|
@@ -542,6 +542,7 @@ import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
|
|
|
542
542
|
import { unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
543
543
|
import { initialsOf } from '../../utils/initials.js'
|
|
544
544
|
import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
|
|
545
|
+
import { isObjectRowsSpec, objectRowsOf } from '../../utils/objectRows.js'
|
|
545
546
|
import { pageFilter, pageSort } from '../../utils/pageControls.js'
|
|
546
547
|
import { readPath } from '../../utils/readPath.js'
|
|
547
548
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveDeepTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -607,7 +608,8 @@ function unwrap(value) {
|
|
|
607
608
|
* `object-list` (`register`, `schema`, `filter` with the `@objectId` /
|
|
608
609
|
* `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
|
|
609
610
|
* `rowRoute`, `pageControls`), or `endpointSource` (an app endpoint whose
|
|
610
|
-
* payload, after `responsePath`, is the array of rows, as on the object table)
|
|
611
|
+
* payload, after `responsePath`, is the array of rows, as on the object table), or `objectRows`
|
|
612
|
+
* (`{ path, where?, valuePath? }`: rows the page record holds, such as one answer of a case); `avatar` puts a circle with the initials of a name
|
|
611
613
|
* template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
|
|
612
614
|
* or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
|
|
613
615
|
* `"Termijn {deadline|date}"`). `cards` draws a framed card per row in a
|
|
@@ -647,7 +649,8 @@ export default {
|
|
|
647
649
|
* with `overVariant: "error"`), or without `maxField` against the
|
|
648
650
|
* largest row. `endpointSource` (`{ url, method?,
|
|
649
651
|
* params?, responsePath? }`) reads the rows from an app endpoint instead
|
|
650
|
-
* of `register` / `schema`. `
|
|
652
|
+
* of `register` / `schema`. `objectRows` (`{ path, where?, valuePath? }`)
|
|
653
|
+
* lists rows the page record holds, with no request. `valueLabels` maps a field's stored values to the labels
|
|
651
654
|
* the templates show. `headerNote` and `headerLink.label` fill `{total}`
|
|
652
655
|
* and `{count:<field>=<value>}`, the loaded rows with that value.
|
|
653
656
|
* A row that is a plain string or number (an endpoint
|
|
@@ -960,6 +963,30 @@ export default {
|
|
|
960
963
|
return Boolean(source && typeof source === 'object' && source.url)
|
|
961
964
|
},
|
|
962
965
|
|
|
966
|
+
/**
|
|
967
|
+
* Whether the rows come from the page's own record (`objectRows`).
|
|
968
|
+
*
|
|
969
|
+
* @return {boolean}
|
|
970
|
+
* @spec openspec/changes/record-list-object-rows/specs/record-list/spec.md#requirement-a-record-list-can-list-rows-the-page-record-holds
|
|
971
|
+
*/
|
|
972
|
+
objectRowsActive() {
|
|
973
|
+
return isObjectRowsSpec(this.content?.objectRows)
|
|
974
|
+
},
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* The rows `objectRows` names on the page record (see utils/objectRows.js).
|
|
978
|
+
*
|
|
979
|
+
* @return {Array<object>}
|
|
980
|
+
*/
|
|
981
|
+
objectRowsList() {
|
|
982
|
+
return this.objectRowsActive ? objectRowsOf(this.objectData, this.content.objectRows) : []
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
/** @return {string} The page record's rows, to copy into the list when they change. */
|
|
986
|
+
objectRowsKey() {
|
|
987
|
+
return this.objectRowsActive ? JSON.stringify(this.objectRowsList) : ''
|
|
988
|
+
},
|
|
989
|
+
|
|
963
990
|
/** @return {string} The endpoint read's state, to copy into the list when it changes. */
|
|
964
991
|
endpointKey() {
|
|
965
992
|
return this.endpointActive ? JSON.stringify([this.epData, this.epLoading, this.epError]) : ''
|
|
@@ -1464,6 +1491,10 @@ export default {
|
|
|
1464
1491
|
this.fetchRows()
|
|
1465
1492
|
},
|
|
1466
1493
|
|
|
1494
|
+
objectRowsKey() {
|
|
1495
|
+
this.fetchRows()
|
|
1496
|
+
},
|
|
1497
|
+
|
|
1467
1498
|
linkIdsKey() {
|
|
1468
1499
|
this.loadLinkLabels()
|
|
1469
1500
|
},
|
|
@@ -2609,6 +2640,15 @@ export default {
|
|
|
2609
2640
|
this.takeEndpointRows()
|
|
2610
2641
|
return
|
|
2611
2642
|
}
|
|
2643
|
+
if (this.objectRowsActive) {
|
|
2644
|
+
this.rows = this.flattenRows(this.objectRowsList.map((row, index) => (row.id === undefined ? { id: `row-${index}`, ...row } : row)))
|
|
2645
|
+
this.total = this.rows.length
|
|
2646
|
+
this.loading = false
|
|
2647
|
+
this.error = false
|
|
2648
|
+
this.lenses = {}
|
|
2649
|
+
this.emitHeader()
|
|
2650
|
+
return
|
|
2651
|
+
}
|
|
2612
2652
|
if (!c.register || !c.schema || this.waiting) {
|
|
2613
2653
|
this.rows = []
|
|
2614
2654
|
this.total = 0
|
|
@@ -1056,7 +1056,7 @@ export default {
|
|
|
1056
1056
|
* @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-opening-a-tab-marks-its-part-read
|
|
1057
1057
|
*/
|
|
1058
1058
|
unreadLabelOf(tab, widgetId) {
|
|
1059
|
-
if (!this.readStateBadges || (tab && typeof tab === 'object' && (typeof tab.count === 'number' || tab.countField))) {
|
|
1059
|
+
if (!this.readStateBadges || (tab && typeof tab === 'object' && (typeof tab.count === 'number' || tab.countField || tab.countObjectRows))) {
|
|
1060
1060
|
return ''
|
|
1061
1061
|
}
|
|
1062
1062
|
const key = this.unreadKeyOf(tab, widgetId)
|
package/src/components/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { CnBulkJobProgress } from './CnBulkJobProgress/index.js'
|
|
|
7
7
|
export { CnDataTable } from './CnDataTable/index.js'
|
|
8
8
|
export { CnDataMatrix } from './CnDataMatrix/index.js'
|
|
9
9
|
export { CnFilterBar } from './CnFilterBar/index.js'
|
|
10
|
+
export { CnConditionListEditor } from './CnConditionListEditor/index.js'
|
|
10
11
|
export { CnCronField } from './CnCronField/index.js'
|
|
11
12
|
export { CnFileField } from './CnFileField/index.js'
|
|
12
13
|
export { CnOfflineQueue } from './CnOfflineQueue/index.js'
|
|
@@ -799,6 +799,12 @@
|
|
|
799
799
|
padding-inline: 16px;
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
+
/* The row's actions menu (a 34px toggle) adds no height to a card table's
|
|
803
|
+
row: the cell has no block padding, so the row stays at its text's 40px. */
|
|
804
|
+
.cn-look-board .cn-table-container--board:not(.cn-index-page__main--table > *) .cn-data-table .cn-table-col--actions {
|
|
805
|
+
padding-block: 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
802
808
|
.cn-look-board .cn-index-page__main--table .cn-table-container--board .cn-data-table td > .cn-cell-renderer {
|
|
803
809
|
display: block;
|
|
804
810
|
min-width: 0;
|