@conduction/nextcloud-vue 2.62.0 → 2.63.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 (154) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +271 -32
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  21. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  22. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  23. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  24. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  25. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +216 -8
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  34. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  36. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  38. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  46. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  50. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  52. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  53. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  54. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  56. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  57. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  58. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  60. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  65. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  66. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  67. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  68. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  69. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  70. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  71. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  72. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  73. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  74. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  79. package/dist/esm/composables/useDependencyCheck.js +65 -0
  80. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  81. package/dist/esm/composables/useWalkthrough.js +218 -6
  82. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  83. package/dist/esm/index.js +2 -0
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +43 -0
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +43 -0
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/nextcloud-vue.css +449 -0
  90. package/dist/esm/store/useObjectStore.js +8 -0
  91. package/dist/esm/store/useObjectStore.js.map +1 -1
  92. package/dist/esm/utils/columnFilters.js +311 -0
  93. package/dist/esm/utils/columnFilters.js.map +1 -0
  94. package/dist/esm/utils/groupAutocomplete.js +123 -0
  95. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  96. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  97. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  98. package/dist/esm/utils/pickerOptions.js +198 -0
  99. package/dist/esm/utils/pickerOptions.js.map +1 -0
  100. package/dist/esm/utils/schema.js +83 -7
  101. package/dist/esm/utils/schema.js.map +1 -1
  102. package/dist/esm/utils/timelineEvents.js +174 -0
  103. package/dist/esm/utils/timelineEvents.js.map +1 -0
  104. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  105. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  106. package/dist/esm/utils/walkthroughSignals.js +88 -0
  107. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  108. package/dist/nextcloud-vue.cjs.js +9369 -5193
  109. package/dist/nextcloud-vue.cjs.js.map +2 -2
  110. package/dist/nextcloud-vue.css +449 -0
  111. package/l10n/en.json +43 -0
  112. package/l10n/nl.json +43 -0
  113. package/package.json +1 -1
  114. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  115. package/src/components/CnAdminActionCard/index.js +4 -0
  116. package/src/components/CnAppNav/CnAppNav.vue +271 -32
  117. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  118. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  119. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
  120. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  121. package/src/components/CnDataTable/CnDataTable.md +3 -0
  122. package/src/components/CnDataTable/CnDataTable.vue +260 -4
  123. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  124. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  125. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  126. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  127. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  128. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  129. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  130. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  131. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  132. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  133. package/src/components/CnTimelineWidget/index.js +4 -0
  134. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  135. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  136. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  137. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  138. package/src/components/index.js +2 -0
  139. package/src/composables/useDependencyCheck.js +61 -0
  140. package/src/composables/useWalkthrough.js +217 -5
  141. package/src/css/dashboard.css +49 -0
  142. package/src/css/detail-page.css +19 -0
  143. package/src/css/table.css +81 -0
  144. package/src/index.js +2 -0
  145. package/src/schemas/app-manifest-v2.schema.json +23 -1
  146. package/src/store/useObjectStore.js +8 -0
  147. package/src/utils/columnFilters.js +324 -0
  148. package/src/utils/groupAutocomplete.js +119 -0
  149. package/src/utils/libraryWidgetKeys.js +2 -0
  150. package/src/utils/pickerOptions.js +194 -0
  151. package/src/utils/schema.js +83 -7
  152. package/src/utils/timelineEvents.js +171 -0
  153. package/src/utils/validateManifestV2.compiled.js +2 -2
  154. package/src/utils/walkthroughSignals.js +94 -0
@@ -0,0 +1,198 @@
1
+ import { getLanguage } from '@nextcloud/l10n';
2
+
3
+ /**
4
+ * Option lists for the language and time zone pickers, and the defaults a
5
+ * schema can ask for with `x-default`.
6
+ *
7
+ * Both pickers store a code (`nl`, `Europe/Amsterdam`) and show a label in
8
+ * the user's own language, so a person picks "Nederlands" and the record
9
+ * stores `nl`.
10
+ *
11
+ * @module utils/pickerOptions
12
+ */
13
+
14
+
15
+ /**
16
+ * ISO 639-1 language codes. `Intl` has no list of languages, so the codes
17
+ * live here; the labels come from `Intl.DisplayNames` at runtime.
18
+ *
19
+ * @type {string[]}
20
+ */
21
+ const LANGUAGE_CODES = (''
22
+ + 'aa ab af ak am an ar as av ay az ba be bg bi bm bn bo br bs '
23
+ + 'ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es et eu '
24
+ + 'fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu '
25
+ + 'hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl '
26
+ + 'km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh '
27
+ + 'mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc '
28
+ + 'oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg '
29
+ + 'si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl '
30
+ + 'tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za '
31
+ + 'zh zu'
32
+ ).split(' ');
33
+
34
+ /**
35
+ * A small fallback for runtimes without `Intl.supportedValuesOf`.
36
+ *
37
+ * @type {string[]}
38
+ */
39
+ const FALLBACK_TIMEZONES = 'UTC Europe/Amsterdam Europe/Berlin Europe/Brussels Europe/London Europe/Paris America/New_York America/Chicago America/Los_Angeles Asia/Tokyo Australia/Sydney'.split(' ');
40
+
41
+ /**
42
+ * The user's Nextcloud language as a BCP 47 tag (`en_GB` becomes `en-GB`).
43
+ *
44
+ * @return {string} The tag, `en` when Nextcloud reports nothing.
45
+ */
46
+ function currentLanguageTag() {
47
+ let lang;
48
+ try {
49
+ lang = getLanguage();
50
+ } catch {
51
+ lang = '';
52
+ }
53
+ return String(lang || 'en').replace(/_/g, '-')
54
+ }
55
+
56
+ /**
57
+ * The browser's time zone (IANA id).
58
+ *
59
+ * @return {string} The time zone, `UTC` when the runtime does not say.
60
+ */
61
+ function currentTimezone() {
62
+ try {
63
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
64
+ } catch {
65
+ return 'UTC'
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Cached option lists, keyed by display language. Stable identity matters:
71
+ * the form's select matches the chosen option by reference.
72
+ *
73
+ * @type {Map<string, {codes: string[], labels: object}>}
74
+ */
75
+ const languageCache = new Map();
76
+
77
+ /**
78
+ * The language picker's codes and labels, labelled in `displayLanguage`.
79
+ *
80
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
81
+ * @param {string} [displayLanguage] BCP 47 tag the labels are written in; defaults to the user's language.
82
+ * @return {{codes: string[], labels: object}} Codes sorted by label, and `{ code: label }`.
83
+ */
84
+ function languageOptions(displayLanguage) {
85
+ const locale = displayLanguage || currentLanguageTag();
86
+ if (languageCache.has(locale)) {
87
+ return languageCache.get(locale)
88
+ }
89
+ let names;
90
+ try {
91
+ names = new Intl.DisplayNames([locale, 'en'], { type: 'language' });
92
+ } catch {
93
+ names = null;
94
+ }
95
+ const labels = {};
96
+ for (const code of LANGUAGE_CODES) {
97
+ let label;
98
+ try {
99
+ label = (names && names.of(code)) || code;
100
+ } catch {
101
+ label = code;
102
+ }
103
+ // `Intl` lower-cases some names ("nederlands" is fine in a sentence,
104
+ // not in a list); capitalise the first letter for the list.
105
+ labels[code] = label.charAt(0).toLocaleUpperCase(locale) + label.slice(1);
106
+ }
107
+ const codes = [...LANGUAGE_CODES].sort((a, b) => labels[a].localeCompare(labels[b], locale));
108
+ const result = { codes, labels };
109
+ languageCache.set(locale, result);
110
+ return result
111
+ }
112
+
113
+ /** @type {{codes: string[], labels: object}|null} */
114
+ let timezoneCache = null;
115
+
116
+ /**
117
+ * The time zone picker's codes and labels (`Europe/Amsterdam` shows as
118
+ * `Europe/Amsterdam`, underscores as spaces).
119
+ *
120
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
121
+ * @return {{codes: string[], labels: object}} IANA ids, and `{ id: label }`.
122
+ */
123
+ function timezoneOptions() {
124
+ if (timezoneCache) {
125
+ return timezoneCache
126
+ }
127
+ let codes;
128
+ try {
129
+ codes = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : [];
130
+ } catch {
131
+ codes = [];
132
+ }
133
+ if (!codes.length) {
134
+ codes = FALLBACK_TIMEZONES;
135
+ }
136
+ if (!codes.includes('UTC')) {
137
+ codes = ['UTC', ...codes];
138
+ }
139
+ const labels = {};
140
+ for (const code of codes) {
141
+ labels[code] = code.replace(/_/g, ' ');
142
+ }
143
+ timezoneCache = { codes: [...codes], labels };
144
+ return timezoneCache
145
+ }
146
+
147
+ /**
148
+ * Fit a language tag to the property's `pattern`, if it has one.
149
+ *
150
+ * A schema that stores ISO 639-1 (`^[a-z]{2}$`) cannot take `en-GB`, so the
151
+ * tag falls back to its primary subtag. Without a pattern the full tag stays.
152
+ *
153
+ * @param {string} tag A BCP 47 tag.
154
+ * @param {string|undefined} pattern The property's `pattern`.
155
+ * @return {string} The tag as the schema accepts it.
156
+ */
157
+ function fitLanguageTag(tag, pattern) {
158
+ if (!pattern) {
159
+ return tag
160
+ }
161
+ let re;
162
+ try {
163
+ re = new RegExp(pattern);
164
+ } catch {
165
+ return tag
166
+ }
167
+ if (re.test(tag)) {
168
+ return tag
169
+ }
170
+ const primary = tag.split('-')[0].toLowerCase();
171
+ return re.test(primary) ? primary : tag
172
+ }
173
+
174
+ /**
175
+ * Resolve an `x-default` token to the value a NEW object starts with.
176
+ *
177
+ * - `current-language`: the user's Nextcloud language (BCP 47, fitted to the
178
+ * property's `pattern`).
179
+ * - `current-timezone`: the browser's time zone.
180
+ *
181
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
182
+ * @param {string|null} token The `x-default` value.
183
+ * @param {object} [field] The field descriptor (reads `validation.pattern`).
184
+ * @return {string|null} The value, or null for an unknown token.
185
+ */
186
+ function resolveDefaultToken(token, field = {}) {
187
+ if (token === 'current-language') {
188
+ const pattern = field && field.validation ? field.validation.pattern : undefined;
189
+ return fitLanguageTag(currentLanguageTag(), pattern)
190
+ }
191
+ if (token === 'current-timezone') {
192
+ return currentTimezone()
193
+ }
194
+ return null
195
+ }
196
+
197
+ export { LANGUAGE_CODES, currentLanguageTag, currentTimezone, languageOptions, resolveDefaultToken, timezoneOptions };
198
+ //# sourceMappingURL=pickerOptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pickerOptions.js","sources":["../../../src/utils/pickerOptions.js"],"sourcesContent":["/**\n * Option lists for the language and time zone pickers, and the defaults a\n * schema can ask for with `x-default`.\n *\n * Both pickers store a code (`nl`, `Europe/Amsterdam`) and show a label in\n * the user's own language, so a person picks \"Nederlands\" and the record\n * stores `nl`.\n *\n * @module utils/pickerOptions\n */\n\nimport { getLanguage } from '@nextcloud/l10n'\n\n/**\n * ISO 639-1 language codes. `Intl` has no list of languages, so the codes\n * live here; the labels come from `Intl.DisplayNames` at runtime.\n *\n * @type {string[]}\n */\nexport const LANGUAGE_CODES = (''\n\t+ 'aa ab af ak am an ar as av ay az ba be bg bi bm bn bo br bs '\n\t+ 'ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es et eu '\n\t+ 'fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu '\n\t+ 'hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl '\n\t+ 'km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh '\n\t+ 'mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc '\n\t+ 'oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg '\n\t+ 'si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl '\n\t+ 'tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za '\n\t+ 'zh zu'\n).split(' ')\n\n/**\n * A small fallback for runtimes without `Intl.supportedValuesOf`.\n *\n * @type {string[]}\n */\nconst FALLBACK_TIMEZONES = 'UTC Europe/Amsterdam Europe/Berlin Europe/Brussels Europe/London Europe/Paris America/New_York America/Chicago America/Los_Angeles Asia/Tokyo Australia/Sydney'.split(' ')\n\n/**\n * The user's Nextcloud language as a BCP 47 tag (`en_GB` becomes `en-GB`).\n *\n * @return {string} The tag, `en` when Nextcloud reports nothing.\n */\nexport function currentLanguageTag() {\n\tlet lang\n\ttry {\n\t\tlang = getLanguage()\n\t} catch {\n\t\tlang = ''\n\t}\n\treturn String(lang || 'en').replace(/_/g, '-')\n}\n\n/**\n * The browser's time zone (IANA id).\n *\n * @return {string} The time zone, `UTC` when the runtime does not say.\n */\nexport function currentTimezone() {\n\ttry {\n\t\treturn Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n\t} catch {\n\t\treturn 'UTC'\n\t}\n}\n\n/**\n * Cached option lists, keyed by display language. Stable identity matters:\n * the form's select matches the chosen option by reference.\n *\n * @type {Map<string, {codes: string[], labels: object}>}\n */\nconst languageCache = new Map()\n\n/**\n * The language picker's codes and labels, labelled in `displayLanguage`.\n *\n * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md\n * @param {string} [displayLanguage] BCP 47 tag the labels are written in; defaults to the user's language.\n * @return {{codes: string[], labels: object}} Codes sorted by label, and `{ code: label }`.\n */\nexport function languageOptions(displayLanguage) {\n\tconst locale = displayLanguage || currentLanguageTag()\n\tif (languageCache.has(locale)) {\n\t\treturn languageCache.get(locale)\n\t}\n\tlet names\n\ttry {\n\t\tnames = new Intl.DisplayNames([locale, 'en'], { type: 'language' })\n\t} catch {\n\t\tnames = null\n\t}\n\tconst labels = {}\n\tfor (const code of LANGUAGE_CODES) {\n\t\tlet label\n\t\ttry {\n\t\t\tlabel = (names && names.of(code)) || code\n\t\t} catch {\n\t\t\tlabel = code\n\t\t}\n\t\t// `Intl` lower-cases some names (\"nederlands\" is fine in a sentence,\n\t\t// not in a list); capitalise the first letter for the list.\n\t\tlabels[code] = label.charAt(0).toLocaleUpperCase(locale) + label.slice(1)\n\t}\n\tconst codes = [...LANGUAGE_CODES].sort((a, b) => labels[a].localeCompare(labels[b], locale))\n\tconst result = { codes, labels }\n\tlanguageCache.set(locale, result)\n\treturn result\n}\n\n/** @type {{codes: string[], labels: object}|null} */\nlet timezoneCache = null\n\n/**\n * The time zone picker's codes and labels (`Europe/Amsterdam` shows as\n * `Europe/Amsterdam`, underscores as spaces).\n *\n * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md\n * @return {{codes: string[], labels: object}} IANA ids, and `{ id: label }`.\n */\nexport function timezoneOptions() {\n\tif (timezoneCache) {\n\t\treturn timezoneCache\n\t}\n\tlet codes\n\ttry {\n\t\tcodes = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : []\n\t} catch {\n\t\tcodes = []\n\t}\n\tif (!codes.length) {\n\t\tcodes = FALLBACK_TIMEZONES\n\t}\n\tif (!codes.includes('UTC')) {\n\t\tcodes = ['UTC', ...codes]\n\t}\n\tconst labels = {}\n\tfor (const code of codes) {\n\t\tlabels[code] = code.replace(/_/g, ' ')\n\t}\n\ttimezoneCache = { codes: [...codes], labels }\n\treturn timezoneCache\n}\n\n/**\n * Fit a language tag to the property's `pattern`, if it has one.\n *\n * A schema that stores ISO 639-1 (`^[a-z]{2}$`) cannot take `en-GB`, so the\n * tag falls back to its primary subtag. Without a pattern the full tag stays.\n *\n * @param {string} tag A BCP 47 tag.\n * @param {string|undefined} pattern The property's `pattern`.\n * @return {string} The tag as the schema accepts it.\n */\nfunction fitLanguageTag(tag, pattern) {\n\tif (!pattern) {\n\t\treturn tag\n\t}\n\tlet re\n\ttry {\n\t\tre = new RegExp(pattern)\n\t} catch {\n\t\treturn tag\n\t}\n\tif (re.test(tag)) {\n\t\treturn tag\n\t}\n\tconst primary = tag.split('-')[0].toLowerCase()\n\treturn re.test(primary) ? primary : tag\n}\n\n/**\n * Resolve an `x-default` token to the value a NEW object starts with.\n *\n * - `current-language`: the user's Nextcloud language (BCP 47, fitted to the\n * property's `pattern`).\n * - `current-timezone`: the browser's time zone.\n *\n * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md\n * @param {string|null} token The `x-default` value.\n * @param {object} [field] The field descriptor (reads `validation.pattern`).\n * @return {string|null} The value, or null for an unknown token.\n */\nexport function resolveDefaultToken(token, field = {}) {\n\tif (token === 'current-language') {\n\t\tconst pattern = field && field.validation ? field.validation.pattern : undefined\n\t\treturn fitLanguageTag(currentLanguageTag(), pattern)\n\t}\n\tif (token === 'current-timezone') {\n\t\treturn currentTimezone()\n\t}\n\treturn null\n}\n"],"names":[],"mappings":"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"}
@@ -298,10 +298,13 @@ function truncateString(str, maxLength) {
298
298
  * surface (CnFormDialog) resolves the reference to a searchable dropdown
299
299
  * of the referenced objects (label = human name, value = UUID).
300
300
  * 4. Nextcloud user reference: `referenceType: 'nextcloud-user'` (or
301
- * `format: 'user'`/`'username'`) → `'user'`; an array of such
301
+ * `format: 'user'`/`'username'`/`'nc-user'`) → `'user'`; an array of such
302
302
  * properties → `'user-multiselect'`. CnFormDialog resolves these to a
303
303
  * searchable dropdown of real Nextcloud users (label = display name,
304
- * value = UID).
304
+ * value = UID). A Nextcloud group (`referenceType: 'nextcloud-group'` or
305
+ * `format: 'nc-group'`) → `'group'`, an array of groups →
306
+ * `'group-multiselect'`. `format: 'language'` → `'language'` and
307
+ * `format: 'timezone'` → `'timezone'` (both searchable code lists).
305
308
  * 5. Type-based: `boolean` → `'checkbox'`, `integer`/`number` → `'number'`,
306
309
  * `array` + `items.enum` → `'multiselect'`, `array` → `'tags'`
307
310
  * 6. Format-based: `date-time` → `'datetime'`, `date` → `'date'`,
@@ -369,9 +372,32 @@ function isUserProp(prop) {
369
372
  return true
370
373
  }
371
374
  const format = prop.format || '';
372
- return format === 'user' || format === 'username'
375
+ return format === 'user' || format === 'username' || format === 'nc-user'
373
376
  }
374
377
 
378
+ /**
379
+ * Whether a (single-value) schema property represents a Nextcloud group.
380
+ *
381
+ * A property is a group field when it declares `referenceType: 'nextcloud-group'`
382
+ * or `format: 'nc-group'`. CnFormDialog renders it as a searchable dropdown of
383
+ * Nextcloud groups (label = display name, value = group id).
384
+ *
385
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
386
+ * @param {object} prop A schema property definition (or `items` for an array).
387
+ * @return {boolean} True when the property marks a Nextcloud group.
388
+ */
389
+ function isGroupProp(prop) {
390
+ if (!prop || typeof prop !== 'object') {
391
+ return false
392
+ }
393
+ return prop.referenceType === 'nextcloud-group' || prop.format === 'nc-group'
394
+ }
395
+
396
+ /**
397
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
398
+ * @param {object} prop The schema property definition.
399
+ * @return {string} The widget identifier (see the block above).
400
+ */
375
401
  function resolveWidget(prop) {
376
402
  // Explicit widget hint takes priority
377
403
  if (prop.widget) {
@@ -410,6 +436,24 @@ function resolveWidget(prop) {
410
436
  return 'user-multiselect'
411
437
  }
412
438
 
439
+ // Nextcloud group (`format: 'nc-group'` / `referenceType: 'nextcloud-group'`):
440
+ // a searchable dropdown of groups storing the group id.
441
+ if (isGroupProp(prop)) {
442
+ return 'group'
443
+ }
444
+ if (type === 'array' && isGroupProp(prop.items)) {
445
+ return 'group-multiselect'
446
+ }
447
+
448
+ // Language and time zone codes: a searchable list instead of a free-text
449
+ // box, so a person picks "Dutch" and the record stores `nl`.
450
+ if (type === 'string' && format === 'language') {
451
+ return 'language'
452
+ }
453
+ if (type === 'string' && format === 'timezone') {
454
+ return 'timezone'
455
+ }
456
+
413
457
  // Boolean → switch/checkbox
414
458
  if (type === 'boolean') {
415
459
  return 'checkbox'
@@ -588,7 +632,8 @@ function isTenantProperty(key, prop) {
588
632
  * @param {boolean} [options.includeReadOnly] Whether to include readOnly properties
589
633
  * @param {boolean} [options.hideTenant] Drop properties that hold the record's tenant (see `isTenantProperty`). Off by default, so a detail page still shows the tenant; CnFormDialog turns it on because nobody should be asked for it. `overrides[key].hidden === false` keeps one visible.
590
634
  * @param {(text: string) => string} [options.translate] Optional display-layer translation function applied to each field's `label` and `description`. Schema property titles/descriptions are authored in English as the canonical source; consumers pass their bound `t()` (via the injected `cnTranslate`) so the rendered field label follows the user's language. When omitted, label/description are the English source strings unchanged (pure, backward-compatible).
591
- * @return {Array<{key: string, label: string, description: string, descriptionLong: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: unknown, enum: Array|null, enumLabels: object|null, items: object|null, referenceType: string|null, referenceSemanticType: string|null, referenceSemanticApp: string|null, reference: {schema: string|number, multiple: boolean}|null, userPicker: {multiple: boolean}|null, fillFrom: object|null, validation: object, order: number}>} `description` is the inline helper text (see `splitDescription`); `descriptionLong` carries the full text when it was too long to render inline, else ''. `enumLabels` maps each raw enum value to its English display label (from the property's `x-enum-labels`), or null.
635
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
636
+ * @return {Array<{key: string, label: string, description: string, descriptionLong: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: unknown, enum: Array|null, enumLabels: object|null, items: object|null, referenceType: string|null, referenceSemanticType: string|null, referenceSemanticApp: string|null, reference: {schema: string|number, multiple: boolean, register?: string, labelField?: string}|null, allowCreate: boolean, userPicker: {multiple: boolean}|null, groupPicker: {multiple: boolean}|null, defaultToken: string|null, fillFrom: object|null, validation: object, order: number}>} `description` is the inline helper text (see `splitDescription`); `descriptionLong` carries the property's `x-help` text when it declares one, else the full description when it was too long to render inline, else ''. `enumLabels` maps each raw enum value to its English display label (from the property's `x-enum-labels`), or null.
592
637
  */
593
638
  function fieldsFromSchema(schema, options = {}) {
594
639
  const { exclude = [], include = null, overrides = {}, includeReadOnly = false, hideTenant = false, translate } = options;
@@ -664,11 +709,19 @@ function fieldsFromSchema(schema, options = {}) {
664
709
 
665
710
  return entries.map(([key, prop]) => {
666
711
  const description = splitDescription(prop.description ? tr(prop.description) : '');
712
+ // `x-help` is the long explanation behind the (i) popover. It always
713
+ // shows the popover, while `description` stays the short helper line.
714
+ const help = typeof prop['x-help'] === 'string' && prop['x-help'].trim() !== ''
715
+ ? tr(prop['x-help'].trim())
716
+ : '';
717
+ const labelField = prop['x-label-field']
718
+ || (prop.items && prop.items['x-label-field'])
719
+ || null;
667
720
  const field = {
668
721
  key,
669
722
  label: tr(prop.title || key),
670
723
  description: description.short,
671
- descriptionLong: description.long,
724
+ descriptionLong: help || description.long,
672
725
  type: prop.type || 'string',
673
726
  format: prop.format || null,
674
727
  widget: resolveWidget(prop),
@@ -686,6 +739,10 @@ function fieldsFromSchema(schema, options = {}) {
686
739
  required: requiredKeys.includes(key),
687
740
  readOnly: prop.readOnly || false,
688
741
  default: prop.default !== undefined ? prop.default : null,
742
+ // A default that depends on who opens the form (`x-default:
743
+ // 'current-language'` / `'current-timezone'`). Resolved by the
744
+ // consuming form on a NEW object only; never on edit.
745
+ defaultToken: typeof prop['x-default'] === 'string' ? prop['x-default'] : null,
689
746
  enum: prop.enum || null,
690
747
  // Display labels for the enum CODES, keyed by raw value. An enum
691
748
  // value is a stored contract value, not display text — several are
@@ -733,11 +790,23 @@ function fieldsFromSchema(schema, options = {}) {
733
790
  // form's own; absent, the reference stays same-register (the form's
734
791
  // `register` prop is used). Consolidates the ad-hoc caseReference /
735
792
  // approvalDecisionId / x-mirror-of variants onto one convention.
793
+ //
794
+ // `x-label-field` names the referenced object's property that labels
795
+ // an option and that an inline create writes the typed term to
796
+ // (`reference.labelField`, default `name` at the consumer).
736
797
  reference: (normalizeRef(prop.$ref) !== null)
737
- ? { schema: normalizeRef(prop.$ref), multiple: false, ...(prop['x-external-register'] ? { register: prop['x-external-register'] } : {}) }
798
+ ? { schema: normalizeRef(prop.$ref), multiple: false, ...(prop['x-external-register'] ? { register: prop['x-external-register'] } : {}), ...(labelField ? { labelField } : {}) }
738
799
  : (prop.type === 'array' && prop.items && normalizeRef(prop.items.$ref) !== null)
739
- ? { schema: normalizeRef(prop.items.$ref), multiple: true, ...((prop.items['x-external-register'] || prop['x-external-register']) ? { register: prop.items['x-external-register'] || prop['x-external-register'] } : {}) }
800
+ ? { schema: normalizeRef(prop.items.$ref), multiple: true, ...((prop.items['x-external-register'] || prop['x-external-register']) ? { register: prop.items['x-external-register'] || prop['x-external-register'] } : {}), ...(labelField ? { labelField } : {}) }
740
801
  : null,
802
+ // Select OR create (`x-allow-create: true`, on the property or on
803
+ // its `items` for an array): the picker offers "Create" next to the
804
+ // existing objects. Only meaningful on a reference.
805
+ allowCreate: prop['x-allow-create'] === true
806
+ || (prop.type === 'array' && !!prop.items && prop.items['x-allow-create'] === true),
807
+ // Template copy (`x-fill-from: { formKey: sourceKey }`): choosing a
808
+ // referenced object copies those of its values into this form.
809
+ fillFrom: (prop['x-fill-from'] && typeof prop['x-fill-from'] === 'object') ? prop['x-fill-from'] : null,
741
810
  // Nextcloud user reference: when a property marks a NC user
742
811
  // (`referenceType: 'nextcloud-user'`, or `format: 'user'`/
743
812
  // `'username'`), tag it so CnFormDialog renders a searchable
@@ -750,6 +819,13 @@ function fieldsFromSchema(schema, options = {}) {
750
819
  : (prop.type === 'array' && isUserProp(prop.items))
751
820
  ? { multiple: true }
752
821
  : null,
822
+ // Nextcloud group reference (`format: 'nc-group'` or
823
+ // `referenceType: 'nextcloud-group'`): the form stores the group id.
824
+ groupPicker: isGroupProp(prop)
825
+ ? { multiple: false }
826
+ : (prop.type === 'array' && isGroupProp(prop.items))
827
+ ? { multiple: true }
828
+ : null,
753
829
  // Conditional immutability (AD: x-openregister-readonly-when): a
754
830
  // property can declare it becomes read-only when another field on the
755
831
  // same object holds a given value — e.g. a hybrid app's identity