@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.
Files changed (107) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +14 -0
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +5 -4
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js +9 -0
  7. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js.map +1 -0
  8. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js +237 -0
  9. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js.map +1 -0
  10. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js +103 -0
  11. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js.map +1 -0
  12. package/dist/esm/components/CnConditionListEditor/conditionOperators.js +121 -0
  13. package/dist/esm/components/CnConditionListEditor/conditionOperators.js.map +1 -0
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +12 -0
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  19. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +85 -10
  20. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +60 -36
  22. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue.js +1 -1
  27. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js +10 -0
  28. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  31. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
  32. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  34. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  36. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +258 -6
  37. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +283 -131
  39. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  41. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -2
  42. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
  46. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  48. package/dist/esm/dialogs/CnPartyAddDialog.vue.js +9 -0
  49. package/dist/esm/dialogs/CnPartyAddDialog.vue.js.map +1 -0
  50. package/dist/esm/dialogs/CnPartyAddDialog.vue2.js +184 -0
  51. package/dist/esm/dialogs/CnPartyAddDialog.vue2.js.map +1 -0
  52. package/dist/esm/dialogs/CnPartyAddDialog.vue3.js +168 -0
  53. package/dist/esm/dialogs/CnPartyAddDialog.vue3.js.map +1 -0
  54. package/dist/esm/index.js +2 -0
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/l10n/en.json.js +27 -1
  57. package/dist/esm/l10n/en.json.js.map +1 -1
  58. package/dist/esm/l10n/nl.json.js +27 -1
  59. package/dist/esm/l10n/nl.json.js.map +1 -1
  60. package/dist/esm/library-style.js +1 -1
  61. package/dist/esm/nextcloud-vue.css +353 -217
  62. package/dist/esm/store/plugins/liveUpdates.js +13 -4
  63. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  64. package/dist/esm/store/useObjectStore.js +28 -5
  65. package/dist/esm/store/useObjectStore.js.map +1 -1
  66. package/dist/esm/utils/detailActionModel.js +6 -0
  67. package/dist/esm/utils/detailActionModel.js.map +1 -1
  68. package/dist/esm/utils/listLenses.js +20 -1
  69. package/dist/esm/utils/listLenses.js.map +1 -1
  70. package/dist/esm/utils/objectParties.js +79 -1
  71. package/dist/esm/utils/objectParties.js.map +1 -1
  72. package/dist/esm/utils/objectRows.js +59 -0
  73. package/dist/esm/utils/objectRows.js.map +1 -0
  74. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  75. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  76. package/dist/nextcloud-vue.cjs.js +7460 -5803
  77. package/dist/nextcloud-vue.cjs.js.map +2 -2
  78. package/dist/nextcloud-vue.css +353 -217
  79. package/l10n/en.json +27 -1
  80. package/l10n/nl.json +27 -1
  81. package/package.json +1 -1
  82. package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
  83. package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
  84. package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
  85. package/src/components/CnConditionListEditor/index.js +17 -0
  86. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  87. package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
  88. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +105 -12
  89. package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
  90. package/src/components/CnIntegrationWidget/CnIntegrationWidget.vue +10 -0
  91. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
  92. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +401 -3
  93. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
  94. package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
  95. package/src/components/index.js +1 -0
  96. package/src/css/look-board-index.css +6 -0
  97. package/src/css/look-board.css +35 -7
  98. package/src/dialogs/CnPartyAddDialog.vue +309 -0
  99. package/src/index.js +2 -0
  100. package/src/schemas/app-manifest-v2.schema.json +17 -1
  101. package/src/store/plugins/liveUpdates.js +13 -4
  102. package/src/store/useObjectStore.js +28 -5
  103. package/src/utils/detailActionModel.js +6 -0
  104. package/src/utils/listLenses.js +20 -1
  105. package/src/utils/objectParties.js +78 -0
  106. package/src/utils/objectRows.js +55 -0
  107. 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 === 'select'"
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 === 'select'"
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? }]`: 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'`.
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: (Array.isArray(field.options) ? field.options : []).map((option) => (
532
- typeof option === 'object' ? { value: option.value, label: this.tr(option.label || String(option.value)) } : { value: option, label: this.tr(String(option)) }
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.type === 'checkbox' ? false : ''
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.type === 'checkbox' ? false : '') : value
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 (value !== undefined && value !== null && String(value).trim() !== '') {
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
- const appsOf = [own(item.app)]
370
- for (const url of [item.link, item.iconUrl, item.overlayIconUrl]) {
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
- appsOf.push(match[1].toLowerCase())
392
+ apps.push(match[1].toLowerCase())
374
393
  }
375
394
  }
376
- if (apps.length > 0 && appsOf.some((app) => app !== '' && apps.includes(app))) {
377
- return true
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 types.length > 0 && [own(item.type), own(item.objectType)].some((type) => type !== '' && types.includes(type))
418
+ return ''
380
419
  },
381
420
 
382
421
  t,