@conduction/nextcloud-vue 2.132.0 → 2.133.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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 +59 -3
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +197 -127
- 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/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +17 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +17 -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 +277 -166
- 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/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 +5961 -5127
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +277 -166
- package/l10n/en.json +17 -1
- package/l10n/nl.json +17 -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/CnIndexPage/CnIndexPage.vue +8 -1
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +139 -2
- 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/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/objectRows.js +55 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
v-for="(item, idx) in shownItems"
|
|
56
56
|
:key="item.sinceId || item.id || idx"
|
|
57
57
|
class="cn-nc-widget-widget__item"
|
|
58
|
-
:class="itemClass"
|
|
58
|
+
:class="[itemClass, toneClass(item)]"
|
|
59
59
|
:href="item.link || '#'"
|
|
60
60
|
:title="item.title">
|
|
61
61
|
<span class="cn-nc-widget-widget__item-icon-wrap">
|
|
@@ -93,6 +93,9 @@ import axios from '@nextcloud/axios'
|
|
|
93
93
|
import { translate as t } from '@nextcloud/l10n'
|
|
94
94
|
import { generateOcsUrl } from '@nextcloud/router'
|
|
95
95
|
|
|
96
|
+
/** The tones `content.itemTones` may give an item. */
|
|
97
|
+
const ITEM_TONES = ['success', 'info', 'neutral', 'warning', 'error']
|
|
98
|
+
|
|
96
99
|
const API_ITEM_LIMIT = 7
|
|
97
100
|
|
|
98
101
|
/**
|
|
@@ -137,7 +140,7 @@ export default {
|
|
|
137
140
|
|
|
138
141
|
props: {
|
|
139
142
|
/**
|
|
140
|
-
* Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?}`.
|
|
143
|
+
* Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?, itemTones?}`.
|
|
141
144
|
* `excludeApps` (`["systemtags"]`) and `excludeTypes` leave out the
|
|
142
145
|
* items of those apps or types (an item's own `app` / `type`, else the
|
|
143
146
|
* app in its link or icon URL); the list then comes from the items API.
|
|
@@ -365,18 +368,54 @@ export default {
|
|
|
365
368
|
return false
|
|
366
369
|
}
|
|
367
370
|
const { apps, types } = this.exclusions
|
|
371
|
+
const kinds = this.kindsOf(item)
|
|
372
|
+
if (apps.length > 0 && kinds.apps.some((app) => apps.includes(app))) {
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
return types.length > 0 && kinds.types.some((type) => types.includes(type))
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* What an item is, lower case: its own `type` / `objectType`, and its
|
|
380
|
+
* own `app` or the app in an `/apps/<app>/` part of its icons or link.
|
|
381
|
+
*
|
|
382
|
+
* @param {object} item A widget item.
|
|
383
|
+
* @return {{apps: string[], types: string[]}}
|
|
384
|
+
*/
|
|
385
|
+
kindsOf(item) {
|
|
368
386
|
const own = (value) => (typeof value === 'string' ? value.toLowerCase() : '')
|
|
369
|
-
|
|
370
|
-
|
|
387
|
+
// The icons first: the activity app sets them from the activity's own app.
|
|
388
|
+
const apps = [own(item.app)]
|
|
389
|
+
for (const url of [item.iconUrl, item.overlayIconUrl, item.link]) {
|
|
371
390
|
const match = typeof url === 'string' ? url.match(/\/apps\/([a-z0-9_-]+)\//i) : null
|
|
372
391
|
if (match) {
|
|
373
|
-
|
|
392
|
+
apps.push(match[1].toLowerCase())
|
|
374
393
|
}
|
|
375
394
|
}
|
|
376
|
-
|
|
377
|
-
|
|
395
|
+
return { apps: apps.filter((app) => app !== ''), types: [own(item.type), own(item.objectType)].filter((type) => type !== '') }
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The item's class for its tone (`content.itemTones`), keyed by its
|
|
400
|
+
* type first, then its app; '' without a match.
|
|
401
|
+
*
|
|
402
|
+
* @spec openspec/changes/nc-widget-item-tones/specs/nc-widget-item-tones/spec.md#requirement-a-nextcloud-widget-item-can-take-a-tone-from-its-app-or-type
|
|
403
|
+
* @param {object} item A widget item.
|
|
404
|
+
* @return {string}
|
|
405
|
+
*/
|
|
406
|
+
toneClass(item) {
|
|
407
|
+
const map = this.content && this.content.itemTones
|
|
408
|
+
if (!map || typeof map !== 'object' || !item || typeof item !== 'object') {
|
|
409
|
+
return ''
|
|
410
|
+
}
|
|
411
|
+
const kinds = this.kindsOf(item)
|
|
412
|
+
for (const key of [...kinds.types, ...kinds.apps]) {
|
|
413
|
+
const tone = Object.keys(map).find((name) => name.toLowerCase() === key)
|
|
414
|
+
if (tone && ITEM_TONES.includes(map[tone])) {
|
|
415
|
+
return `cn-nc-widget-widget__item--tone-${map[tone]}`
|
|
416
|
+
}
|
|
378
417
|
}
|
|
379
|
-
return
|
|
418
|
+
return ''
|
|
380
419
|
},
|
|
381
420
|
|
|
382
421
|
t,
|
|
@@ -13,6 +13,35 @@
|
|
|
13
13
|
:description="label('failedDetail')"
|
|
14
14
|
data-testid="cn-object-parties-failed" />
|
|
15
15
|
|
|
16
|
+
<div
|
|
17
|
+
v-else-if="isPrimaryPresentation"
|
|
18
|
+
class="cn-object-parties__main-card"
|
|
19
|
+
data-testid="cn-object-parties-main-card">
|
|
20
|
+
<div
|
|
21
|
+
v-if="mainParty"
|
|
22
|
+
class="cn-object-parties__main"
|
|
23
|
+
:data-party="mainParty.partyUuid"
|
|
24
|
+
data-testid="cn-object-parties-main">
|
|
25
|
+
<span class="cn-object-parties__main-initials" aria-hidden="true" data-testid="cn-object-parties-main-initials">{{ initialsOf(nameOf(mainParty)) }}</span>
|
|
26
|
+
<span class="cn-object-parties__main-text">
|
|
27
|
+
<strong class="cn-object-parties__main-name" data-testid="cn-object-parties-main-name">{{ nameOf(mainParty) }}</strong>
|
|
28
|
+
<span v-if="mainRole" class="cn-object-parties__muted cn-object-parties__main-role" data-testid="cn-object-parties-main-role">{{ mainRole }}</span>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<span v-else class="cn-object-parties__muted" data-testid="cn-object-parties-main-none">{{ label('noPrimary') }}</span>
|
|
32
|
+
<a
|
|
33
|
+
v-if="mainParty && mainParty.email"
|
|
34
|
+
class="cn-object-parties__main-email"
|
|
35
|
+
:href="`mailto:${mainParty.email}`"
|
|
36
|
+
data-testid="cn-object-parties-main-email">
|
|
37
|
+
<EmailOutline :size="16" aria-hidden="true" />{{ mainParty.email }}</a>
|
|
38
|
+
<a
|
|
39
|
+
v-if="footLink"
|
|
40
|
+
class="cn-object-parties__main-link"
|
|
41
|
+
:href="footLink.href"
|
|
42
|
+
data-testid="cn-object-parties-main-link">{{ footLink.label }}</a>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
16
45
|
<NcEmptyContent
|
|
17
46
|
v-else-if="groups.length === 0"
|
|
18
47
|
:name="label('empty')"
|
|
@@ -111,15 +140,18 @@ import axios from '@nextcloud/axios'
|
|
|
111
140
|
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
112
141
|
import { generateUrl } from '@nextcloud/router'
|
|
113
142
|
import { NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
143
|
+
import EmailOutline from 'vue-material-design-icons/EmailOutline.vue'
|
|
114
144
|
import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
|
|
115
145
|
import innerHeading from '../../mixins/innerHeading.js'
|
|
116
146
|
import { conditionalLine } from '../../utils/conditionalLine.js'
|
|
147
|
+
import { initialsOf } from '../../utils/initials.js'
|
|
117
148
|
import {
|
|
118
149
|
documentsOfParty,
|
|
119
150
|
indicatorsOf,
|
|
120
151
|
indicatorVerdict,
|
|
121
152
|
kindCode,
|
|
122
153
|
representedByMap,
|
|
154
|
+
roleLabel,
|
|
123
155
|
rolesInOrder,
|
|
124
156
|
} from '../../utils/objectParties.js'
|
|
125
157
|
import { readPath } from '../../utils/readPath.js'
|
|
@@ -141,6 +173,7 @@ function defaultLabels() {
|
|
|
141
173
|
primary: t('nextcloud-vue', 'Primary party'),
|
|
142
174
|
notAccepted: t('nextcloud-vue', 'Kind not accepted here'),
|
|
143
175
|
representedBy: t('nextcloud-vue', 'Represented by'),
|
|
176
|
+
noPrimary: t('nextcloud-vue', 'No main contact yet'),
|
|
144
177
|
}
|
|
145
178
|
}
|
|
146
179
|
|
|
@@ -164,6 +197,12 @@ function defaultLabels() {
|
|
|
164
197
|
* }
|
|
165
198
|
* ```
|
|
166
199
|
*
|
|
200
|
+
* `presentation: "primary"` draws only the primary party, as a side card
|
|
201
|
+
* does: initials, name and role label, its correspondence e-mail, and
|
|
202
|
+
* `link` (`{ label, href? | tab? }`, `{total}` = the number of parties) at
|
|
203
|
+
* the foot; `tab` opens that tab of the page through the route hash. It
|
|
204
|
+
* reads the listing only.
|
|
205
|
+
*
|
|
167
206
|
* A failed read says so and is never shown as "no parties".
|
|
168
207
|
*
|
|
169
208
|
* @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-record-shows-its-parties-by-role
|
|
@@ -171,7 +210,7 @@ function defaultLabels() {
|
|
|
171
210
|
export default {
|
|
172
211
|
name: 'CnObjectPartiesWidget',
|
|
173
212
|
|
|
174
|
-
components: { NcEmptyContent, NcLoadingIcon, NcNoteCard },
|
|
213
|
+
components: { EmailOutline, NcEmptyContent, NcLoadingIcon, NcNoteCard },
|
|
175
214
|
|
|
176
215
|
mixins: [innerHeading],
|
|
177
216
|
|
|
@@ -188,7 +227,9 @@ export default {
|
|
|
188
227
|
* `kindsSource` (`{ url, enforce?, sourceText? }`), `extraLines`
|
|
189
228
|
* (`[{ endpoint, path?, valueLabels?, lines }]`, one request per
|
|
190
229
|
* party, `{party}` filled in), `correspondence` (`{ endpoint, path?,
|
|
191
|
-
* senderField?, recipientsField? }`)
|
|
230
|
+
* senderField?, recipientsField? }`), `labels`, `presentation`
|
|
231
|
+
* (`"primary"`: only the primary party) and `link` (`{ label,
|
|
232
|
+
* href? | tab? }`, the primary presentation's foot link).
|
|
192
233
|
*/
|
|
193
234
|
content: {
|
|
194
235
|
type: Object,
|
|
@@ -235,6 +276,46 @@ export default {
|
|
|
235
276
|
return rolesInOrder(this.listing, this.translatedMap(this.content?.roleLabels))
|
|
236
277
|
},
|
|
237
278
|
|
|
279
|
+
/** @return {boolean} Whether only the primary party is drawn. */
|
|
280
|
+
isPrimaryPresentation() {
|
|
281
|
+
return this.content?.presentation === 'primary'
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/** @return {object|null} The primary party's listing row. */
|
|
285
|
+
mainParty() {
|
|
286
|
+
if (!this.primary) {
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
return (this.listing?.results || []).find((row) => row && row.partyUuid === this.primary) || null
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/** @return {string} The primary party's role, in the app's words. */
|
|
293
|
+
mainRole() {
|
|
294
|
+
const key = this.mainParty?.role
|
|
295
|
+
if (!key) {
|
|
296
|
+
return ''
|
|
297
|
+
}
|
|
298
|
+
const own = this.translatedMap(this.content?.roleLabels)
|
|
299
|
+
if (typeof own[key] === 'string' && own[key] !== '') {
|
|
300
|
+
return own[key]
|
|
301
|
+
}
|
|
302
|
+
return this.cnTranslate(roleLabel(key, this.listing?.roles, {}))
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
/** @return {{label: string, href: string}|null} The primary presentation's foot link; none without parties. */
|
|
306
|
+
footLink() {
|
|
307
|
+
const link = this.content?.link
|
|
308
|
+
const total = Number(this.listing?.total ?? (this.listing?.results || []).length)
|
|
309
|
+
if (!link || typeof link !== 'object' || !link.label || !(total > 0)) {
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
const href = link.tab ? `#${encodeURIComponent(link.tab)}` : (link.href || '')
|
|
313
|
+
if (href === '') {
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
return { label: this.cnTranslate(link.label).replaceAll('{total}', String(total)), href }
|
|
317
|
+
},
|
|
318
|
+
|
|
238
319
|
/** @return {string|null} The primary party's uuid. */
|
|
239
320
|
primary() {
|
|
240
321
|
return this.listing?.primary || null
|
|
@@ -301,6 +382,7 @@ export default {
|
|
|
301
382
|
},
|
|
302
383
|
|
|
303
384
|
methods: {
|
|
385
|
+
initialsOf,
|
|
304
386
|
kindCode,
|
|
305
387
|
|
|
306
388
|
/**
|
|
@@ -383,6 +465,10 @@ export default {
|
|
|
383
465
|
return
|
|
384
466
|
}
|
|
385
467
|
this.listing = listing
|
|
468
|
+
if (this.isPrimaryPresentation) {
|
|
469
|
+
this.loading = false
|
|
470
|
+
return
|
|
471
|
+
}
|
|
386
472
|
const uuids = [...new Set((listing.results || []).map((row) => row && row.partyUuid).filter(Boolean))]
|
|
387
473
|
const partyUrl = this.content?.partySource || '/apps/openregister/api/parties/{uuid}'
|
|
388
474
|
const records = await Promise.all(uuids.map((uuid) => this.read(this.urlOf(partyUrl, uuid))))
|
|
@@ -657,6 +743,57 @@ export default {
|
|
|
657
743
|
color: var(--color-text-maxcontrast);
|
|
658
744
|
}
|
|
659
745
|
|
|
746
|
+
.cn-object-parties__main-card {
|
|
747
|
+
display: flex;
|
|
748
|
+
flex-direction: column;
|
|
749
|
+
gap: calc(var(--default-grid-baseline, 4px) * 3);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.cn-object-parties__main {
|
|
753
|
+
display: flex;
|
|
754
|
+
align-items: center;
|
|
755
|
+
gap: calc(var(--default-grid-baseline, 4px) * 3);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.cn-object-parties__main-initials {
|
|
759
|
+
display: flex;
|
|
760
|
+
flex: none;
|
|
761
|
+
align-items: center;
|
|
762
|
+
justify-content: center;
|
|
763
|
+
width: 40px;
|
|
764
|
+
height: 40px;
|
|
765
|
+
border-radius: 50%;
|
|
766
|
+
background-color: var(--color-background-dark);
|
|
767
|
+
color: var(--color-main-text);
|
|
768
|
+
font-weight: 700;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.cn-object-parties__main-text {
|
|
772
|
+
display: flex;
|
|
773
|
+
flex-direction: column;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.cn-object-parties__main-name {
|
|
777
|
+
font-size: 15px;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.cn-object-parties__main-role {
|
|
781
|
+
font-size: 13px;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.cn-object-parties__main-email {
|
|
785
|
+
display: flex;
|
|
786
|
+
align-items: center;
|
|
787
|
+
gap: calc(var(--default-grid-baseline, 4px) * 2.5);
|
|
788
|
+
color: var(--color-main-text);
|
|
789
|
+
overflow-wrap: anywhere;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.cn-object-parties__main-link {
|
|
793
|
+
color: var(--color-primary-element);
|
|
794
|
+
font-weight: 600;
|
|
795
|
+
}
|
|
796
|
+
|
|
660
797
|
.cn-object-parties__accepted {
|
|
661
798
|
display: flex;
|
|
662
799
|
flex-wrap: wrap;
|
|
@@ -542,6 +542,7 @@ import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
|
|
|
542
542
|
import { unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
543
543
|
import { initialsOf } from '../../utils/initials.js'
|
|
544
544
|
import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
|
|
545
|
+
import { isObjectRowsSpec, objectRowsOf } from '../../utils/objectRows.js'
|
|
545
546
|
import { pageFilter, pageSort } from '../../utils/pageControls.js'
|
|
546
547
|
import { readPath } from '../../utils/readPath.js'
|
|
547
548
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveDeepTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -607,7 +608,8 @@ function unwrap(value) {
|
|
|
607
608
|
* `object-list` (`register`, `schema`, `filter` with the `@objectId` /
|
|
608
609
|
* `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
|
|
609
610
|
* `rowRoute`, `pageControls`), or `endpointSource` (an app endpoint whose
|
|
610
|
-
* payload, after `responsePath`, is the array of rows, as on the object table)
|
|
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;
|
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
|
}
|
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
|