@conduction/nextcloud-vue 2.99.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 (180) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +6 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
  10. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +51 -7
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +104 -73
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +25 -8
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -8
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
  26. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
  28. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
  30. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  37. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
  38. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
  40. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
  42. package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
  43. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  44. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
  45. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  46. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
  47. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
  49. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
  51. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
  53. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
  54. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
  55. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
  56. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
  57. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
  58. package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
  59. package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +387 -78
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +317 -209
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/useNamedSource.js +4 -1
  65. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  66. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  67. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
  68. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  71. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
  72. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  73. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
  74. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  75. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  81. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
  82. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  83. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
  84. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  85. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  90. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
  91. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
  92. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
  93. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
  95. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
  96. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +1 -1
  97. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +38 -9
  98. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -1
  99. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -1
  100. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  101. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +51 -4
  102. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  103. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
  104. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  105. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
  106. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  110. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
  111. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  112. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  113. package/dist/esm/composables/indexSources.js +167 -20
  114. package/dist/esm/composables/indexSources.js.map +1 -1
  115. package/dist/esm/composables/useEndpointSource.js +28 -6
  116. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  117. package/dist/esm/index.js +1 -0
  118. package/dist/esm/index.js.map +1 -1
  119. package/dist/esm/l10n/en.json.js +43 -1
  120. package/dist/esm/l10n/en.json.js.map +1 -1
  121. package/dist/esm/l10n/nl.json.js +41 -1
  122. package/dist/esm/l10n/nl.json.js.map +1 -1
  123. package/dist/esm/library-style.js +1 -1
  124. package/dist/esm/nextcloud-vue.css +1101 -396
  125. package/dist/esm/utils/languageMapText.js +50 -0
  126. package/dist/esm/utils/languageMapText.js.map +1 -0
  127. package/dist/esm/utils/libraryWidgetKeys.js +3 -0
  128. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  129. package/dist/esm/utils/mapList.js +71 -0
  130. package/dist/esm/utils/mapList.js.map +1 -0
  131. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  132. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  133. package/dist/nextcloud-vue.cjs.js +9996 -7378
  134. package/dist/nextcloud-vue.cjs.js.map +2 -2
  135. package/dist/nextcloud-vue.css +1101 -396
  136. package/l10n/en.json +43 -1
  137. package/l10n/nl.json +41 -1
  138. package/package.json +1 -1
  139. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -1
  140. package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
  141. package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
  142. package/src/components/CnDashboardPage/CnDashboardPage.vue +89 -20
  143. package/src/components/CnDetailPage/CnDetailPage.vue +28 -9
  144. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
  145. package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
  146. package/src/components/CnExportsWidget/index.js +7 -0
  147. package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
  148. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
  149. package/src/components/CnFilesBrowser/filesReview.js +57 -0
  150. package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
  151. package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
  152. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
  153. package/src/components/CnGroupedCardsWidget/index.js +28 -0
  154. package/src/components/CnIndexPage/CnIndexPage.vue +376 -11
  155. package/src/components/CnIndexPage/useNamedSource.js +4 -1
  156. package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
  157. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
  158. package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
  159. package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
  160. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +114 -7
  161. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
  162. package/src/components/CnSavedViewAlertWidget/index.js +7 -0
  163. package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +38 -9
  164. package/src/components/CnSetupWizard/CnSetupWizard.vue +72 -5
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
  168. package/src/components/index.js +3 -0
  169. package/src/composables/indexSources.js +166 -19
  170. package/src/composables/useEndpointSource.js +28 -6
  171. package/src/css/detail-page.css +15 -0
  172. package/src/css/dialog.css +48 -0
  173. package/src/css/index-page.css +117 -0
  174. package/src/css/look-board.css +127 -1
  175. package/src/index.js +1 -0
  176. package/src/schemas/app-manifest-v2.schema.json +55 -1
  177. package/src/utils/languageMapText.js +46 -0
  178. package/src/utils/libraryWidgetKeys.js +3 -0
  179. package/src/utils/mapList.js +67 -0
  180. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -11,6 +11,8 @@ import HandBackRight from 'vue-material-design-icons/HandBackRight.vue'
11
11
  import LightningBolt from 'vue-material-design-icons/LightningBolt.vue'
12
12
  import Pencil from 'vue-material-design-icons/Pencil.vue'
13
13
  import { formatRelativeDate } from '../utils/boardDate.js'
14
+ import { fetchReferenceObjects } from '../utils/boardReferenceColumns.js'
15
+ import { fillTemplate } from '../utils/fillTemplate.js'
14
16
  import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
15
17
  import { fetchGroupMembers, fetchPresence, fetchStatuses } from '../utils/nextcloudDirectory.js'
16
18
  import { readPath } from '../utils/readPath.js'
@@ -648,6 +650,10 @@ function readRowsPath(body, path) {
648
650
  * requested: page-config keys must never become query parameters. The list
649
651
  * stays empty and one console error names the page.
650
652
  *
653
+ * A quick filter with `showCount` is counted with `count(config)`: the same
654
+ * request with that tab's filter, its total read at `sourceConfig.totalPath`
655
+ * (`total` by default), else the number of rows the answer holds.
656
+ *
651
657
  * @return {object} The source adapter.
652
658
  * @spec openspec/changes/index-endpoint-source/specs/index-page/spec.md#requirement-an-index-page-can-list-rows-from-an-app-endpoint
653
659
  */
@@ -656,6 +662,36 @@ function endpointSource() {
656
662
  let seq = 0
657
663
  let warned = false
658
664
 
665
+ /**
666
+ * The query parameters for a config: every key but the url and the paths,
667
+ * empty values left out, a list joined by commas.
668
+ *
669
+ * @param {object} rest The config without `url`, `rowsPath` and `totalPath`.
670
+ * @return {object} The parameters.
671
+ */
672
+ const paramsOf = (rest) => {
673
+ const params = {}
674
+ for (const [key, value] of Object.entries(rest)) {
675
+ if (value === undefined || value === null || value === '') {
676
+ continue
677
+ }
678
+ params[key] = Array.isArray(value) ? value.join(',') : (typeof value === 'object' ? JSON.stringify(value) : value)
679
+ }
680
+ return params
681
+ }
682
+
683
+ /**
684
+ * Whether a config can be requested: it names a url and is not the page
685
+ * config handed over in its place.
686
+ *
687
+ * @param {object} config The config.
688
+ * @return {boolean}
689
+ */
690
+ const requestable = (config) => {
691
+ const url = config && config.url
692
+ return typeof url === 'string' && url !== '' && !Object.hasOwn(config, 'entitySource')
693
+ }
694
+
659
695
  return {
660
696
  /**
661
697
  * Fetch the rows.
@@ -664,10 +700,9 @@ function endpointSource() {
664
700
  * @return {Promise<void>} Resolves when loaded; a failed request leaves no rows and rethrows.
665
701
  */
666
702
  load: async (config) => {
667
- const { url, rowsPath, ...rest } = config || {}
703
+ const { url, rowsPath, totalPath, ...rest } = config || {}
668
704
  const mine = ++seq
669
- const pageConfig = Boolean(config && Object.hasOwn(config, 'entitySource'))
670
- if (typeof url !== 'string' || url === '' || pageConfig) {
705
+ if (!requestable(config)) {
671
706
  state.rows = []
672
707
  if (!warned) {
673
708
  warned = true
@@ -677,13 +712,7 @@ function endpointSource() {
677
712
  }
678
713
  return
679
714
  }
680
- const params = {}
681
- for (const [key, value] of Object.entries(rest)) {
682
- if (value === undefined || value === null || value === '') {
683
- continue
684
- }
685
- params[key] = Array.isArray(value) ? value.join(',') : (typeof value === 'object' ? JSON.stringify(value) : value)
686
- }
715
+ const params = paramsOf(rest)
687
716
  state.loading = true
688
717
  try {
689
718
  const { default: axios } = await import('@nextcloud/axios')
@@ -705,6 +734,29 @@ function endpointSource() {
705
734
  }
706
735
  },
707
736
 
737
+ /**
738
+ * Count the rows for a config without touching the rows on screen, for
739
+ * a quick-filter tab with `showCount`.
740
+ *
741
+ * @param {object} config The page's source config with the tab's filter merged in.
742
+ * @return {Promise<number>} The total, or NaN when nothing can be requested.
743
+ */
744
+ count: async (config) => {
745
+ if (!requestable(config)) {
746
+ return Number.NaN
747
+ }
748
+ const { url, rowsPath, totalPath, ...rest } = config
749
+ const { default: axios } = await import('@nextcloud/axios')
750
+ const res = await axios.get(generateUrl(url), { params: paramsOf(rest) })
751
+ const body = res ? res.data : null
752
+ const total = body && typeof body === 'object' && !Array.isArray(body) ? readRowsPath(body, totalPath || 'total') : undefined
753
+ if (typeof total === 'number' && Number.isFinite(total)) {
754
+ return total
755
+ }
756
+ const rows = Array.isArray(body) ? body : readRowsPath(body, rowsPath || 'results')
757
+ return Array.isArray(rows) ? rows.length : Number.NaN
758
+ },
759
+
708
760
  /**
709
761
  * @return {Array<object>} The rows the endpoint returned.
710
762
  */
@@ -737,39 +789,102 @@ function fillRowTemplate(template, row) {
737
789
  * and its count (DqZoekresultaten).
738
790
  *
739
791
  * `sourceConfig.kinds` lists what is searched:
740
- * `{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }`
792
+ * `{ id, label, register, schema, titleField?, subtitleField?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
741
793
  * for objects (OpenRegister's `_search`, so the reader sees what they may
742
794
  * read), or `{ id, label, files: true, limit? }` for the reader's files
743
795
  * (Nextcloud's unified search). The term is `sourceConfig.term`, else the
744
796
  * address's `?q=` (or `sourceConfig.termQuery`). A kind that fails shows no
745
797
  * rows and no count; the others still answer.
746
798
  *
799
+ * The kind's `label` goes through the app's translate (the "Soort" cell and
800
+ * the quick filters). `statusResolve: { register?, schema, labelField? }`
801
+ * reads `statusField` as a reference and shows the referenced object's label
802
+ * (`labelField`, else `title`, then `name`); the status column is a pill,
803
+ * coloured by `sourceConfig.statusColorMap`. `linkedTemplate` (a field
804
+ * template, "{case.title}") or `linkedField` (a path) fills the "Linked"
805
+ * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
806
+ * The page's title and count line can name the term as `{term}`, and the
807
+ * count is the active kind's total. Kind, link and status are filterable.
808
+ *
747
809
  * @param {object} [config] The page's source config.
810
+ * @param {object} [context] The page's helpers.
811
+ * @param {Function} [context.translate] The app's translate.
748
812
  * @return {object} The source adapter.
749
813
  * @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
750
814
  */
751
- function searchSource(config = {}) {
815
+ function searchSource(config = {}, context = {}) {
816
+ const translate = typeof context.translate === 'function' ? context.translate : (text) => text
752
817
  const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
753
818
  .filter((kind) => kind && typeof kind.id === 'string' && kind.id !== '' && (kind.files === true || (kind.register && kind.schema)))
754
- const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '' })
819
+ .map((kind) => ({ ...kind, label: translate(String(kind.label || kind.id)) }))
820
+ const hasLinked = kinds.some((kind) => kind.linkedTemplate || kind.linkedField)
821
+ const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '', filters: {} })
755
822
  let seq = 0
756
823
 
824
+ /**
825
+ * The labels of the objects a kind's status points at, by id: the
826
+ * referenced schema read once per search.
827
+ *
828
+ * @param {object} kind The kind with `statusResolve`.
829
+ * @return {Promise<Map<string, string>>} Labels by id; empty when it cannot be read.
830
+ */
831
+ const statusLabels = async (kind) => {
832
+ const resolve = kind.statusResolve
833
+ if (!resolve || !resolve.schema) {
834
+ return new Map()
835
+ }
836
+ const objects = await fetchReferenceObjects({ register: resolve.register || kind.register, schema: resolve.schema }).catch(() => [])
837
+ const labels = new Map()
838
+ for (const object of objects) {
839
+ const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
840
+ const label = readPath(object, resolve.labelField || 'title') ?? object.name
841
+ if (id !== undefined && label !== undefined && label !== null) {
842
+ labels.set(String(id), String(label))
843
+ }
844
+ }
845
+ return labels
846
+ }
847
+
848
+ /**
849
+ * What a row shows in the Linked column.
850
+ *
851
+ * @param {object} kind The row's kind.
852
+ * @param {object} object The found object.
853
+ * @return {string}
854
+ */
855
+ const linkedOf = (kind, object) => {
856
+ if (kind.linkedTemplate) {
857
+ return fillTemplate(translate(kind.linkedTemplate), object)
858
+ }
859
+ if (kind.linkedField) {
860
+ const value = readPath(object, kind.linkedField)
861
+ return value === undefined || value === null ? '' : String(value)
862
+ }
863
+ return ''
864
+ }
865
+
757
866
  const searchObjects = async (axios, kind, term) => {
758
867
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
759
868
  const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
760
869
  const body = (res && res.data) || {}
761
870
  const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
871
+ const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
762
872
  return {
763
873
  total: typeof body.total === 'number' ? body.total : results.length,
764
874
  rows: results.map((object) => {
765
875
  const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
876
+ const rawStatus = kind.statusField ? readPath(object, kind.statusField) : undefined
877
+ const status = rawStatus === undefined || rawStatus === null
878
+ ? ''
879
+ : (labels ? (labels.get(String(rawStatus)) || '') : String(rawStatus))
766
880
  return {
767
881
  id: `${kind.id}:${id}`,
768
882
  kind: kind.id,
769
883
  kindLabel: kind.label,
770
884
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
771
885
  subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
772
- status: kind.statusField ? String(readPath(object, kind.statusField) ?? '') : '',
886
+ status,
887
+ linked: linkedOf(kind, object),
773
888
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
774
889
  object,
775
890
  }
@@ -792,6 +907,7 @@ function searchSource(config = {}) {
792
907
  title: entry.title || '',
793
908
  subtitle: entry.subline || '',
794
909
  status: '',
910
+ linked: '',
795
911
  href: entry.resourceUrl || '',
796
912
  })),
797
913
  }
@@ -806,6 +922,17 @@ function searchSource(config = {}) {
806
922
  */
807
923
  load: async (cfg) => {
808
924
  state.kind = cfg && typeof cfg.kind === 'string' ? cfg.kind : 'all'
925
+ // The header filters on Kind, Linked and Status narrow the answer
926
+ // already fetched; they do not search again.
927
+ const filters = {}
928
+ for (const key of ['kindLabel', 'linked', 'status']) {
929
+ const value = cfg && cfg[key]
930
+ const values = (Array.isArray(value) ? value : (value === undefined || value === null || value === '' ? [] : [value])).map(String)
931
+ if (values.length > 0) {
932
+ filters[key] = values
933
+ }
934
+ }
935
+ state.filters = filters
809
936
  const query = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null
810
937
  const term = String((cfg && cfg.term) || (query && query.get((cfg && cfg.termQuery) || 'q')) || '').trim()
811
938
  const mine = ++seq
@@ -837,7 +964,20 @@ function searchSource(config = {}) {
837
964
  /**
838
965
  * @return {Array<object>} The results of the active kind, or all of them.
839
966
  */
840
- rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind)),
967
+ rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind))
968
+ .filter((row) => Object.entries(state.filters).every(([key, values]) => values.includes(String(row[key])))),
969
+
970
+ /**
971
+ * @return {string} The term searched for, for a `{term}` in the title or count line.
972
+ */
973
+ term: () => state.term,
974
+
975
+ /**
976
+ * @return {number} The active kind's total, or every kind's together on All.
977
+ */
978
+ total: () => (state.kind === 'all'
979
+ ? Object.values(state.totals).reduce((sum, value) => sum + value, 0)
980
+ : (state.totals[state.kind] || 0)),
841
981
 
842
982
  /**
843
983
  * @return {boolean} Whether a search is in flight.
@@ -846,8 +986,14 @@ function searchSource(config = {}) {
846
986
 
847
987
  columns: [
848
988
  { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle' },
849
- { key: 'kindLabel', label: t('nextcloud-vue', 'Kind') },
850
- { key: 'status', label: t('nextcloud-vue', 'Status') },
989
+ { key: 'kindLabel', label: t('nextcloud-vue', 'Kind'), enum: kinds.map((kind) => kind.label) },
990
+ ...(hasLinked ? [{ key: 'linked', label: config.linkedLabel ? translate(config.linkedLabel) : t('nextcloud-vue', 'Linked') }] : []),
991
+ {
992
+ key: 'status',
993
+ label: t('nextcloud-vue', 'Status'),
994
+ widget: 'badge',
995
+ widgetProps: { colorMap: (config && config.statusColorMap) || {} },
996
+ },
851
997
  ],
852
998
 
853
999
  quickFilters: kinds.length > 0
@@ -1125,10 +1271,11 @@ export const indexSources = {
1125
1271
  *
1126
1272
  * @param {string} name The source name from `config.source`.
1127
1273
  * @param {object} [config] The page's source config, for a source that builds its tabs from it.
1274
+ * @param {object} [context] The page's helpers a source may use, such as `translate` (the app's catalogue).
1128
1275
  *
1129
1276
  * @return {object|null} The adapter, or null when the name is not registered.
1130
1277
  */
1131
- export function resolveIndexSource(name, config) {
1278
+ export function resolveIndexSource(name, config, context) {
1132
1279
  const key = String(name || '')
1133
1280
  if (!key) {
1134
1281
  return null
@@ -1141,7 +1288,7 @@ export function resolveIndexSource(name, config) {
1141
1288
  return null
1142
1289
  }
1143
1290
 
1144
- return factory(config || {})
1291
+ return factory(config || {}, context || {})
1145
1292
  }
1146
1293
 
1147
1294
  /**
@@ -155,7 +155,7 @@ export function interpolateUrlTokens(str, ctx) {
155
155
  *
156
156
  * @param {{url: string, method?: string, params?: object}} config The endpointSource block.
157
157
  * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object, range?: object}} [ctx] The token context.
158
- * @return {{url: string, method: ('GET'|'POST'), params: object, blocked: boolean}} The resolved request.
158
+ * @return {{url: string, method: ('GET'|'POST'), params: object, blocked: boolean, responsePath: string}} The resolved request.
159
159
  */
160
160
  export function resolveEndpointRequest(config, ctx) {
161
161
  const cfg = config || {}
@@ -163,8 +163,30 @@ export function resolveEndpointRequest(config, ctx) {
163
163
  const resolved = resolveFilterTokens(cfg.params || {}, ctx)
164
164
  const params = dropOptionalUnresolved(resolved)
165
165
  const method = String(cfg.method || 'GET').toUpperCase() === 'POST' ? 'POST' : 'GET'
166
- const blocked = hasUnresolvedTokens(params) || hasEmptyPathSegment(cfg.url || '', url)
167
- return { url, method, params, blocked }
166
+ const responsePath = resolveResponsePath(cfg.responsePath, ctx)
167
+ const blocked = hasUnresolvedTokens(params) || hasEmptyPathSegment(cfg.url || '', url) || responsePath === null
168
+ return { url, method, params, blocked, responsePath: responsePath || '' }
169
+ }
170
+
171
+ /**
172
+ * Resolve the tokens in a `responsePath` (`phaseDays.@workspace.measure`), so a
173
+ * page control can choose which part of one response a widget reads. Null when
174
+ * a token resolved to nothing and left an empty path segment: the read waits
175
+ * instead of plucking the wrong part.
176
+ *
177
+ * @param {string|undefined} path The configured path.
178
+ * @param {object} [ctx] The token context.
179
+ * @return {string|null} The resolved path, '' for none, null when blocked.
180
+ */
181
+ function resolveResponsePath(path, ctx) {
182
+ if (typeof path !== 'string' || path === '') {
183
+ return ''
184
+ }
185
+ if (path.indexOf('@') === -1) {
186
+ return path
187
+ }
188
+ const resolved = interpolateUrlTokens(path, ctx)
189
+ return resolved.split('.').some((segment) => segment === '') ? null : resolved
168
190
  }
169
191
 
170
192
  /**
@@ -309,7 +331,7 @@ export async function fetchEndpointSource(config, ctx, opts) {
309
331
  return null
310
332
  }
311
333
  const body = await fetchSharedResponse(request, opts)
312
- const payload = getByPath(body, config.responsePath)
334
+ const payload = getByPath(body, request.responsePath)
313
335
  return payload === undefined ? null : payload
314
336
  }
315
337
 
@@ -378,7 +400,7 @@ export function useEndpointSource(source, options) {
378
400
  u: request.url,
379
401
  m: request.method,
380
402
  p: request.params,
381
- r: cfg.responsePath || '',
403
+ r: request.responsePath,
382
404
  b: request.blocked,
383
405
  })
384
406
  })
@@ -418,7 +440,7 @@ export function useEndpointSource(source, options) {
418
440
  if (seq !== fetchSeq) {
419
441
  return
420
442
  }
421
- const payload = getByPath(body, cfg.responsePath)
443
+ const payload = getByPath(body, request.responsePath)
422
444
  data.value = payload === undefined ? null : payload
423
445
  } catch (e) {
424
446
  if (seq !== fetchSeq) {
@@ -525,3 +525,18 @@
525
525
  align-items: center;
526
526
  gap: 4px;
527
527
  }
528
+
529
+ /* The tag after an admin-only entry in a board detail page's Actions menu
530
+ ("beheerder", DqZaakMeer): 12px muted text at the end of the row. The menu
531
+ opens outside the page, so the rule keys on the attribute CnDetailPage sets
532
+ only in the board look. */
533
+ .action-button[data-cn-admin-tag]::after,
534
+ .action-link[data-cn-admin-tag]::after {
535
+ content: attr(data-cn-admin-tag);
536
+ margin-inline-start: auto;
537
+ padding-inline: 12px 4px;
538
+ color: var(--color-text-maxcontrast);
539
+ font-size: 12px;
540
+ font-weight: 400;
541
+ white-space: nowrap;
542
+ }
@@ -173,6 +173,54 @@
173
173
  color: var(--color-main-text);
174
174
  }
175
175
 
176
+ /* A Nextcloud text field or text area in a board dialog is the board's field:
177
+ its label above the control at 14px/600, the control 40px with a 1px grey
178
+ border and radius 8 (PlanItemDialog, DqWerkvoorraad). NcInputField draws
179
+ its border as a 65%-transparent shadow, which on the white dialog read as
180
+ an empty band, and floats its label inside the border. A field with
181
+ `labelOutside` keeps the app's own label and takes the same control. */
182
+ .cn-dialog-board .dialog__content .input-field:has(.input-field__label),
183
+ .cn-dialog-board .dialog__content .textarea:has(.textarea__label) {
184
+ margin-block-start: 26px;
185
+ }
186
+
187
+ /* The doubled class outranks NcInputField's raised-label rule (focused or
188
+ filled), which would pin the label's top as well and squash it. */
189
+ .cn-dialog-board .dialog__content .input-field__input + .input-field__label.input-field__label,
190
+ .cn-dialog-board .dialog__content .textarea__input + .textarea__label.textarea__label {
191
+ inset-block-start: auto;
192
+ inset-block-end: calc(100% + 4px);
193
+ inset-inline: 0 auto;
194
+ max-width: 100%;
195
+ margin: 0;
196
+ padding: 0;
197
+ background: none;
198
+ color: var(--color-main-text);
199
+ font-size: var(--cn-field-label-size, 14px);
200
+ font-weight: 600;
201
+ line-height: 20px;
202
+ pointer-events: auto;
203
+ transition: none;
204
+ }
205
+
206
+ .cn-dialog-board .dialog__content .input-field__main-wrapper {
207
+ height: calc(var(--cn-field-height, 40px) + 2 * var(--border-width-input-focused, 2px));
208
+ }
209
+
210
+ .cn-dialog-board .dialog__content .input-field__input,
211
+ .cn-dialog-board .dialog__content .input-field__input:hover:not([disabled]),
212
+ .cn-dialog-board .dialog__content .textarea__input,
213
+ .cn-dialog-board .dialog__content .textarea__input:hover:not([disabled]) {
214
+ border-radius: 8px;
215
+ box-shadow: 0 0 0 1px var(--color-border-dark);
216
+ font-size: var(--cn-field-text-size, 15px);
217
+ }
218
+
219
+ .cn-dialog-board .dialog__content .input-field:has(.input-field__label) .input-field__input:not(:focus)::placeholder,
220
+ .cn-dialog-board .dialog__content .textarea__input:not(:focus)::placeholder {
221
+ opacity: 1 !important;
222
+ }
223
+
176
224
  /* Type-to-confirm: a labelled 40px field with a monospace value and a hint. */
177
225
  .cn-dialog__confirm-field {
178
226
  display: flex;
@@ -73,6 +73,123 @@
73
73
  min-height: 300px;
74
74
  }
75
75
 
76
+ /* The map with its numbered list (`mapConfig.list`, DqZakenKaart): one framed
77
+ card, the map taking the larger share and the list beside it, stacking
78
+ under a narrow width. */
79
+ .cn-index-page__map-card {
80
+ display: flex;
81
+ flex: 1;
82
+ flex-wrap: wrap;
83
+ gap: 16px;
84
+ min-height: 0;
85
+ padding: 16px;
86
+ border: 1px solid var(--color-border);
87
+ border-radius: var(--cn-board-card-radius, 12px);
88
+ background: var(--color-main-background);
89
+ }
90
+
91
+ .cn-index-page__map-card .cn-index-page__map {
92
+ flex: 3 1 420px;
93
+ min-width: 0;
94
+ }
95
+
96
+ .cn-index-page__map-side {
97
+ display: flex;
98
+ flex: 2 1 320px;
99
+ flex-direction: column;
100
+ gap: 12px;
101
+ min-width: 0;
102
+ overflow-y: auto;
103
+ }
104
+
105
+ .cn-index-page__map-list {
106
+ display: flex;
107
+ flex-direction: column;
108
+ margin: 0;
109
+ padding: 0;
110
+ border: 1px solid var(--color-border);
111
+ border-radius: 10px;
112
+ list-style: none;
113
+ }
114
+
115
+ .cn-index-page__map-item {
116
+ display: flex;
117
+ gap: 12px;
118
+ padding: 12px 14px;
119
+ }
120
+
121
+ .cn-index-page__map-item + .cn-index-page__map-item {
122
+ border-top: 1px solid var(--color-border);
123
+ }
124
+
125
+ .cn-index-page__map-item--active {
126
+ background: var(--color-primary-element-light);
127
+ }
128
+
129
+ .cn-index-page__map-number {
130
+ display: inline-flex;
131
+ flex: none;
132
+ align-items: center;
133
+ justify-content: center;
134
+ width: 26px;
135
+ height: 26px;
136
+ border-radius: 13px;
137
+ background: var(--color-primary-element);
138
+ color: var(--color-primary-element-text);
139
+ font-size: 13px;
140
+ font-weight: 700;
141
+ }
142
+
143
+ .cn-index-page__map-item--active .cn-index-page__map-number {
144
+ background: var(--color-primary-element-light-text);
145
+ }
146
+
147
+ .cn-index-page__map-text {
148
+ display: flex;
149
+ flex: 1;
150
+ flex-direction: column;
151
+ gap: 4px;
152
+ min-width: 0;
153
+ }
154
+
155
+ .cn-index-page__map-title {
156
+ margin: 0;
157
+ padding: 0;
158
+ border: 0;
159
+ background: none;
160
+ color: var(--color-main-text);
161
+ font: inherit;
162
+ font-size: 15px;
163
+ font-weight: 600;
164
+ line-height: 1.35;
165
+ text-align: start;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .cn-index-page__map-sub,
170
+ .cn-index-page__map-due {
171
+ color: var(--color-text-maxcontrast);
172
+ font-size: 13px;
173
+ }
174
+
175
+ .cn-index-page__map-pills {
176
+ display: flex;
177
+ flex-wrap: wrap;
178
+ align-items: center;
179
+ gap: 6px;
180
+ }
181
+
182
+ .cn-index-page__map-missing {
183
+ display: flex;
184
+ flex-direction: column;
185
+ align-items: flex-start;
186
+ gap: 8px;
187
+ padding: 14px;
188
+ border: 1px solid var(--color-border);
189
+ border-radius: 10px;
190
+ font-size: 14px;
191
+ }
192
+
76
193
  /* Opt-in folder navigation pane (left of the list). */
77
194
  .cn-index-page__folder-pane {
78
195
  flex: 0 0 240px;