@conduction/nextcloud-vue 2.100.0 → 2.101.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 (127) hide show
  1. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  2. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
  3. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  4. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
  5. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  6. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  7. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +51 -7
  8. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +104 -73
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  11. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
  12. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
  13. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
  14. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
  15. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
  16. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
  17. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  18. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
  19. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  20. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
  21. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  22. package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
  23. package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
  24. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
  25. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
  26. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
  27. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
  28. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
  29. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
  30. package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
  31. package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +251 -71
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +317 -209
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/useNamedSource.js +4 -1
  37. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  38. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  39. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
  40. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  48. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
  49. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  50. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
  51. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  52. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  53. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
  54. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  55. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
  56. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  57. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
  58. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
  59. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
  60. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
  61. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
  62. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
  63. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
  65. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  66. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  67. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
  68. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  69. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  70. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  71. package/dist/esm/composables/indexSources.js +167 -20
  72. package/dist/esm/composables/indexSources.js.map +1 -1
  73. package/dist/esm/composables/useEndpointSource.js +28 -6
  74. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  75. package/dist/esm/index.js +1 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/l10n/en.json.js +37 -1
  78. package/dist/esm/l10n/en.json.js.map +1 -1
  79. package/dist/esm/l10n/nl.json.js +35 -1
  80. package/dist/esm/l10n/nl.json.js.map +1 -1
  81. package/dist/esm/library-style.js +1 -1
  82. package/dist/esm/nextcloud-vue.css +769 -251
  83. package/dist/esm/utils/languageMapText.js +50 -0
  84. package/dist/esm/utils/languageMapText.js.map +1 -0
  85. package/dist/esm/utils/libraryWidgetKeys.js +3 -0
  86. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  87. package/dist/esm/utils/mapList.js +71 -0
  88. package/dist/esm/utils/mapList.js.map +1 -0
  89. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  90. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  91. package/dist/nextcloud-vue.cjs.js +8977 -6843
  92. package/dist/nextcloud-vue.cjs.js.map +2 -2
  93. package/dist/nextcloud-vue.css +769 -251
  94. package/l10n/en.json +37 -1
  95. package/l10n/nl.json +35 -1
  96. package/package.json +1 -1
  97. package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
  98. package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
  99. package/src/components/CnDashboardPage/CnDashboardPage.vue +89 -20
  100. package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
  101. package/src/components/CnExportsWidget/index.js +7 -0
  102. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
  103. package/src/components/CnFilesBrowser/filesReview.js +57 -0
  104. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
  105. package/src/components/CnGroupedCardsWidget/index.js +28 -0
  106. package/src/components/CnIndexPage/CnIndexPage.vue +240 -4
  107. package/src/components/CnIndexPage/useNamedSource.js +4 -1
  108. package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
  109. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
  110. package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
  111. package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
  112. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
  113. package/src/components/CnSavedViewAlertWidget/index.js +7 -0
  114. package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
  115. package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
  116. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  117. package/src/components/index.js +3 -0
  118. package/src/composables/indexSources.js +166 -19
  119. package/src/composables/useEndpointSource.js +28 -6
  120. package/src/css/index-page.css +117 -0
  121. package/src/css/look-board.css +44 -0
  122. package/src/index.js +1 -0
  123. package/src/schemas/app-manifest-v2.schema.json +31 -1
  124. package/src/utils/languageMapText.js +46 -0
  125. package/src/utils/libraryWidgetKeys.js +3 -0
  126. package/src/utils/mapList.js +67 -0
  127. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -6,6 +6,8 @@ import HandBackRight from 'vue-material-design-icons/HandBackRight.vue';
6
6
  import LightningBolt from 'vue-material-design-icons/LightningBolt.vue';
7
7
  import Pencil from 'vue-material-design-icons/Pencil.vue';
8
8
  import { formatRelativeDate } from '../utils/boardDate.js';
9
+ import { fetchReferenceObjects } from '../utils/boardReferenceColumns.js';
10
+ import { fillTemplate } from '../utils/fillTemplate.js';
9
11
  import { flowRunStatuses, FLOW_RUN_ERROR_STATUSES } from '../utils/flowRunStatus.js';
10
12
  import { fetchGroupMembers, fetchStatuses, fetchPresence } from '../utils/nextcloudDirectory.js';
11
13
  import { readPath } from '../utils/readPath.js';
@@ -649,6 +651,10 @@ function readRowsPath(body, path) {
649
651
  * requested: page-config keys must never become query parameters. The list
650
652
  * stays empty and one console error names the page.
651
653
  *
654
+ * A quick filter with `showCount` is counted with `count(config)`: the same
655
+ * request with that tab's filter, its total read at `sourceConfig.totalPath`
656
+ * (`total` by default), else the number of rows the answer holds.
657
+ *
652
658
  * @return {object} The source adapter.
653
659
  * @spec openspec/changes/index-endpoint-source/specs/index-page/spec.md#requirement-an-index-page-can-list-rows-from-an-app-endpoint
654
660
  */
@@ -657,6 +663,36 @@ function endpointSource() {
657
663
  let seq = 0;
658
664
  let warned = false;
659
665
 
666
+ /**
667
+ * The query parameters for a config: every key but the url and the paths,
668
+ * empty values left out, a list joined by commas.
669
+ *
670
+ * @param {object} rest The config without `url`, `rowsPath` and `totalPath`.
671
+ * @return {object} The parameters.
672
+ */
673
+ const paramsOf = (rest) => {
674
+ const params = {};
675
+ for (const [key, value] of Object.entries(rest)) {
676
+ if (value === undefined || value === null || value === '') {
677
+ continue
678
+ }
679
+ params[key] = Array.isArray(value) ? value.join(',') : (typeof value === 'object' ? JSON.stringify(value) : value);
680
+ }
681
+ return params
682
+ };
683
+
684
+ /**
685
+ * Whether a config can be requested: it names a url and is not the page
686
+ * config handed over in its place.
687
+ *
688
+ * @param {object} config The config.
689
+ * @return {boolean}
690
+ */
691
+ const requestable = (config) => {
692
+ const url = config && config.url;
693
+ return typeof url === 'string' && url !== '' && !Object.hasOwn(config, 'entitySource')
694
+ };
695
+
660
696
  return {
661
697
  /**
662
698
  * Fetch the rows.
@@ -665,10 +701,9 @@ function endpointSource() {
665
701
  * @return {Promise<void>} Resolves when loaded; a failed request leaves no rows and rethrows.
666
702
  */
667
703
  load: async (config) => {
668
- const { url, rowsPath, ...rest } = config || {};
704
+ const { url, rowsPath, totalPath, ...rest } = config || {};
669
705
  const mine = ++seq;
670
- const pageConfig = Boolean(config && Object.hasOwn(config, 'entitySource'));
671
- if (typeof url !== 'string' || url === '' || pageConfig) {
706
+ if (!requestable(config)) {
672
707
  state.rows = [];
673
708
  if (!warned) {
674
709
  warned = true;
@@ -678,13 +713,7 @@ function endpointSource() {
678
713
  }
679
714
  return
680
715
  }
681
- const params = {};
682
- for (const [key, value] of Object.entries(rest)) {
683
- if (value === undefined || value === null || value === '') {
684
- continue
685
- }
686
- params[key] = Array.isArray(value) ? value.join(',') : (typeof value === 'object' ? JSON.stringify(value) : value);
687
- }
716
+ const params = paramsOf(rest);
688
717
  state.loading = true;
689
718
  try {
690
719
  const { default: axios } = await import('@nextcloud/axios');
@@ -706,6 +735,29 @@ function endpointSource() {
706
735
  }
707
736
  },
708
737
 
738
+ /**
739
+ * Count the rows for a config without touching the rows on screen, for
740
+ * a quick-filter tab with `showCount`.
741
+ *
742
+ * @param {object} config The page's source config with the tab's filter merged in.
743
+ * @return {Promise<number>} The total, or NaN when nothing can be requested.
744
+ */
745
+ count: async (config) => {
746
+ if (!requestable(config)) {
747
+ return Number.NaN
748
+ }
749
+ const { url, rowsPath, totalPath, ...rest } = config;
750
+ const { default: axios } = await import('@nextcloud/axios');
751
+ const res = await axios.get(generateUrl(url), { params: paramsOf(rest) });
752
+ const body = res ? res.data : null;
753
+ const total = body && typeof body === 'object' && !Array.isArray(body) ? readRowsPath(body, totalPath || 'total') : undefined;
754
+ if (typeof total === 'number' && Number.isFinite(total)) {
755
+ return total
756
+ }
757
+ const rows = Array.isArray(body) ? body : readRowsPath(body, rowsPath || 'results');
758
+ return Array.isArray(rows) ? rows.length : Number.NaN
759
+ },
760
+
709
761
  /**
710
762
  * @return {Array<object>} The rows the endpoint returned.
711
763
  */
@@ -738,39 +790,102 @@ function fillRowTemplate(template, row) {
738
790
  * and its count (DqZoekresultaten).
739
791
  *
740
792
  * `sourceConfig.kinds` lists what is searched:
741
- * `{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }`
793
+ * `{ id, label, register, schema, titleField?, subtitleField?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
742
794
  * for objects (OpenRegister's `_search`, so the reader sees what they may
743
795
  * read), or `{ id, label, files: true, limit? }` for the reader's files
744
796
  * (Nextcloud's unified search). The term is `sourceConfig.term`, else the
745
797
  * address's `?q=` (or `sourceConfig.termQuery`). A kind that fails shows no
746
798
  * rows and no count; the others still answer.
747
799
  *
800
+ * The kind's `label` goes through the app's translate (the "Soort" cell and
801
+ * the quick filters). `statusResolve: { register?, schema, labelField? }`
802
+ * reads `statusField` as a reference and shows the referenced object's label
803
+ * (`labelField`, else `title`, then `name`); the status column is a pill,
804
+ * coloured by `sourceConfig.statusColorMap`. `linkedTemplate` (a field
805
+ * template, "{case.title}") or `linkedField` (a path) fills the "Linked"
806
+ * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
807
+ * The page's title and count line can name the term as `{term}`, and the
808
+ * count is the active kind's total. Kind, link and status are filterable.
809
+ *
748
810
  * @param {object} [config] The page's source config.
811
+ * @param {object} [context] The page's helpers.
812
+ * @param {Function} [context.translate] The app's translate.
749
813
  * @return {object} The source adapter.
750
814
  * @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
751
815
  */
752
- function searchSource(config = {}) {
816
+ function searchSource(config = {}, context = {}) {
817
+ const translate$1 = typeof context.translate === 'function' ? context.translate : (text) => text;
753
818
  const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
754
- .filter((kind) => kind && typeof kind.id === 'string' && kind.id !== '' && (kind.files === true || (kind.register && kind.schema)));
755
- const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '' });
819
+ .filter((kind) => kind && typeof kind.id === 'string' && kind.id !== '' && (kind.files === true || (kind.register && kind.schema)))
820
+ .map((kind) => ({ ...kind, label: translate$1(String(kind.label || kind.id)) }));
821
+ const hasLinked = kinds.some((kind) => kind.linkedTemplate || kind.linkedField);
822
+ const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '', filters: {} });
756
823
  let seq = 0;
757
824
 
825
+ /**
826
+ * The labels of the objects a kind's status points at, by id: the
827
+ * referenced schema read once per search.
828
+ *
829
+ * @param {object} kind The kind with `statusResolve`.
830
+ * @return {Promise<Map<string, string>>} Labels by id; empty when it cannot be read.
831
+ */
832
+ const statusLabels = async (kind) => {
833
+ const resolve = kind.statusResolve;
834
+ if (!resolve || !resolve.schema) {
835
+ return new Map()
836
+ }
837
+ const objects = await fetchReferenceObjects({ register: resolve.register || kind.register, schema: resolve.schema }).catch(() => []);
838
+ const labels = new Map();
839
+ for (const object of objects) {
840
+ const id = object.id || object.uuid || (object['@self'] && object['@self'].id);
841
+ const label = readPath(object, resolve.labelField || 'title') ?? object.name;
842
+ if (id !== undefined && label !== undefined && label !== null) {
843
+ labels.set(String(id), String(label));
844
+ }
845
+ }
846
+ return labels
847
+ };
848
+
849
+ /**
850
+ * What a row shows in the Linked column.
851
+ *
852
+ * @param {object} kind The row's kind.
853
+ * @param {object} object The found object.
854
+ * @return {string}
855
+ */
856
+ const linkedOf = (kind, object) => {
857
+ if (kind.linkedTemplate) {
858
+ return fillTemplate(translate$1(kind.linkedTemplate), object)
859
+ }
860
+ if (kind.linkedField) {
861
+ const value = readPath(object, kind.linkedField);
862
+ return value === undefined || value === null ? '' : String(value)
863
+ }
864
+ return ''
865
+ };
866
+
758
867
  const searchObjects = async (axios, kind, term) => {
759
868
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema });
760
869
  const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } });
761
870
  const body = (res && res.data) || {};
762
871
  const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : []);
872
+ const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null;
763
873
  return {
764
874
  total: typeof body.total === 'number' ? body.total : results.length,
765
875
  rows: results.map((object) => {
766
876
  const id = object.id || object.uuid || (object['@self'] && object['@self'].id);
877
+ const rawStatus = kind.statusField ? readPath(object, kind.statusField) : undefined;
878
+ const status = rawStatus === undefined || rawStatus === null
879
+ ? ''
880
+ : (labels ? (labels.get(String(rawStatus)) || '') : String(rawStatus));
767
881
  return {
768
882
  id: `${kind.id}:${id}`,
769
883
  kind: kind.id,
770
884
  kindLabel: kind.label,
771
885
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
772
886
  subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
773
- status: kind.statusField ? String(readPath(object, kind.statusField) ?? '') : '',
887
+ status,
888
+ linked: linkedOf(kind, object),
774
889
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
775
890
  object,
776
891
  }
@@ -793,6 +908,7 @@ function searchSource(config = {}) {
793
908
  title: entry.title || '',
794
909
  subtitle: entry.subline || '',
795
910
  status: '',
911
+ linked: '',
796
912
  href: entry.resourceUrl || '',
797
913
  })),
798
914
  }
@@ -807,6 +923,17 @@ function searchSource(config = {}) {
807
923
  */
808
924
  load: async (cfg) => {
809
925
  state.kind = cfg && typeof cfg.kind === 'string' ? cfg.kind : 'all';
926
+ // The header filters on Kind, Linked and Status narrow the answer
927
+ // already fetched; they do not search again.
928
+ const filters = {};
929
+ for (const key of ['kindLabel', 'linked', 'status']) {
930
+ const value = cfg && cfg[key];
931
+ const values = (Array.isArray(value) ? value : (value === undefined || value === null || value === '' ? [] : [value])).map(String);
932
+ if (values.length > 0) {
933
+ filters[key] = values;
934
+ }
935
+ }
936
+ state.filters = filters;
810
937
  const query = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null;
811
938
  const term = String((cfg && cfg.term) || (query && query.get((cfg && cfg.termQuery) || 'q')) || '').trim();
812
939
  const mine = ++seq;
@@ -838,7 +965,20 @@ function searchSource(config = {}) {
838
965
  /**
839
966
  * @return {Array<object>} The results of the active kind, or all of them.
840
967
  */
841
- rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind)),
968
+ rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind))
969
+ .filter((row) => Object.entries(state.filters).every(([key, values]) => values.includes(String(row[key])))),
970
+
971
+ /**
972
+ * @return {string} The term searched for, for a `{term}` in the title or count line.
973
+ */
974
+ term: () => state.term,
975
+
976
+ /**
977
+ * @return {number} The active kind's total, or every kind's together on All.
978
+ */
979
+ total: () => (state.kind === 'all'
980
+ ? Object.values(state.totals).reduce((sum, value) => sum + value, 0)
981
+ : (state.totals[state.kind] || 0)),
842
982
 
843
983
  /**
844
984
  * @return {boolean} Whether a search is in flight.
@@ -847,8 +987,14 @@ function searchSource(config = {}) {
847
987
 
848
988
  columns: [
849
989
  { key: 'title', label: translate('nextcloud-vue', 'Result'), secondary: 'subtitle' },
850
- { key: 'kindLabel', label: translate('nextcloud-vue', 'Kind') },
851
- { key: 'status', label: translate('nextcloud-vue', 'Status') },
990
+ { key: 'kindLabel', label: translate('nextcloud-vue', 'Kind'), enum: kinds.map((kind) => kind.label) },
991
+ ...(hasLinked ? [{ key: 'linked', label: config.linkedLabel ? translate$1(config.linkedLabel) : translate('nextcloud-vue', 'Linked') }] : []),
992
+ {
993
+ key: 'status',
994
+ label: translate('nextcloud-vue', 'Status'),
995
+ widget: 'badge',
996
+ widgetProps: { colorMap: (config && config.statusColorMap) || {} },
997
+ },
852
998
  ],
853
999
 
854
1000
  quickFilters: kinds.length > 0
@@ -1119,10 +1265,11 @@ const indexSources = {
1119
1265
  *
1120
1266
  * @param {string} name The source name from `config.source`.
1121
1267
  * @param {object} [config] The page's source config, for a source that builds its tabs from it.
1268
+ * @param {object} [context] The page's helpers a source may use, such as `translate` (the app's catalogue).
1122
1269
  *
1123
1270
  * @return {object|null} The adapter, or null when the name is not registered.
1124
1271
  */
1125
- function resolveIndexSource(name, config) {
1272
+ function resolveIndexSource(name, config, context) {
1126
1273
  const key = String(name || '');
1127
1274
  if (!key) {
1128
1275
  return null
@@ -1135,7 +1282,7 @@ function resolveIndexSource(name, config) {
1135
1282
  return null
1136
1283
  }
1137
1284
 
1138
- return factory(config || {})
1285
+ return factory(config || {}, context || {})
1139
1286
  }
1140
1287
 
1141
1288
  /**