@conduction/nextcloud-vue 2.97.0 → 2.99.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 (160) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +12 -0
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +11 -2
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +17 -0
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -1
  9. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +1 -1
  11. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
  19. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
  20. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
  21. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
  22. package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
  23. package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  36. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
  37. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  38. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  40. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
  41. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
  43. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +43 -3
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
  48. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
  49. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +239 -0
  50. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
  51. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +182 -0
  52. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
  53. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +9 -0
  54. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js.map +1 -0
  55. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +265 -0
  56. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -0
  57. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +191 -0
  58. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -0
  59. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +9 -0
  60. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js.map +1 -0
  61. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +508 -0
  62. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -0
  63. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js +99 -0
  64. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -0
  65. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  66. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +18 -70
  67. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +6 -44
  69. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  71. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
  72. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  73. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
  74. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  75. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
  76. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  77. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +9 -0
  78. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js.map +1 -0
  79. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js +472 -0
  80. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -0
  81. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js +231 -0
  82. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -0
  83. package/dist/esm/components/CnWorkingCalendarWidget/index.js +27 -0
  84. package/dist/esm/components/CnWorkingCalendarWidget/index.js.map +1 -0
  85. package/dist/esm/composables/indexSources.js +238 -2
  86. package/dist/esm/composables/indexSources.js.map +1 -1
  87. package/dist/esm/composables/useFlowStore.js +2 -1
  88. package/dist/esm/composables/useFlowStore.js.map +1 -1
  89. package/dist/esm/composables/useOrganisationStore.js +115 -0
  90. package/dist/esm/composables/useOrganisationStore.js.map +1 -0
  91. package/dist/esm/index.js +5 -0
  92. package/dist/esm/index.js.map +1 -1
  93. package/dist/esm/l10n/en.json.js +109 -1
  94. package/dist/esm/l10n/en.json.js.map +1 -1
  95. package/dist/esm/l10n/nl.json.js +138 -1
  96. package/dist/esm/l10n/nl.json.js.map +1 -1
  97. package/dist/esm/library-style.js +1 -1
  98. package/dist/esm/nextcloud-vue.css +1071 -274
  99. package/dist/esm/utils/builtInFormatters.js +34 -1
  100. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  101. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  102. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  103. package/dist/esm/utils/matrixCells.js +158 -0
  104. package/dist/esm/utils/matrixCells.js.map +1 -0
  105. package/dist/esm/utils/nextcloudDirectory.js +121 -0
  106. package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/esm/utils/workingCalendarMonth.js +85 -0
  110. package/dist/esm/utils/workingCalendarMonth.js.map +1 -0
  111. package/dist/nextcloud-vue.cjs.js +14888 -10014
  112. package/dist/nextcloud-vue.cjs.js.map +2 -2
  113. package/dist/nextcloud-vue.css +1071 -274
  114. package/l10n/en.json +109 -1
  115. package/l10n/nl.json +138 -1
  116. package/package.json +2 -2
  117. package/scripts/merge-l10n.js +196 -0
  118. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  119. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  120. package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
  121. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +1 -0
  122. package/src/components/CnChartWidget/CnChartWidget.md +2 -1
  123. package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
  124. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
  125. package/src/components/CnChecklistRunWidget/index.js +27 -0
  126. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  127. package/src/components/CnDataTable/CnDataTable.vue +26 -8
  128. package/src/components/CnDetailPage/CnDetailPage.md +48 -0
  129. package/src/components/CnDetailPage/CnDetailPage.vue +189 -5
  130. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
  131. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
  132. package/src/components/CnIndexPage/CnIndexPage.md +28 -0
  133. package/src/components/CnIndexPage/CnIndexPage.vue +43 -3
  134. package/src/components/CnOrderedListField/CnOrderedListField.vue +458 -0
  135. package/src/components/CnOrderedListField/index.js +7 -0
  136. package/src/components/CnSettingsField/CnSettingsField.vue +449 -0
  137. package/src/components/CnSettingsField/index.js +4 -0
  138. package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +613 -0
  139. package/src/components/CnSettingsFieldsSection/index.js +4 -0
  140. package/src/components/CnSettingsPage/CnSettingsPage.vue +6 -86
  141. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
  142. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
  143. package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +725 -0
  144. package/src/components/CnWorkingCalendarWidget/index.js +26 -0
  145. package/src/components/index.js +5 -0
  146. package/src/composables/indexSources.js +237 -1
  147. package/src/composables/useFlowStore.js +2 -1
  148. package/src/composables/useOrganisationStore.js +111 -0
  149. package/src/css/badge.css +32 -16
  150. package/src/css/look-board-detail.css +80 -8
  151. package/src/css/look-board-index.css +52 -8
  152. package/src/css/look-board.css +219 -1
  153. package/src/index.js +5 -0
  154. package/src/schemas/app-manifest-v2.schema.json +33 -3
  155. package/src/utils/builtInFormatters.js +33 -0
  156. package/src/utils/libraryWidgetKeys.js +2 -0
  157. package/src/utils/matrixCells.js +154 -0
  158. package/src/utils/nextcloudDirectory.js +117 -0
  159. package/src/utils/validateManifestV2.compiled.js +3 -3
  160. package/src/utils/workingCalendarMonth.js +82 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnWorkingCalendarWidget — self-registers the `working-calendar` widget type
6
+ * (one month of a working calendar kept as an OpenRegister object, with its
7
+ * closed dates and Add day) into the shared dashboardWidgetRegistry at module
8
+ * load. Renderer only: the calendar object is named in the manifest.
9
+ */
10
+
11
+ import CnWorkingCalendarWidget from './CnWorkingCalendarWidget.vue'
12
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
13
+
14
+ registerDashboardWidget('working-calendar', {
15
+ renderer: CnWorkingCalendarWidget,
16
+ form: null,
17
+ defaultContent: {
18
+ calendar: { register: 'flow-timers', schema: 'working-calendar', filter: {} },
19
+ },
20
+ displayName: 'Working calendar',
21
+ icon: 'CalendarMonthOutline',
22
+ surfaces: ['app-dashboard', 'detail-page'],
23
+ })
24
+
25
+ export { CnWorkingCalendarWidget }
26
+ export default CnWorkingCalendarWidget
@@ -13,6 +13,8 @@ export { CnStatusBadge } from './CnStatusBadge/index.js'
13
13
  export { CnPagination } from './CnPagination/index.js'
14
14
  export { CnSettingsCard } from './CnSettingsCard/index.js'
15
15
  export { CnSettingsSection } from './CnSettingsSection/index.js'
16
+ export { CnSettingsField } from './CnSettingsField/index.js'
17
+ export { CnSettingsFieldsSection } from './CnSettingsFieldsSection/index.js'
16
18
  export { CnLeafDependencySettings } from './CnLeafDependencySettings/index.js'
17
19
  export { CnStatsBlock } from './CnStatsBlock/index.js'
18
20
  export { CnStructuredDocReview } from './CnStructuredDocReview/index.js'
@@ -27,6 +29,7 @@ export { CnCardGrid } from './CnCardGrid/index.js'
27
29
  export { CnDateAxisView } from './CnDateAxisView/index.js'
28
30
  export { CnObjectRow } from './CnObjectRow/index.js'
29
31
  export { CnObjectList } from './CnObjectList/index.js'
32
+ export { CnOrderedListField } from './CnOrderedListField/index.js'
30
33
  export { CnObjectKanban } from './CnObjectKanban/index.js'
31
34
  export { CnObjectCalendar } from './CnObjectCalendar/index.js'
32
35
  export { CnFolderTree } from './CnFolderTree/index.js'
@@ -221,6 +224,8 @@ export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
221
224
  export { CnSectionsWidget } from './CnSectionsWidget/index.js'
222
225
  export { CnFactsWidget } from './CnFactsWidget/index.js'
223
226
  export { CnDeviceFramesWidget } from './CnDeviceFramesWidget/index.js'
227
+ export { CnChecklistRunWidget } from './CnChecklistRunWidget/index.js'
228
+ export { CnWorkingCalendarWidget } from './CnWorkingCalendarWidget/index.js'
224
229
  export { CnPartyActionsWidget } from './CnPartyActionsWidget/index.js'
225
230
  export { CnFieldListWidget } from './CnFieldListWidget/index.js'
226
231
  export { CnSettingsFormWidget } from './CnSettingsFormWidget/index.js'
@@ -3,7 +3,7 @@
3
3
  * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
4
4
  */
5
5
 
6
- import { translate as t } from '@nextcloud/l10n'
6
+ import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
7
7
  import { generateOcsUrl, generateUrl } from '@nextcloud/router'
8
8
  import { reactive } from 'vue'
9
9
  import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
@@ -12,8 +12,10 @@ 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
14
  import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
15
+ import { fetchGroupMembers, fetchPresence, fetchStatuses } from '../utils/nextcloudDirectory.js'
15
16
  import { readPath } from '../utils/readPath.js'
16
17
  import { useFlowStore } from './useFlowStore.js'
18
+ import { organisationRecord, useOrganisationStore } from './useOrganisationStore.js'
17
19
  import { useTaskInboxStore } from './useTaskInboxStore.js'
18
20
 
19
21
  /**
@@ -891,6 +893,210 @@ function searchSource(config = {}) {
891
893
  }
892
894
  }
893
895
 
896
+ /**
897
+ * The users source: the members of a Nextcloud group, with their function,
898
+ * roles, e-mail and whether they are in today (`entitySource: "users"`,
899
+ * DqAfdelingen). Pair it with `folderSidebar: { source: 'groups',
900
+ * filterField: 'group' }`, which lists the groups as the folder tree; the
901
+ * selected group arrives as `group`. `sourceConfig.group` is the group shown
902
+ * before one is picked.
903
+ *
904
+ * Today is the dav app's out-of-office period when one runs now ("Absent
905
+ * until 14 Oct"), else the user status (present, away) and its message. The
906
+ * function is the profile `headline`, the roles the profile `role`, unless
907
+ * `sourceConfig.functionField` / `rolesField` name other user fields.
908
+ * Listing a group needs an admin or the group's sub-admin; a refused request
909
+ * leaves the list empty.
910
+ *
911
+ * @return {object} The source adapter.
912
+ * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
913
+ */
914
+ function usersSource() {
915
+ const state = reactive({ rows: [], loading: false })
916
+ let seq = 0
917
+ const presenceLabels = {
918
+ present: t('nextcloud-vue', 'Present'),
919
+ away: t('nextcloud-vue', 'Away'),
920
+ }
921
+ const presenceColors = {
922
+ [presenceLabels.present]: 'success',
923
+ [presenceLabels.away]: 'info',
924
+ }
925
+
926
+ return {
927
+ /**
928
+ * Load the group's members and their presence.
929
+ *
930
+ * @param {object} config The page's source config with the picked `group`.
931
+ * @return {Promise<void>} Resolves when loaded.
932
+ */
933
+ load: async (config) => {
934
+ const group = String((config && (config.group || config.defaultGroup)) || '')
935
+ const mine = ++seq
936
+ if (group === '') {
937
+ state.rows = []
938
+ return
939
+ }
940
+ state.loading = true
941
+ try {
942
+ const { default: axios } = await import('@nextcloud/axios')
943
+ const [members, statuses] = await Promise.all([fetchGroupMembers(axios, group), fetchStatuses(axios)])
944
+ const functionField = (config && config.functionField) || 'headline'
945
+ const rolesField = (config && config.rolesField) || 'role'
946
+ const rows = await Promise.all(members.map(async (user) => {
947
+ const presence = await fetchPresence(axios, user.id, statuses[user.id] || null)
948
+ let today = ''
949
+ if (presence.state === 'absent') {
950
+ const until = presence.until ? new Date(`${presence.until}T12:00:00`) : null
951
+ today = until
952
+ ? t('nextcloud-vue', 'Absent until {date}', { date: until.toLocaleDateString(getCanonicalLocale() || undefined, { day: 'numeric', month: 'short' }) })
953
+ : t('nextcloud-vue', 'Absent')
954
+ } else if (presence.message) {
955
+ today = presence.message
956
+ } else if (presenceLabels[presence.state]) {
957
+ today = presenceLabels[presence.state]
958
+ }
959
+ return {
960
+ id: user.id,
961
+ displayname: user.displayname || user['display-name'] || user.id,
962
+ function: String(user[functionField] || ''),
963
+ roles: String(user[rolesField] || ''),
964
+ email: user.email || '',
965
+ today,
966
+ presence: presence.state,
967
+ }
968
+ }))
969
+ if (mine === seq) {
970
+ state.rows = rows.sort((a, b) => a.displayname.localeCompare(b.displayname))
971
+ }
972
+ } catch (error) {
973
+ if (mine === seq) {
974
+ state.rows = []
975
+ }
976
+ throw error
977
+ } finally {
978
+ if (mine === seq) {
979
+ state.loading = false
980
+ }
981
+ }
982
+ },
983
+
984
+ /**
985
+ * @return {Array<object>} The members.
986
+ */
987
+ rows: () => state.rows,
988
+
989
+ /**
990
+ * @return {boolean} Whether a load is in flight.
991
+ */
992
+ loading: () => state.loading,
993
+
994
+ columns: [
995
+ { key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function' },
996
+ { key: 'roles', label: t('nextcloud-vue', 'Roles') },
997
+ { key: 'email', label: t('nextcloud-vue', 'E-mail') },
998
+ { key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' } },
999
+ ],
1000
+
1001
+ searchFields: {
1002
+ group: { param: 'group', single: true },
1003
+ },
1004
+
1005
+ showAdd: false,
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * The organisations source: OpenRegister organisations, which are not
1011
+ * register/schema objects.
1012
+ *
1013
+ * The list is the viewer's own organisations and the single read refuses an
1014
+ * organisation the viewer has no access to; both are decided by OpenRegister
1015
+ * from the session. The adapter forwards nothing that could widen that.
1016
+ *
1017
+ * It is a RECORD source as well (`loadRecord` + `record`), so a
1018
+ * `type: "detail"` page with the same `entitySource` shows one organisation
1019
+ * with the header, pills, facts and `@objectId` / `@object.*` tokens an object
1020
+ * page has.
1021
+ *
1022
+ * @return {object} The source adapter.
1023
+ * @spec openspec/changes/organisations-entity-source/specs/organisations-entity-source/spec.md
1024
+ */
1025
+ function organisationsSource() {
1026
+ const store = useOrganisationStore()
1027
+
1028
+ const statusLabels = {
1029
+ provisioning: t('nextcloud-vue', 'Provisioning'),
1030
+ active: t('nextcloud-vue', 'Active'),
1031
+ suspended: t('nextcloud-vue', 'Suspended'),
1032
+ deprovisioning: t('nextcloud-vue', 'Deprovisioning'),
1033
+ archived: t('nextcloud-vue', 'Archived'),
1034
+ retained: t('nextcloud-vue', 'Retained'),
1035
+ }
1036
+
1037
+ return {
1038
+ store,
1039
+
1040
+ /**
1041
+ * Load the viewer's organisations. The endpoint takes no parameters.
1042
+ *
1043
+ * @return {Promise<void>} Resolves when loaded.
1044
+ */
1045
+ load: () => store.load(),
1046
+
1047
+ /**
1048
+ * @return {Array<object>} The rows, keyed on the organisation uuid.
1049
+ */
1050
+ rows: () => (store.organisations || []).map(organisationRecord),
1051
+
1052
+ /**
1053
+ * @return {boolean} Whether a load is in flight.
1054
+ */
1055
+ loading: () => !!store.loading,
1056
+
1057
+ /**
1058
+ * Read one organisation for a detail page.
1059
+ *
1060
+ * @param {string} id The organisation uuid.
1061
+ * @return {Promise<object|null>} The organisation, or null when it could not be read.
1062
+ */
1063
+ loadRecord: (id) => store.fetchRecord(id),
1064
+
1065
+ /**
1066
+ * @param {string} id The organisation uuid.
1067
+ * @return {object|null} The organisation read for this id, keyed on its uuid, or null.
1068
+ */
1069
+ record: (id) => {
1070
+ const organisation = store.records?.[String(id)]
1071
+ return organisation ? organisationRecord(organisation) : null
1072
+ },
1073
+
1074
+ /**
1075
+ * @param {string} id The organisation uuid.
1076
+ * @return {number|null} The HTTP status of a failed read of this id, or null.
1077
+ */
1078
+ recordError: (id) => store.recordErrors?.[String(id)] ?? null,
1079
+
1080
+ columns: [
1081
+ { key: 'name', label: t('nextcloud-vue', 'Name') },
1082
+ {
1083
+ key: 'status',
1084
+ label: t('nextcloud-vue', 'Status'),
1085
+ enum: Object.keys(statusLabels),
1086
+ enumLabels: statusLabels,
1087
+ colorMap: { active: 'success', provisioning: 'info', suspended: 'warning', deprovisioning: 'error', archived: 'default', retained: 'default' },
1088
+ },
1089
+ { key: 'type', label: t('nextcloud-vue', 'Type') },
1090
+ { key: 'created', label: t('nextcloud-vue', 'Created'), type: 'string', format: 'date-time' },
1091
+ ],
1092
+
1093
+ // Creating an organisation is not the index's object form, and
1094
+ // OpenRegister has no delete for one, so neither action is offered.
1095
+ showAdd: false,
1096
+ title: t('nextcloud-vue', 'Organisations'),
1097
+ }
1098
+ }
1099
+
894
1100
  /**
895
1101
  * The registered sources, by the name a manifest uses.
896
1102
  *
@@ -904,6 +1110,8 @@ export const indexSources = {
904
1110
  tasks: tasksSource,
905
1111
  endpoint: endpointSource,
906
1112
  search: searchSource,
1113
+ users: usersSource,
1114
+ organisations: organisationsSource,
907
1115
  }
908
1116
 
909
1117
  /**
@@ -935,3 +1143,31 @@ export function resolveIndexSource(name, config) {
935
1143
 
936
1144
  return factory(config || {})
937
1145
  }
1146
+
1147
+ /**
1148
+ * Resolve a named source for ONE record, for a `type: "detail"` page.
1149
+ *
1150
+ * The same registry as the index: a source that can also read a single record
1151
+ * supplies `loadRecord(id)` and `record(id)`, and the detail page names it with
1152
+ * the same `entitySource`. A name that is unknown, or a source that cannot read
1153
+ * one record, resolves to null with a console warning, and the page shows no
1154
+ * record rather than the wrong one.
1155
+ *
1156
+ * @param {string} name The source name from `config.entitySource`.
1157
+ * @return {object|null} The adapter, or null.
1158
+ * @spec openspec/changes/organisations-entity-source/specs/organisations-entity-source/spec.md
1159
+ */
1160
+ export function resolveRecordSource(name) {
1161
+ const key = String(name || '')
1162
+ if (!key) {
1163
+ return null
1164
+ }
1165
+ const factory = indexSources[key]
1166
+ const source = typeof factory === 'function' ? factory() : null
1167
+ if (!source || typeof source.loadRecord !== 'function' || typeof source.record !== 'function') {
1168
+ // eslint-disable-next-line no-console
1169
+ console.warn(`[CnDetailPage] entitySource "${key}" is not a source that reads one record. The page shows no record.`)
1170
+ return null
1171
+ }
1172
+ return source
1173
+ }
@@ -23,6 +23,7 @@
23
23
  // vocabulary.
24
24
 
25
25
  import axios from '@nextcloud/axios'
26
+ import { translate as t } from '@nextcloud/l10n'
26
27
  import { generateUrl } from '@nextcloud/router'
27
28
  import { defineStore } from 'pinia'
28
29
  import { layoutFlowNodes, needsFullLayout, placeLooseNodes } from './flowGraphLayout.js'
@@ -968,7 +969,7 @@ export const useFlowStore = defineStore('cnFlow', {
968
969
  // A new flow is runnable on demand until its author picks a real
969
970
  // trigger, so the flow-level trigger and the seeded start node
970
971
  // say the same thing.
971
- this.flow = { ...emptyFlow(), name: 'New flow', app: app || 'openregister', trigger: 'manual' }
972
+ this.flow = { ...emptyFlow(), name: t('nextcloud-vue', 'New flow'), app: app || 'openregister', trigger: 'manual' }
972
973
  this.seedStartNode()
973
974
  this.dirty = false
974
975
  return
@@ -0,0 +1,111 @@
1
+ /**
2
+ * SPDX-License-Identifier: EUPL-1.2
3
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
4
+ */
5
+
6
+ import axios from '@nextcloud/axios'
7
+ import { generateUrl } from '@nextcloud/router'
8
+ import { defineStore } from 'pinia'
9
+
10
+ /**
11
+ * OpenRegister's organisation endpoints, behind the `organisations` entity source.
12
+ *
13
+ * An organisation is not a register/schema object, so the object store cannot
14
+ * read it. Both reads answer from the session: the list holds only the
15
+ * organisations the viewer belongs to, and one organisation answers 403 when
16
+ * the viewer has no access to it. This store adds no check of its own and
17
+ * skips none.
18
+ *
19
+ * @type {string}
20
+ */
21
+ export const ORGANISATIONS_URL = '/apps/openregister/api/organisations'
22
+
23
+ /**
24
+ * One organisation as a row or a record: the organisation as OpenRegister
25
+ * serialises it, with `id` set to its uuid. An OpenRegister object's `id` is
26
+ * its uuid as well, so `@object.id`, a row route and the page's `objectId`
27
+ * all name the same thing. The numeric database id stays out of sight.
28
+ *
29
+ * @param {object} organisation The organisation from the endpoint.
30
+ * @return {object} The organisation keyed on its uuid.
31
+ */
32
+ export function organisationRecord(organisation) {
33
+ return { ...organisation, id: organisation?.uuid ?? organisation?.id ?? null }
34
+ }
35
+
36
+ /**
37
+ * The organisations store.
38
+ *
39
+ * @spec openspec/changes/organisations-entity-source/specs/organisations-entity-source/spec.md
40
+ */
41
+ export const useOrganisationStore = defineStore('cnOrganisations', {
42
+ state: () => ({
43
+ /** @type {Array<object>} The viewer's organisations, as the list endpoint returned them. */
44
+ organisations: [],
45
+ /** @type {boolean} Whether the list load is in flight. */
46
+ loading: false,
47
+ /** @type {string|null} The last list failure, for the console trail. */
48
+ error: null,
49
+ /** @type {Record<string, object>} Single organisations read so far, by uuid. */
50
+ records: {},
51
+ /** @type {Record<string, number|null>} The HTTP status of a failed single read, by uuid. */
52
+ recordErrors: {},
53
+ }),
54
+
55
+ actions: {
56
+ /**
57
+ * Load the viewer's organisations. The endpoint takes no parameters and
58
+ * returns every organisation the viewer belongs to in one response.
59
+ *
60
+ * @return {Promise<void>} Resolves when the rows are in the store.
61
+ */
62
+ async load() {
63
+ this.loading = true
64
+ this.error = null
65
+ try {
66
+ const response = await axios.get(generateUrl(ORGANISATIONS_URL))
67
+ this.organisations = Array.isArray(response.data?.results) ? response.data.results : []
68
+ } catch (error) {
69
+ this.error = error?.message || String(error)
70
+ this.organisations = []
71
+ // eslint-disable-next-line no-console
72
+ console.error('[useOrganisationStore] Loading the organisations failed', error)
73
+ } finally {
74
+ this.loading = false
75
+ }
76
+ },
77
+
78
+ /**
79
+ * Read one organisation into `records`. A refusal (403) or a missing
80
+ * organisation (404) leaves no record and keeps the status in
81
+ * `recordErrors`, so the page can tell "not found" from "still loading".
82
+ *
83
+ * @param {string} uuid The organisation uuid.
84
+ * @return {Promise<object|null>} The organisation, or null when it could not be read.
85
+ */
86
+ async fetchRecord(uuid) {
87
+ const key = String(uuid || '')
88
+ if (key === '') {
89
+ return null
90
+ }
91
+ try {
92
+ const response = await axios.get(generateUrl(`${ORGANISATIONS_URL}/${encodeURIComponent(key)}`))
93
+ const organisation = response.data?.organisation || null
94
+ const { [key]: dropped, ...errors } = this.recordErrors
95
+ this.recordErrors = errors
96
+ if (!organisation) {
97
+ return null
98
+ }
99
+ this.records = { ...this.records, [key]: organisation }
100
+ return organisation
101
+ } catch (error) {
102
+ const { [key]: dropped, ...records } = this.records
103
+ this.records = records
104
+ this.recordErrors = { ...this.recordErrors, [key]: error?.response?.status ?? null }
105
+ // eslint-disable-next-line no-console
106
+ console.error(`[useOrganisationStore] Loading organisation "${key}" failed`, error)
107
+ return null
108
+ }
109
+ },
110
+ },
111
+ })
package/src/css/badge.css CHANGED
@@ -31,84 +31,100 @@
31
31
 
32
32
  /* Color variants */
33
33
  .cn-status-badge--default {
34
+ --cn-status-badge-ink: var(--color-main-text);
34
35
  background-color: var(--color-background-dark);
35
- color: var(--color-main-text);
36
+ color: var(--cn-status-badge-ink);
36
37
  }
37
38
 
38
39
  .cn-status-badge--primary {
40
+ --cn-status-badge-ink: var(--color-primary-element);
39
41
  background-color: var(--color-primary-element-light);
40
- color: var(--color-primary-element);
42
+ color: var(--cn-status-badge-ink);
41
43
  }
42
44
 
43
45
  .cn-status-badge--success {
46
+ --cn-status-badge-ink: var(--color-success-text, var(--color-success));
44
47
  background-color: var(--color-success-light, rgba(70, 186, 97, 0.15));
45
- color: var(--color-success-text, var(--color-success));
48
+ color: var(--cn-status-badge-ink);
46
49
  }
47
50
 
48
51
  .cn-status-badge--warning {
52
+ --cn-status-badge-ink: var(--color-warning-text, var(--color-warning));
49
53
  background-color: var(--color-warning-light, rgba(232, 163, 39, 0.15));
50
- color: var(--color-warning-text, var(--color-warning));
54
+ color: var(--cn-status-badge-ink);
51
55
  }
52
56
 
53
57
  .cn-status-badge--error {
58
+ --cn-status-badge-ink: var(--color-error-text, var(--color-error));
54
59
  background-color: var(--color-error-light, rgba(224, 68, 68, 0.15));
55
- color: var(--color-error-text, var(--color-error));
60
+ color: var(--cn-status-badge-ink);
56
61
  }
57
62
 
58
63
  .cn-status-badge--info {
64
+ --cn-status-badge-ink: var(--color-info-text, var(--color-info, #0082c9));
59
65
  background-color: var(--color-info-light, rgba(0, 130, 201, 0.15));
60
- color: var(--color-info-text, var(--color-info, #0082c9));
66
+ color: var(--cn-status-badge-ink);
61
67
  }
62
68
 
63
69
  /* Work in progress ("In behandeling"), a board tone. */
64
70
  .cn-status-badge--purple {
71
+ --cn-status-badge-ink: var(--cn-status-purple-text);
65
72
  background-color: var(--cn-status-purple-bg);
66
- color: var(--cn-status-purple-text);
73
+ color: var(--cn-status-badge-ink);
67
74
  }
68
75
 
69
76
  /* Handed on ("Omgezet naar een zaak"), a board tone. */
70
77
  .cn-status-badge--teal {
78
+ --cn-status-badge-ink: var(--cn-status-teal-text);
71
79
  background-color: var(--cn-status-teal-bg);
72
- color: var(--cn-status-teal-text);
80
+ color: var(--cn-status-badge-ink);
73
81
  }
74
82
 
75
83
  /* Solid variants — solid background with white text for use on colored backgrounds */
76
84
  .cn-status-badge--solid.cn-status-badge--default {
85
+ --cn-status-badge-ink: var(--color-main-text);
77
86
  background-color: var(--color-background-dark);
78
- color: var(--color-main-text);
87
+ color: var(--cn-status-badge-ink);
79
88
  }
80
89
 
81
90
  .cn-status-badge--solid.cn-status-badge--primary {
91
+ --cn-status-badge-ink: white;
82
92
  background-color: var(--color-primary-element);
83
- color: white;
93
+ color: var(--cn-status-badge-ink);
84
94
  }
85
95
 
86
96
  .cn-status-badge--solid.cn-status-badge--success {
97
+ --cn-status-badge-ink: white;
87
98
  background-color: var(--color-success);
88
- color: white;
99
+ color: var(--cn-status-badge-ink);
89
100
  }
90
101
 
91
102
  .cn-status-badge--solid.cn-status-badge--warning {
103
+ --cn-status-badge-ink: var(--color-primary-text);
92
104
  background-color: var(--color-warning);
93
- color: var(--color-primary-text);
105
+ color: var(--cn-status-badge-ink);
94
106
  }
95
107
 
96
108
  .cn-status-badge--solid.cn-status-badge--error {
109
+ --cn-status-badge-ink: white;
97
110
  background-color: var(--color-error);
98
- color: white;
111
+ color: var(--cn-status-badge-ink);
99
112
  }
100
113
 
101
114
  .cn-status-badge--solid.cn-status-badge--info {
115
+ --cn-status-badge-ink: white;
102
116
  background-color: var(--color-info, #0082c9);
103
- color: white;
117
+ color: var(--cn-status-badge-ink);
104
118
  }
105
119
 
106
120
  .cn-status-badge--solid.cn-status-badge--purple {
121
+ --cn-status-badge-ink: white;
107
122
  background-color: var(--cn-status-purple-text);
108
- color: white;
123
+ color: var(--cn-status-badge-ink);
109
124
  }
110
125
 
111
126
  .cn-status-badge--solid.cn-status-badge--teal {
127
+ --cn-status-badge-ink: white;
112
128
  background-color: var(--cn-status-teal-text);
113
- color: white;
129
+ color: var(--cn-status-badge-ink);
114
130
  }