@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
|
@@ -25,7 +25,16 @@
|
|
|
25
25
|
class="cn-field-list-widget__field"
|
|
26
26
|
:data-testid="`cn-field-list-input-${field.key}`">
|
|
27
27
|
<NcSelect
|
|
28
|
-
v-if="field.type === '
|
|
28
|
+
v-if="field.type === 'multiselect'"
|
|
29
|
+
v-model="draft[field.key]"
|
|
30
|
+
:inputLabel="field.label"
|
|
31
|
+
:options="field.options"
|
|
32
|
+
:reduce="(option) => option.value"
|
|
33
|
+
:multiple="true"
|
|
34
|
+
:keepOpen="true"
|
|
35
|
+
label="label" />
|
|
36
|
+
<NcSelect
|
|
37
|
+
v-else-if="field.type === 'select'"
|
|
29
38
|
v-model="draft[field.key]"
|
|
30
39
|
:inputLabel="field.label"
|
|
31
40
|
:options="field.options"
|
|
@@ -247,7 +256,16 @@
|
|
|
247
256
|
class="cn-field-list-widget__field"
|
|
248
257
|
:data-testid="`cn-field-list-input-${field.key}`">
|
|
249
258
|
<NcSelect
|
|
250
|
-
v-if="field.type === '
|
|
259
|
+
v-if="field.type === 'multiselect'"
|
|
260
|
+
v-model="draft[field.key]"
|
|
261
|
+
:inputLabel="field.label"
|
|
262
|
+
:options="field.options"
|
|
263
|
+
:reduce="(option) => option.value"
|
|
264
|
+
:multiple="true"
|
|
265
|
+
:keepOpen="true"
|
|
266
|
+
label="label" />
|
|
267
|
+
<NcSelect
|
|
268
|
+
v-else-if="field.type === 'select'"
|
|
251
269
|
v-model="draft[field.key]"
|
|
252
270
|
:inputLabel="field.label"
|
|
253
271
|
:options="field.options"
|
|
@@ -365,12 +383,12 @@ export default {
|
|
|
365
383
|
* - `field` (required) — the object field holding the array.
|
|
366
384
|
* - `key` — the item field that identifies an entry (default: the item itself for strings).
|
|
367
385
|
* - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when?, copy?: [{ path, label }] } }`. `detail.copy` puts a copy button beside the detail line for each named value of the row (an empty value gets none), such as a DNS record's name and value.
|
|
368
|
-
* - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.
|
|
386
|
+
* - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select'|'multiselect'|'checkbox', options?, optionsSource?, required?, pattern?, patternMessage? }], unique?, defaults? }`. A `multiselect` field holds a list of values. `optionsSource` — `{ url, path?, valuePath?, labelPath? }` (default `id` and `name`) — adds options read from an endpoint after the static ones, each value once (tokens `@objectId`, `@object.<field>`), for choices that live in other records such as a case type's statuses.
|
|
369
387
|
* - `remove` — `{ label? }`; omitted = no remove.
|
|
370
388
|
* - `rowActions` — `[{ id, label, ariaLabel?, when?, placement?: 'band'|'inline', request: { method?, url, payload? } }]`, or `{ id, label, dialog, props }` to open a modal registered in the app registry with filled props (shown in `readOnly` too, since it writes nothing, and only when every token in `props` fills); tokens `@object.<field>`, `@objectId`, `@row.<field>`, `@index`. In the table variant `placement: 'inline'` draws the action as a button in the row instead of in the "..." menu; `ariaLabel` is a template filled from the row.
|
|
371
389
|
* - `when` (row actions, `detail`) — `{ field: value }`, every field equal; or per field `{ eq?, neq?, set? }`, where a value `@row.<field>` (or `@<field>`) is another field of the same row: `{ deliveredRef: { set: true, neq: '@originalRef' } }`.
|
|
372
390
|
* - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.
|
|
373
|
-
* - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap? }]
|
|
391
|
+
* - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap?, labelsFrom? }]` (a list value reads as its items joined; `labelsFrom` names an add field whose option labels word the values): rows as a table; a per-row "..." menu holds the row actions and remove; `detail` lines become a warning band under the table, with the row actions that have `placement: 'band'`.
|
|
374
392
|
* - `add.trigger: 'button'`, `add.placement: 'header' | 'title' | 'footer'` — the add form opens from a button; `title` reports the button for the card's title row on the `header` event.
|
|
375
393
|
* - `readOnly`, `hint`, `emptyText`.
|
|
376
394
|
* - `row.href` — the path of a link for the row title; only http(s) and root-relative links, an absolute one opens in a new tab.
|
|
@@ -431,6 +449,8 @@ export default {
|
|
|
431
449
|
noticeType: 'error',
|
|
432
450
|
// What the last copy button put on the clipboard, said to a screen reader.
|
|
433
451
|
copied: '',
|
|
452
|
+
/** Options read through each add field's `optionsSource`, by field key. */
|
|
453
|
+
sourcedOptions: {},
|
|
434
454
|
}
|
|
435
455
|
},
|
|
436
456
|
|
|
@@ -528,9 +548,12 @@ export default {
|
|
|
528
548
|
.map((field) => ({
|
|
529
549
|
...field,
|
|
530
550
|
label: this.tr(field.label || field.key),
|
|
531
|
-
options:
|
|
532
|
-
|
|
533
|
-
|
|
551
|
+
options: [
|
|
552
|
+
...(Array.isArray(field.options) ? field.options : []).map((option) => (
|
|
553
|
+
typeof option === 'object' ? { value: option.value, label: this.tr(option.label || String(option.value)) } : { value: option, label: this.tr(String(option)) }
|
|
554
|
+
)),
|
|
555
|
+
...(this.sourcedOptions[field.key] || []),
|
|
556
|
+
],
|
|
534
557
|
}))
|
|
535
558
|
},
|
|
536
559
|
|
|
@@ -652,12 +675,14 @@ export default {
|
|
|
652
675
|
objectData() {
|
|
653
676
|
this.saved = null
|
|
654
677
|
this.loadLive()
|
|
678
|
+
this.loadOptions()
|
|
655
679
|
},
|
|
656
680
|
},
|
|
657
681
|
|
|
658
682
|
created() {
|
|
659
683
|
this.resetDraft()
|
|
660
684
|
this.loadLive()
|
|
685
|
+
this.loadOptions()
|
|
661
686
|
},
|
|
662
687
|
|
|
663
688
|
methods: {
|
|
@@ -746,12 +771,77 @@ export default {
|
|
|
746
771
|
}
|
|
747
772
|
return def.format === 'date-long' ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1)) : formatBoardDate(date)
|
|
748
773
|
}
|
|
774
|
+
if (Array.isArray(raw)) {
|
|
775
|
+
return raw.map((item) => this.wordOf(item, def)).filter((word) => word !== '').join(', ')
|
|
776
|
+
}
|
|
777
|
+
return this.wordOf(raw, def)
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* One value in words: through the column's `map`, then the option labels
|
|
782
|
+
* of the add field it names in `labelsFrom`, else the value itself.
|
|
783
|
+
*
|
|
784
|
+
* @param {string|number|boolean|object} raw The value.
|
|
785
|
+
* @param {object} def The column.
|
|
786
|
+
* @return {string}
|
|
787
|
+
*/
|
|
788
|
+
wordOf(raw, def) {
|
|
749
789
|
if (def.map && Object.hasOwn(def.map, String(raw))) {
|
|
750
790
|
return this.tr(def.map[String(raw)])
|
|
751
791
|
}
|
|
792
|
+
if (def.labelsFrom) {
|
|
793
|
+
const field = this.addFields.find((candidate) => candidate.key === def.labelsFrom)
|
|
794
|
+
const option = field && field.options.find((candidate) => String(candidate.value) === String(raw))
|
|
795
|
+
if (option) {
|
|
796
|
+
return option.label
|
|
797
|
+
}
|
|
798
|
+
}
|
|
752
799
|
return typeof raw === 'object' ? '' : String(raw)
|
|
753
800
|
},
|
|
754
801
|
|
|
802
|
+
/**
|
|
803
|
+
* The empty value of an add field: false for a checkbox, a list for a multiselect.
|
|
804
|
+
*
|
|
805
|
+
* @param {object} field The add field.
|
|
806
|
+
* @return {boolean|Array|string}
|
|
807
|
+
*/
|
|
808
|
+
emptyValue(field) {
|
|
809
|
+
if (field.type === 'checkbox') {
|
|
810
|
+
return false
|
|
811
|
+
}
|
|
812
|
+
return field.type === 'multiselect' ? [] : ''
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Read the options of every add field that names an `optionsSource`.
|
|
817
|
+
*
|
|
818
|
+
* A source that cannot be read leaves the static options alone.
|
|
819
|
+
*
|
|
820
|
+
* @return {Promise<void>}
|
|
821
|
+
*/
|
|
822
|
+
async loadOptions() {
|
|
823
|
+
const fields = this.content.add && Array.isArray(this.content.add.fields) ? this.content.add.fields : []
|
|
824
|
+
for (const field of fields) {
|
|
825
|
+
const source = field && field.optionsSource
|
|
826
|
+
if (!source || !source.url || !field.key) {
|
|
827
|
+
continue
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const { axios } = await this.http()
|
|
831
|
+
const response = await axios.get(prefixUrl(this.resolve(source.url, {})))
|
|
832
|
+
const list = source.path ? readPath(response && response.data, source.path) : response && response.data
|
|
833
|
+
const options = (Array.isArray(list) ? list : [])
|
|
834
|
+
.map((item) => ({ value: readPath(item, source.valuePath || 'id'), label: String(readPath(item, source.labelPath || 'name') ?? '') }))
|
|
835
|
+
.filter((option, index, all) => option.value !== undefined && option.value !== null && option.value !== ''
|
|
836
|
+
&& all.findIndex((other) => String(other.value) === String(option.value)) === index)
|
|
837
|
+
this.sourcedOptions = { ...this.sourcedOptions, [field.key]: options }
|
|
838
|
+
} catch (e) {
|
|
839
|
+
// eslint-disable-next-line no-console
|
|
840
|
+
console.warn('[CnFieldListWidget] optionsSource could not be read:', e?.message || e)
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
|
|
755
845
|
pillOf(entry, spec) {
|
|
756
846
|
if (!spec || !spec.path) {
|
|
757
847
|
return null
|
|
@@ -923,7 +1013,7 @@ export default {
|
|
|
923
1013
|
resetDraft() {
|
|
924
1014
|
const draft = {}
|
|
925
1015
|
for (const field of this.addFields) {
|
|
926
|
-
const empty = field
|
|
1016
|
+
const empty = this.emptyValue(field)
|
|
927
1017
|
const first = field.type === 'select' && field.options.length > 0 ? field.options[0].value : empty
|
|
928
1018
|
draft[field.key] = field.default !== undefined ? field.default : first
|
|
929
1019
|
}
|
|
@@ -1078,10 +1168,10 @@ export default {
|
|
|
1078
1168
|
const errors = {}
|
|
1079
1169
|
for (const field of this.addFields) {
|
|
1080
1170
|
const value = this.draft[field.key]
|
|
1081
|
-
const empty = value === undefined || value === null || String(value).trim() === ''
|
|
1171
|
+
const empty = value === undefined || value === null || (Array.isArray(value) ? value.length === 0 : String(value).trim() === '')
|
|
1082
1172
|
if (field.required && empty) {
|
|
1083
1173
|
errors[field.key] = this.tr('Fill this in.')
|
|
1084
|
-
} else if (!empty && field.pattern && !new RegExp(field.pattern).test(String(value).trim())) {
|
|
1174
|
+
} else if (!empty && field.pattern && !Array.isArray(value) && !new RegExp(field.pattern).test(String(value).trim())) {
|
|
1085
1175
|
errors[field.key] = this.tr(field.patternMessage || 'This value is not valid.')
|
|
1086
1176
|
}
|
|
1087
1177
|
}
|
|
@@ -1161,7 +1251,7 @@ export default {
|
|
|
1161
1251
|
for (const [index, field] of this.addFields.entries()) {
|
|
1162
1252
|
const scalarValue = this.content.add?.scalar === true && index === 0 ? row.item : undefined
|
|
1163
1253
|
const value = row.item && typeof row.item === 'object' ? readPath(row.item, field.key) : scalarValue
|
|
1164
|
-
draft[field.key] = value === undefined || value === null ? (field
|
|
1254
|
+
draft[field.key] = value === undefined || value === null ? this.emptyValue(field) : value
|
|
1165
1255
|
}
|
|
1166
1256
|
this.draft = draft
|
|
1167
1257
|
this.errors = {}
|
|
@@ -1194,7 +1284,10 @@ export default {
|
|
|
1194
1284
|
const entry = { ...(add.defaults || {}) }
|
|
1195
1285
|
for (const field of this.addFields) {
|
|
1196
1286
|
const value = this.draft[field.key]
|
|
1197
|
-
if (
|
|
1287
|
+
if (Array.isArray(value)) {
|
|
1288
|
+
// An emptied list is written as empty, so an edit can clear it.
|
|
1289
|
+
entry[field.key] = value
|
|
1290
|
+
} else if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
1198
1291
|
entry[field.key] = trimmed(value)
|
|
1199
1292
|
}
|
|
1200
1293
|
}
|
|
@@ -8931,7 +8931,14 @@ export default {
|
|
|
8931
8931
|
deep: true,
|
|
8932
8932
|
},
|
|
8933
8933
|
|
|
8934
|
-
'$route.query._view': function() {
|
|
8934
|
+
'$route.query._view': function(now, before) {
|
|
8935
|
+
// Leaving the mode's address (a plain link to the page) leaves the
|
|
8936
|
+
// mode it opened, unless the person picked another one since.
|
|
8937
|
+
if ((typeof now !== 'string' || now === '') && typeof before === 'string' && before !== '' && before === this.currentViewMode) {
|
|
8938
|
+
this.routeViewPending = false
|
|
8939
|
+
this.currentViewMode = this.viewMode
|
|
8940
|
+
return
|
|
8941
|
+
}
|
|
8935
8942
|
this.applyViewModeFromRoute()
|
|
8936
8943
|
},
|
|
8937
8944
|
|
|
@@ -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,
|
|
@@ -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,
|