@conduction/nextcloud-vue 2.70.0 → 2.71.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 (50) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +151 -13
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  6. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +38 -42
  7. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  8. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +29 -2
  9. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  10. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +71 -34
  11. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +22 -0
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +1 -1
  15. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +1 -1
  17. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  18. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  19. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  20. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +46 -32
  21. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  22. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +30 -9
  23. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  24. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  25. package/dist/esm/composables/useWalkthrough.js +11 -3
  26. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  27. package/dist/esm/nextcloud-vue.css +53 -0
  28. package/dist/esm/utils/columnFilters.js +54 -4
  29. package/dist/esm/utils/columnFilters.js.map +1 -1
  30. package/dist/esm/utils/userAutocomplete.js +69 -3
  31. package/dist/esm/utils/userAutocomplete.js.map +1 -1
  32. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  33. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  34. package/dist/nextcloud-vue.cjs.js +615 -235
  35. package/dist/nextcloud-vue.cjs.js.map +2 -2
  36. package/dist/nextcloud-vue.css +53 -0
  37. package/package.json +1 -1
  38. package/src/components/CnAppRoot/CnAppRoot.vue +153 -13
  39. package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -18
  40. package/src/components/CnFormDialog/CnFormDialog.vue +62 -2
  41. package/src/components/CnIndexPage/CnIndexPage.vue +23 -1
  42. package/src/components/CnObjectSidebar/CnNotesTab.vue +1 -1
  43. package/src/components/CnSetupWizard/CnSetupWizard.vue +7 -0
  44. package/src/components/CnWalkthrough/CnWalkthrough.vue +30 -9
  45. package/src/composables/useWalkthrough.js +11 -3
  46. package/src/css/dashboard.css +44 -0
  47. package/src/schemas/app-manifest-v2.schema.json +2 -1
  48. package/src/utils/columnFilters.js +54 -4
  49. package/src/utils/userAutocomplete.js +68 -2
  50. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -216,11 +216,13 @@ export function walkthroughProgressKey(configKey) {
216
216
 
217
217
  /**
218
218
  * Normalise stored progress (an object, or the JSON string a preference
219
- * holds) into `{ tourId, stepId, index, version }`, or null.
219
+ * holds) into `{ tourId, stepId, index, version }`, plus `paused: true`
220
+ * when the user paused the tour, or null.
220
221
  *
221
222
  * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
223
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
222
224
  * @param {unknown} value The stored value.
223
- * @return {{tourId: string, stepId: string, index: number, version: string}|null} The progress.
225
+ * @return {{tourId: string, stepId: string, index: number, version: string, paused: (boolean|undefined)}|null} The progress.
224
226
  */
225
227
  export function normaliseWalkthroughProgress(value) {
226
228
  let data = value
@@ -237,12 +239,18 @@ export function normaliseWalkthroughProgress(value) {
237
239
  if (!isPlainObject(data) || typeof data.tourId !== 'string' || data.tourId === '') {
238
240
  return null
239
241
  }
240
- return {
242
+ const progress = {
241
243
  tourId: data.tourId,
242
244
  stepId: typeof data.stepId === 'string' ? data.stepId : '',
243
245
  index: Number.isInteger(data.index) && data.index >= 0 ? data.index : 0,
244
246
  version: typeof data.version === 'string' ? data.version : '',
245
247
  }
248
+ // A paused tour stays hidden on the next visit until the user picks
249
+ // "Continue". Only written when true, so older values read unchanged.
250
+ if (data.paused === true) {
251
+ progress.paused = true
252
+ }
253
+ return progress
246
254
  }
247
255
 
248
256
  /**
@@ -129,3 +129,47 @@
129
129
  .cn-requires-app.cn-requires-app .empty-content__action {
130
130
  margin-top: var(--default-grid-baseline, 4px);
131
131
  }
132
+
133
+ /* NcEmptyContent forces its icon's svg to 64px with !important while the
134
+ * rule above lets the icon box shrink, so the svg spilled out of its box and
135
+ * over the name. Hold the svg to the size the caller asked for. */
136
+ .cn-requires-app.cn-requires-app .empty-content__icon svg {
137
+ width: 32px !important;
138
+ height: 32px !important;
139
+ max-width: 32px !important;
140
+ max-height: 32px !important;
141
+ }
142
+
143
+ /* The dashboard's compact "Install <app>" placeholder: two lines of text and
144
+ * the button, nothing else, so it fits a two-row tile even under a title row.
145
+ * `safe center` keeps the top in view when a tile is smaller still, and the
146
+ * body scrolls rather than clipping the button. */
147
+ .cn-requires-app--compact {
148
+ display: flex;
149
+ flex-direction: column;
150
+ align-items: center;
151
+ justify-content: safe center;
152
+ gap: calc(2 * var(--default-grid-baseline, 4px));
153
+ box-sizing: border-box;
154
+ height: 100%;
155
+ min-height: 0;
156
+ overflow: auto;
157
+ padding: calc(2 * var(--default-grid-baseline, 4px));
158
+ text-align: center;
159
+ }
160
+
161
+ .cn-requires-app--compact .cn-requires-app__description {
162
+ display: -webkit-box;
163
+ margin: 0;
164
+ overflow: hidden;
165
+ color: var(--color-text-maxcontrast);
166
+ font-size: 0.9em;
167
+ line-height: 1.4;
168
+ -webkit-box-orient: vertical;
169
+ -webkit-line-clamp: 2;
170
+ line-clamp: 2;
171
+ }
172
+
173
+ .cn-requires-app--compact .button-vue {
174
+ flex-shrink: 0;
175
+ }
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.52.0",
6
+ "version": "2.53.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -64,6 +64,7 @@
64
64
  "enabled": { "type": "boolean", "default": true, "description": "Whether the setup wizard is active for this app." },
65
65
  "version": { "type": "integer", "default": 1, "description": "Setup contract version; bump to re-trigger setup after a breaking change." },
66
66
  "completionConfigKey": { "type": "string", "description": "App-config key storing the completed setup version." },
67
+ "dismissAction": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "description": "Setup action that records a closed or finished setup wizard on the server, so it stays closed in every browser. CnAppRoot posts `/apps/{appId}/api/setup/action/{dismissAction}` with `{ finished }` once when the user closes or finishes the wizard. The app either answers the outstanding optional steps or reports `dismissed` (true, or the setup version it was closed at) in `GET /api/setup/status`. Without it, the close is remembered only in the browser (localStorage), as before." },
67
68
  "steps": {
68
69
  "type": "array",
69
70
  "description": "Ordered setup steps rendered by CnSetupWizard.",
@@ -161,15 +161,65 @@ function referenceTarget(column, prop) {
161
161
  return { schema, register, labelField }
162
162
  }
163
163
 
164
+ /**
165
+ * Whether a schema lists its properties. Without that the table knows its
166
+ * columns but not what backs them.
167
+ *
168
+ * @param {object|null} schema The table's schema.
169
+ * @return {boolean} True when the schema has a properties map.
170
+ */
171
+ function hasProperties(schema) {
172
+ return !!(schema && typeof schema === 'object' && schema.properties && typeof schema.properties === 'object')
173
+ }
174
+
175
+ /**
176
+ * A filter for a column when the table has no schema to read (a manifest
177
+ * page whose schema did not resolve, a host-fed table). The column's own
178
+ * hints decide the kind: an `fkResolve` widget gives a reference filter, a
179
+ * `type` or `format` on the column gives number, date or yes/no, and every
180
+ * other plain stored key gets a text filter on contains (`[like]`). A key
181
+ * that is a path (`a.b`) or a metadata field (`@self`, `_id`) gets none,
182
+ * because OpenRegister does not filter those by that name.
183
+ *
184
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-data-table/spec.md
185
+ * @param {object} column The column definition.
186
+ * @param {object} base The shared part of the definition.
187
+ * @return {object|null} The filter definition.
188
+ */
189
+ function columnHintFilterDef(column, base) {
190
+ const key = column.key
191
+ if (key.includes('.') || key.charAt(0) === '@' || key.charAt(0) === '_') {
192
+ return null
193
+ }
194
+ const reference = referenceTarget(column, null)
195
+ if (reference) {
196
+ return { ...base, kind: 'reference', reference }
197
+ }
198
+ const type = String(column.type || '').toLowerCase()
199
+ const format = String(column.format || '').toLowerCase()
200
+ if (type === 'boolean') {
201
+ return { ...base, kind: 'boolean' }
202
+ }
203
+ if (type === 'number' || type === 'integer') {
204
+ return { ...base, kind: 'number' }
205
+ }
206
+ if (type === 'date' || type === 'date-time' || format === 'date' || format === 'date-time') {
207
+ return { ...base, kind: 'date', dateTime: type === 'date-time' || format === 'date-time' }
208
+ }
209
+ return { ...base, kind: 'string' }
210
+ }
211
+
164
212
  /**
165
213
  * How a column filters, or null when it does not.
166
214
  *
167
215
  * Kinds: `enum` (checkbox list), `boolean` (yes, no, any), `reference`
168
216
  * (searchable list of the referenced objects), `number` and `date` (from and
169
217
  * to), and `string` (contains, through OpenRegister's `[like]`). `filterable: false` on the column turns it
170
- * off. A column with no schema property behind it only filters when it
171
- * carries an enum hint (`enum`, or a badge widget's colour map), because the
172
- * server cannot filter on a value it does not store.
218
+ * off. When the schema lists its properties, a column with no property
219
+ * behind it only filters when it carries an enum hint (`enum`, or a badge
220
+ * widget's colour map), because the server cannot filter on a value it does
221
+ * not store. When there is no schema to read, the column's own hints decide
222
+ * (see `columnHintFilterDef`), falling back to a text filter on contains.
173
223
  *
174
224
  * @param {object} column The column definition.
175
225
  * @param {object|null} schema The table's schema.
@@ -191,7 +241,7 @@ export function columnFilterDef(column, schema) {
191
241
  return { ...base, kind: 'enum', options }
192
242
  }
193
243
  if (!prop) {
194
- return null
244
+ return hasProperties(schema) ? null : columnHintFilterDef(column, base)
195
245
  }
196
246
  const reference = referenceTarget(column, prop)
197
247
  if (reference) {
@@ -12,9 +12,50 @@
12
12
  * @module utils/userAutocomplete
13
13
  */
14
14
 
15
+ import { getCurrentUser } from '@nextcloud/auth'
15
16
  import axios from '@nextcloud/axios'
16
17
  import { generateOcsUrl } from '@nextcloud/router'
17
18
 
19
+ /**
20
+ * The signed-in user as a picker option, or null when nobody is signed in.
21
+ *
22
+ * Core autocomplete never returns the person who searches (it is built for
23
+ * sharing, and you do not share with yourself), so a picker backed by it
24
+ * alone cannot assign a task to yourself.
25
+ *
26
+ * @spec openspec/changes/audit-round-lib-fixes/specs/schema-utilities/spec.md
27
+ * @return {{id: string, label: string, displayName: string, subline: string}|null} The option.
28
+ */
29
+ function currentUserOption() {
30
+ let user
31
+ try {
32
+ user = getCurrentUser()
33
+ } catch {
34
+ return null
35
+ }
36
+ if (!user || !user.uid) {
37
+ return null
38
+ }
39
+ const name = user.displayName || String(user.uid)
40
+ return { id: String(user.uid), label: name, displayName: name, subline: '' }
41
+ }
42
+
43
+ /**
44
+ * Whether the current-user option matches a search term (uid or display
45
+ * name contains the term, ignoring case). An empty term matches.
46
+ *
47
+ * @param {object} option The current-user option.
48
+ * @param {string} query The search term.
49
+ * @return {boolean} True when it matches.
50
+ */
51
+ function matchesQuery(option, query) {
52
+ const q = String(query || '').trim().toLowerCase()
53
+ if (q === '') {
54
+ return true
55
+ }
56
+ return option.id.toLowerCase().includes(q) || option.label.toLowerCase().includes(q)
57
+ }
58
+
18
59
  /**
19
60
  * Map one autocomplete suggestion to a picker option, keeping only user
20
61
  * results. The OCS payload uses `id` for the UID, `label` for the display
@@ -59,10 +100,11 @@ function toUserOption(suggestion) {
59
100
  * @param {string} [query] The search term (empty loads an initial page).
60
101
  * @param {object} [options] Tuning options.
61
102
  * @param {number} [options.limit] Max results (default 25).
103
+ * @param {boolean} [options.includeCurrentUser] Add the signed-in user when they match (default true; a mention list passes false).
62
104
  * @return {Promise<Array<{id: string, label: string, subline: string}>>} Picker options.
63
105
  */
64
106
  export async function searchNextcloudUsers(query = '', options = {}) {
65
- const { limit = 25 } = options
107
+ const { limit = 25, includeCurrentUser = true } = options
66
108
  try {
67
109
  const url = generateOcsUrl('core/autocomplete/get')
68
110
  const response = await axios.get(url, {
@@ -86,14 +128,32 @@ export async function searchNextcloudUsers(query = '', options = {}) {
86
128
  const list = Array.isArray(data)
87
129
  ? data
88
130
  : (Array.isArray(response && response.data) ? response.data : [])
89
- return list
131
+ const found = list
90
132
  .map(toUserOption)
91
133
  .filter((opt) => opt !== null)
134
+ return includeCurrentUser ? withCurrentUser(found, query) : found
92
135
  } catch {
93
136
  return []
94
137
  }
95
138
  }
96
139
 
140
+ /**
141
+ * Put the signed-in user first in a result list when they match the search
142
+ * and the server left them out.
143
+ *
144
+ * @spec openspec/changes/audit-round-lib-fixes/specs/schema-utilities/spec.md
145
+ * @param {Array<object>} options The server's options.
146
+ * @param {string} query The search term.
147
+ * @return {Array<object>} The options, the current user included when they match.
148
+ */
149
+ function withCurrentUser(options, query) {
150
+ const me = currentUserOption()
151
+ if (!me || !matchesQuery(me, query) || options.some((opt) => opt.id === me.id)) {
152
+ return options
153
+ }
154
+ return [me, ...options]
155
+ }
156
+
97
157
  /**
98
158
  * Resolve a single user UID to a `{ id, label }` option for edit-mode display.
99
159
  * Tries the autocomplete endpoint with the UID as the query and matches the
@@ -108,6 +168,12 @@ export async function resolveNextcloudUser(uid) {
108
168
  if (uid === undefined || uid === null || uid === '') {
109
169
  return fallback
110
170
  }
171
+ // The server never returns the signed-in user, so their own uid would
172
+ // otherwise fall back to the bare uid.
173
+ const me = currentUserOption()
174
+ if (me && me.id === String(uid)) {
175
+ return me
176
+ }
111
177
  const results = await searchNextcloudUsers(String(uid))
112
178
  const match = results.find((opt) => opt.id === String(uid))
113
179
  return match || fallback