@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.8

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 (82) hide show
  1. package/dist/nextcloud-vue.cjs.js +13575 -2374
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +1238 -270
  4. package/dist/nextcloud-vue.esm.js +13517 -2336
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +11 -7
  7. package/src/components/CnActionsBar/CnActionsBar.vue +20 -2
  8. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  11. package/src/components/CnCard/CnCard.vue +415 -0
  12. package/src/components/CnCard/index.js +1 -0
  13. package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
  14. package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
  16. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
  17. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
  18. package/src/components/CnDataTable/CnDataTable.vue +6 -2
  19. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
  20. package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
  21. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  22. package/src/components/CnDetailGrid/index.js +1 -0
  23. package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
  24. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
  25. package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
  26. package/src/components/CnIcon/CnIcon.vue +1 -1
  27. package/src/components/CnIndexPage/CnIndexPage.vue +63 -9
  28. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
  29. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  30. package/src/components/CnInfoWidget/index.js +1 -0
  31. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  32. package/src/components/CnJsonViewer/index.js +1 -0
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  34. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
  35. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
  36. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
  38. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  39. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  40. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  41. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
  43. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  44. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  45. package/src/components/CnObjectSidebar/index.js +5 -0
  46. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  47. package/src/components/CnProgressBar/index.js +1 -0
  48. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
  50. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  51. package/src/components/CnStatsPanel/index.js +1 -0
  52. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  53. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
  54. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  55. package/src/components/CnTableWidget/index.js +1 -0
  56. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
  57. package/src/components/index.js +11 -0
  58. package/src/composables/useDashboardView.js +58 -12
  59. package/src/composables/useDetailView.js +3 -2
  60. package/src/composables/useListView.js +7 -6
  61. package/src/composables/useSubResource.js +3 -3
  62. package/src/css/badge.css +32 -0
  63. package/src/css/card.css +1 -0
  64. package/src/css/detail-page.css +74 -7
  65. package/src/index.js +16 -0
  66. package/src/mixins/gridLayout.js +118 -0
  67. package/src/store/createCrudStore.js +360 -0
  68. package/src/store/createSubResourcePlugin.js +5 -15
  69. package/src/store/index.js +1 -0
  70. package/src/store/plugins/auditTrails.js +346 -6
  71. package/src/store/plugins/lifecycle.js +4 -4
  72. package/src/store/plugins/registerMapping.js +18 -8
  73. package/src/store/plugins/relations.js +1 -1
  74. package/src/store/plugins/search.js +21 -8
  75. package/src/store/useObjectStore.js +30 -36
  76. package/src/utils/getTheme.js +9 -0
  77. package/src/utils/headers.js +13 -3
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
  81. package/src/components/CnObjectCard/eslint-setup.md +0 -235
  82. package/src/components/CnObjectCard/package.json-or.json +0 -132
@@ -1,5 +1,5 @@
1
1
  import { defineStore } from 'pinia'
2
- import { buildHeaders, buildQueryString, prefixUrl } from '../utils/headers.js'
2
+ import { buildHeaders, buildQueryString, prefixUrl, capitalize } from '../utils/headers.js'
3
3
  import { parseResponseError, networkError, genericError } from '../utils/errors.js'
4
4
  import { extractId } from '../utils/id.js'
5
5
 
@@ -26,16 +26,6 @@ import { extractId } from '../utils/id.js'
26
26
  const DEFAULT_STORE_ID = 'conduction-objects'
27
27
  const DEFAULT_BASE_URL = '/apps/openregister/api/objects'
28
28
 
29
- /**
30
- * Capitalize the first letter of a string.
31
- *
32
- * @param {string} str Input string
33
- * @return {string} Capitalized string
34
- */
35
- function capitalize(str) {
36
- return str.charAt(0).toUpperCase() + str.slice(1)
37
- }
38
-
39
29
  /**
40
30
  * Merge plugin state factories into a single state object.
41
31
  *
@@ -123,49 +113,49 @@ function baseState(baseUrl = DEFAULT_BASE_URL) {
123
113
  const baseGetters = {
124
114
  /**
125
115
  * Get all registered object type slugs.
126
- * @param state
116
+ * @param {object} state Pinia state
127
117
  * @return {string[]}
128
118
  */
129
119
  objectTypes: (state) => Object.keys(state.objectTypeRegistry),
130
120
 
131
121
  /**
132
122
  * Get the collection array for a type.
133
- * @param state
123
+ * @param {object} state Pinia state
134
124
  * @return {Function} (type: string) => Array
135
125
  */
136
126
  getCollection: (state) => (type) => state.collections[type] || [],
137
127
 
138
128
  /**
139
129
  * Get a single cached object by type and ID.
140
- * @param state
130
+ * @param {object} state Pinia state
141
131
  * @return {Function} (type: string, id: string) => object|null
142
132
  */
143
133
  getObject: (state) => (type, id) => state.objects[type]?.[id] || null,
144
134
 
145
135
  /**
146
136
  * Alias for getObject — check cache without fetching.
147
- * @param state
137
+ * @param {object} state Pinia state
148
138
  * @return {Function} (type: string, id: string) => object|null
149
139
  */
150
140
  getCachedObject: (state) => (type, id) => state.objects[type]?.[id] || null,
151
141
 
152
142
  /**
153
143
  * Check if a type is currently loading.
154
- * @param state
144
+ * @param {object} state Pinia state
155
145
  * @return {Function} (type: string) => boolean
156
146
  */
157
147
  isLoading: (state) => (type) => state.loading[type] || false,
158
148
 
159
149
  /**
160
150
  * Get the current error for a type.
161
- * @param state
151
+ * @param {object} state Pinia state
162
152
  * @return {Function} (type: string) => ApiError|null
163
153
  */
164
154
  getError: (state) => (type) => state.errors[type] || null,
165
155
 
166
156
  /**
167
157
  * Get pagination state for a type.
168
- * @param state
158
+ * @param {object} state Pinia state
169
159
  * @return {Function} (type: string) => {total, page, pages, limit}
170
160
  */
171
161
  getPagination: (state) => (type) =>
@@ -173,28 +163,28 @@ const baseGetters = {
173
163
 
174
164
  /**
175
165
  * Get the current search term for a type.
176
- * @param state
166
+ * @param {object} state Pinia state
177
167
  * @return {Function} (type: string) => string
178
168
  */
179
169
  getSearchTerm: (state) => (type) => state.searchTerms[type] || '',
180
170
 
181
171
  /**
182
172
  * Get a cached schema for a type.
183
- * @param state
173
+ * @param {object} state Pinia state
184
174
  * @return {Function} (type: string) => object|null
185
175
  */
186
176
  getSchema: (state) => (type) => state.schemas[type] || null,
187
177
 
188
178
  /**
189
179
  * Get a cached register for a type.
190
- * @param state
180
+ * @param {object} state Pinia state
191
181
  * @return {Function} (type: string) => object|null
192
182
  */
193
183
  getRegister: (state) => (type) => state.registers[type] || null,
194
184
 
195
185
  /**
196
186
  * Get facet data for a type (CnIndexSidebar-compatible format).
197
- * @param state
187
+ * @param {object} state Pinia state
198
188
  * @return {Function} (type: string) => object
199
189
  */
200
190
  getFacets: (state) => (type) => state.facets[type] || {},
@@ -256,16 +246,20 @@ const baseActions = {
256
246
  * @param {string} slug The type slug to unregister
257
247
  */
258
248
  unregisterObjectType(slug) {
259
- delete this.objectTypeRegistry[slug]
260
- delete this.collections[slug]
261
- delete this.objects[slug]
262
- delete this.loading[slug]
263
- delete this.errors[slug]
264
- delete this.pagination[slug]
265
- delete this.searchTerms[slug]
266
- delete this.schemas[slug]
267
- delete this.registers[slug]
268
- delete this.facets[slug]
249
+ const omit = (obj, key) => {
250
+ const { [key]: _, ...rest } = obj
251
+ return rest
252
+ }
253
+ this.objectTypeRegistry = omit(this.objectTypeRegistry, slug)
254
+ this.collections = omit(this.collections, slug)
255
+ this.objects = omit(this.objects, slug)
256
+ this.loading = omit(this.loading, slug)
257
+ this.errors = omit(this.errors, slug)
258
+ this.pagination = omit(this.pagination, slug)
259
+ this.searchTerms = omit(this.searchTerms, slug)
260
+ this.schemas = omit(this.schemas, slug)
261
+ this.registers = omit(this.registers, slug)
262
+ this.facets = omit(this.facets, slug)
269
263
  },
270
264
 
271
265
  /**
@@ -343,7 +337,7 @@ const baseActions = {
343
337
 
344
338
  try {
345
339
  const response = await fetch(
346
- `/apps/openregister/api/schemas/${config.schema}`,
340
+ prefixUrl(`/apps/openregister/api/schemas/${config.schema}`),
347
341
  { method: 'GET', headers: buildHeaders() },
348
342
  )
349
343
 
@@ -373,7 +367,7 @@ const baseActions = {
373
367
 
374
368
  try {
375
369
  const response = await fetch(
376
- `/apps/openregister/api/registers/${config.register}`,
370
+ prefixUrl(`/apps/openregister/api/registers/${config.register}`),
377
371
  { method: 'GET', headers: buildHeaders() },
378
372
  )
379
373
 
@@ -693,7 +687,7 @@ const baseActions = {
693
687
  *
694
688
  * @param {string} type The registered type slug
695
689
  * @param {string[]} ids Array of object IDs to resolve
696
- * @return {Promise<Object<string, object>>} Map of id -> object
690
+ * @return {Promise<{[key: string]: object}>} Map of id -> object
697
691
  */
698
692
  async resolveReferences(type, ids) {
699
693
  if (!ids || ids.length === 0) return {}
@@ -795,7 +789,7 @@ function defineObjectStore(storeId, plugins = [], baseUrl = DEFAULT_BASE_URL) {
795
789
  * import { useObjectStore } from '@conduction/nextcloud-vue'
796
790
  * const store = useObjectStore()
797
791
  */
798
- export const useObjectStore = defineObjectStore(DEFAULT_STORE_ID)
792
+ export const useObjectStore = defineObjectStore(DEFAULT_STORE_ID, [], prefixUrl(DEFAULT_BASE_URL))
799
793
 
800
794
  /**
801
795
  * Factory function to create an object store with a custom Pinia store ID
@@ -0,0 +1,9 @@
1
+ export const getTheme = () => {
2
+ if (document.body.hasAttribute('data-theme-light')) {
3
+ return 'light'
4
+ }
5
+ if (document.body.hasAttribute('data-theme-default')) {
6
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
7
+ }
8
+ return 'dark'
9
+ }
@@ -27,7 +27,7 @@ export function prefixUrl(path) {
27
27
  */
28
28
  export function buildHeaders(contentType = 'application/json') {
29
29
  const headers = {
30
- requesttoken: OC.requestToken,
30
+ requesttoken: typeof OC !== 'undefined' ? OC.requestToken : '',
31
31
  'OCS-APIREQUEST': 'true',
32
32
  }
33
33
  if (contentType) {
@@ -36,11 +36,21 @@ export function buildHeaders(contentType = 'application/json') {
36
36
  return headers
37
37
  }
38
38
 
39
+ /**
40
+ * Capitalize the first letter of a string.
41
+ *
42
+ * @param {string} str Input string
43
+ * @return {string} Capitalized string
44
+ */
45
+ export function capitalize(str) {
46
+ return str.charAt(0).toUpperCase() + str.slice(1)
47
+ }
48
+
39
49
  /**
40
50
  * Build a query string from a params object.
41
51
  *
42
- * Handles array values by appending each item separately, plain object values
43
- * by JSON-serializing them, and skips null/undefined/empty values.
52
+ * Handles _order serialization (JSON.stringify for objects) and skips
53
+ * null/undefined/empty values.
44
54
  *
45
55
  * @param {object} params Key-value pairs for query parameters
46
56
  * @return {string} Query string including leading '?' or empty string
@@ -1,3 +1,4 @@
1
1
  export { buildHeaders, buildQueryString } from './headers.js'
2
2
  export { parseResponseError, networkError, genericError } from './errors.js'
3
3
  export { columnsFromSchema, formatValue, filtersFromSchema, fieldsFromSchema } from './schema.js'
4
+ export { filterWidgetsByVisibility, isWidgetVisible, getCurrentUserId, getCurrentUserGroups, resetVisibilityCache } from './widgetVisibility.js'
@@ -116,7 +116,7 @@ export function columnsFromSchema(schema, options = {}) {
116
116
  * @param {*} value The raw value
117
117
  * @param {object} [property] The schema property definition `{ type, format, enum, items }`
118
118
  * @param {object} [options] Formatting options
119
- * @param {number} [options.truncate=100] Maximum string length before truncation
119
+ * @param {number} [options.truncate] Maximum string length before truncation
120
120
  * @return {string} Formatted display string
121
121
  */
122
122
  export function formatValue(value, property = {}, options = {}) {
@@ -301,7 +301,7 @@ function resolveWidget(prop) {
301
301
  * @param {string[]} [options.exclude] Property keys to exclude
302
302
  * @param {string[]} [options.include] Property keys to include (whitelist mode)
303
303
  * @param {object} [options.overrides] Per-key field overrides, e.g. `{ status: { widget: 'select' } }`
304
- * @param {boolean} [options.includeReadOnly=false] Whether to include readOnly properties
304
+ * @param {boolean} [options.includeReadOnly] Whether to include readOnly properties
305
305
  * @return {Array<{key: string, label: string, description: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: *, enum: Array|null, items: object|null, validation: object, order: number}>}
306
306
  */
307
307
  export function fieldsFromSchema(schema, options = {}) {
@@ -383,7 +383,7 @@ export function filtersFromSchema(schema) {
383
383
  }
384
384
 
385
385
  return Object.entries(schema.properties)
386
- .filter(([key, prop]) => {
386
+ .filter(([, prop]) => {
387
387
  if (prop.facetable !== true) return false
388
388
  return true
389
389
  })
@@ -0,0 +1,162 @@
1
+ import axios from '@nextcloud/axios'
2
+ import { generateOcsUrl } from '@nextcloud/router'
3
+
4
+ /**
5
+ * Cached user groups — fetched once per session and reused.
6
+ * @type {{ userId: string|null, groups: string[]|null, promise: Promise|null }}
7
+ */
8
+ const _cache = {
9
+ userId: null,
10
+ groups: null,
11
+ promise: null,
12
+ }
13
+
14
+ /**
15
+ * Get the current Nextcloud user ID from OC.currentUser.
16
+ *
17
+ * @return {string|null} The current user ID, or null if not logged in
18
+ */
19
+ export function getCurrentUserId() {
20
+ return window.OC?.currentUser?.uid
21
+ || window.OC?.currentUser
22
+ || null
23
+ }
24
+
25
+ /**
26
+ * Fetch the current user's Nextcloud groups. Results are cached so the
27
+ * OCS API is only called once per page load.
28
+ *
29
+ * @return {Promise<string[]>} Array of group IDs the current user belongs to
30
+ */
31
+ export async function getCurrentUserGroups() {
32
+ const userId = getCurrentUserId()
33
+ if (!userId) {
34
+ return []
35
+ }
36
+
37
+ // Return cached result if we already fetched for this user
38
+ if (_cache.userId === userId && _cache.groups !== null) {
39
+ return _cache.groups
40
+ }
41
+
42
+ // If a fetch is already in progress for this user, await it
43
+ if (_cache.userId === userId && _cache.promise) {
44
+ return _cache.promise
45
+ }
46
+
47
+ _cache.userId = userId
48
+ _cache.promise = _fetchGroups(userId)
49
+
50
+ try {
51
+ _cache.groups = await _cache.promise
52
+ } catch {
53
+ _cache.groups = []
54
+ } finally {
55
+ _cache.promise = null
56
+ }
57
+
58
+ return _cache.groups
59
+ }
60
+
61
+ /**
62
+ * Internal: fetch groups from OCS API.
63
+ *
64
+ * @param {string} userId The user ID to look up
65
+ * @return {Promise<string[]>} Array of group IDs
66
+ */
67
+ async function _fetchGroups(userId) {
68
+ try {
69
+ const url = generateOcsUrl('/cloud/users/{userId}/groups', { userId })
70
+ const response = await axios.get(url)
71
+ const groups = response.data?.ocs?.data?.groups || []
72
+ return groups
73
+ } catch (error) {
74
+ console.error('[widgetVisibility] Failed to fetch user groups:', error)
75
+ return []
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check whether a single widget is visible to the current user based on
81
+ * its `visibility` configuration.
82
+ *
83
+ * Visibility rules:
84
+ * - No `visibility` property -> visible to everyone
85
+ * - `visibility.users` contains the current user ID -> visible
86
+ * - `visibility.groups` overlaps with the user's groups -> visible
87
+ * - Both `users` and `groups` are specified -> either match grants access (OR logic)
88
+ * - Both are empty arrays -> visible to everyone
89
+ *
90
+ * @param {object} widget Widget definition object
91
+ * @param {object} [widget.visibility] Optional visibility configuration
92
+ * @param {string[]} [widget.visibility.users] User IDs who can see this widget
93
+ * @param {string[]} [widget.visibility.groups] Group names who can see this widget
94
+ * @param {string|null} userId Current user ID
95
+ * @param {string[]} userGroups Current user's group memberships
96
+ * @return {boolean} Whether the widget should be visible
97
+ */
98
+ export function isWidgetVisible(widget, userId, userGroups) {
99
+ const visibility = widget?.visibility
100
+ if (!visibility) {
101
+ return true
102
+ }
103
+
104
+ const allowedUsers = visibility.users || []
105
+ const allowedGroups = visibility.groups || []
106
+
107
+ // If both are empty, visible to everyone
108
+ if (allowedUsers.length === 0 && allowedGroups.length === 0) {
109
+ return true
110
+ }
111
+
112
+ // Check user match
113
+ if (allowedUsers.length > 0 && userId && allowedUsers.includes(userId)) {
114
+ return true
115
+ }
116
+
117
+ // Check group match
118
+ if (allowedGroups.length > 0 && userGroups.length > 0) {
119
+ const hasGroupMatch = allowedGroups.some(group => userGroups.includes(group))
120
+ if (hasGroupMatch) {
121
+ return true
122
+ }
123
+ }
124
+
125
+ return false
126
+ }
127
+
128
+ /**
129
+ * Filter an array of widget definitions by visibility for the current user.
130
+ *
131
+ * This is an async function because it may need to fetch the user's groups
132
+ * from the OCS API (cached after first call).
133
+ *
134
+ * @param {Array} widgets Array of widget definition objects
135
+ * @return {Promise<Array>} Filtered array of visible widgets
136
+ */
137
+ export async function filterWidgetsByVisibility(widgets) {
138
+ if (!widgets || widgets.length === 0) {
139
+ return []
140
+ }
141
+
142
+ // Quick path: if no widgets have visibility config, return all
143
+ const hasVisibilityConfig = widgets.some(w => w.visibility)
144
+ if (!hasVisibilityConfig) {
145
+ return widgets
146
+ }
147
+
148
+ const userId = getCurrentUserId()
149
+ const userGroups = await getCurrentUserGroups()
150
+
151
+ return widgets.filter(widget => isWidgetVisible(widget, userId, userGroups))
152
+ }
153
+
154
+ /**
155
+ * Reset the internal groups cache. Useful for testing or when user
156
+ * context changes.
157
+ */
158
+ export function resetVisibilityCache() {
159
+ _cache.userId = null
160
+ _cache.groups = null
161
+ _cache.promise = null
162
+ }
@@ -1,235 +0,0 @@
1
- # ESLint Setup — How Red Lines and Auto-fix Work
2
-
3
- Exact files, exact settings, no guessing. This is the working setup in OpenRegister.
4
-
5
- ---
6
-
7
- ## Required VSCode Extension
8
-
9
- **`dbaeumer.vscode-eslint`** (version 3.0.24)
10
-
11
- This is the only extension involved in linting and formatting `.vue` and `.js` files. It does two things:
12
- 1. Runs ESLint in the background and draws red/yellow underlines (diagnostics)
13
- 2. Acts as the formatter — fixes the code when you save
14
-
15
- ---
16
-
17
- ## The 6 Files That Matter
18
-
19
- ### 1. `.vscode/settings.json`
20
-
21
- Tells VSCode to use ESLint as the formatter for all files (including `.vue`) and to run it on save.
22
-
23
- ```json
24
- {
25
- "editor.defaultFormatter": "dbaeumer.vscode-eslint",
26
- "editor.formatOnSave": true,
27
- "eslint.format.enable": true,
28
-
29
- "[javascript]": {
30
- "editor.defaultFormatter": "dbaeumer.vscode-eslint"
31
- },
32
- "[css]": {
33
- "editor.defaultFormatter": "esbenp.prettier-vscode"
34
- },
35
- "[php]": {
36
- "editor.defaultFormatter": "DEVSENSE.phptools-vscode"
37
- }
38
- }
39
- ```
40
-
41
- - No `[vue]` block exists. Vue falls back to the top-level `editor.defaultFormatter` = ESLint.
42
- - `eslint.format.enable: true` is what makes ESLint able to **fix** on save, not just show errors.
43
- - `editor.formatOnSave: true` triggers that fix every Ctrl+S.
44
-
45
- ---
46
-
47
- ### 2. `eslint.config.js`
48
-
49
- The ESLint flat config entry point. The extension finds this automatically at the project root.
50
-
51
- ```js
52
- const { defineConfig } = require('@eslint/config-helpers')
53
- const js = require('@eslint/js')
54
- const { FlatCompat } = require('@eslint/eslintrc')
55
-
56
- const compat = new FlatCompat({
57
- baseDirectory: __dirname,
58
- recommendedConfig: js.configs.recommended,
59
- allConfig: js.configs.all,
60
- })
61
-
62
- module.exports = defineConfig([{
63
- extends: compat.extends('@nextcloud'),
64
-
65
- settings: {
66
- 'import/resolver': {
67
- alias: {
68
- map: [
69
- ['@', './src'],
70
- ['@floating-ui/dom-actual', './node_modules/@floating-ui/dom'],
71
- ['@conduction/nextcloud-vue', '../nextcloud-vue/src'],
72
- ],
73
- extensions: ['.js', '.ts', '.vue', '.json', '.css'],
74
- },
75
- },
76
- },
77
-
78
- rules: {
79
- 'no-unused-vars': ['error', { varsIgnorePattern: '^(t|n)$', argsIgnorePattern: '^_' }],
80
- 'jsdoc/require-jsdoc': 'off',
81
- 'vue/first-attribute-linebreak': 'off',
82
- '@typescript-eslint/no-explicit-any': 'off',
83
- 'n/no-missing-import': 'off',
84
- 'import/namespace': 'off',
85
- 'import/default': 'off',
86
- 'import/no-named-as-default': 'off',
87
- 'import/no-named-as-default-member': 'off',
88
- },
89
- }])
90
- ```
91
-
92
- - Uses **flat config** format (ESLint 8.57.0).
93
- - Uses `FlatCompat` to bridge `@nextcloud/eslint-config` which is still in legacy format.
94
- - Works with `require()` because `package.json` has **no** `"type": "module"`.
95
-
96
- ---
97
-
98
- ### 3. `.babelrc`
99
-
100
- The Babel config used by `@babel/eslint-parser` to parse `.js` and `.vue` `<script>` blocks. This is a **project-root** `.babelrc` file, not a `babel.config.js`.
101
-
102
- ```json
103
- {
104
- "presets": [
105
- "@babel/preset-env",
106
- "@babel/preset-typescript"
107
- ],
108
- "plugins": [
109
- "@babel/plugin-transform-typescript"
110
- ]
111
- }
112
- ```
113
-
114
- - `@babel/preset-env` — handles modern JS syntax without a `targets` restriction, so it supports ES module syntax (`export default`, `import`, etc.).
115
- - `@babel/preset-typescript` + `@babel/plugin-transform-typescript` — allows TypeScript syntax in `.vue` `<script lang="ts">` blocks.
116
- - No `"type": "module"` in `package.json` means this `.babelrc` is loaded as JSON without any module format issues.
117
-
118
- ---
119
-
120
- ### 4. `node_modules/@nextcloud/eslint-config/index.js` (v8.4.1)
121
-
122
- Loaded by `compat.extends('@nextcloud')`. Combines three parts:
123
-
124
- ```js
125
- module.exports = {
126
- ...base, // parts/base.js
127
- overrides: [
128
- { ...typescriptOverrides }, // parts/typescript.js
129
- { ...vueOverrides }, // parts/vue.js
130
- ],
131
- }
132
- ```
133
-
134
- ---
135
-
136
- ### 5. `node_modules/@nextcloud/eslint-config/parts/base.js`
137
-
138
- Applies to all files. Contains the indent rules that cause **red lines in `<script>` blocks**.
139
-
140
- ```js
141
- parser: '@babel/eslint-parser',
142
- parserOptions: {
143
- requireConfigFile: false, // won't crash if no babel config — but .babelrc IS present and IS used
144
- },
145
-
146
- rules: {
147
- indent: ['error', 'tab'], // red line: spaces used instead of tabs
148
- 'no-tabs': ['error', { allowIndentationTabs: true }], // red line: tabs used outside indentation
149
- 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], // red line: mixed
150
- ...
151
- }
152
- ```
153
-
154
- `'error'` = **red underline**. `'warn'` = yellow. Auto-fix on save corrects all of these.
155
-
156
- ---
157
-
158
- ### 6. `node_modules/@nextcloud/eslint-config/parts/vue.js`
159
-
160
- Applies **only** to `**/*.vue` files. Contains the indent rule that causes **red lines in `<template>` blocks**.
161
-
162
- ```js
163
- files: ['**/*.vue'],
164
- parser: 'vue-eslint-parser', // parses the full .vue file structure
165
- parserOptions: {
166
- parser: '@babel/eslint-parser', // delegates <script> parsing to Babel (picks up .babelrc)
167
- },
168
- extends: ['plugin:vue/recommended'],
169
-
170
- rules: {
171
- 'vue/html-indent': ['error', 'tab'], // red line: wrong indentation in <template>
172
- 'vue/component-name-in-template-casing': ['error', 'PascalCase'],
173
- 'vue/html-closing-bracket-spacing': 'error',
174
- 'vue/html-closing-bracket-newline': ['error', { multiline: 'never' }],
175
- 'vue/max-attributes-per-line': ['error', { singleline: 3, multiline: 1 }],
176
- 'vue/multi-word-component-names': ['off'],
177
- ...
178
- }
179
- ```
180
-
181
- - `vue-eslint-parser` is what lets ESLint understand `<template>`, `<script>`, `<style>` as separate blocks.
182
- - `vue/html-indent: ['error', 'tab']` → red line on wrong indentation inside `<template>`.
183
- - `indent: ['error', 'tab']` from base.js → red line on wrong indentation inside `<script>`.
184
-
185
- ---
186
-
187
- ## The Full Chain
188
-
189
- ```
190
- You open a .vue file in VSCode
191
-
192
-
193
- dbaeumer.vscode-eslint extension starts
194
- reads .vscode/settings.json
195
- → editor.defaultFormatter = ESLint for .vue (no [vue] override, falls back to global)
196
- → eslint.format.enable = true (ESLint can fix, not just report)
197
-
198
-
199
- ESLint loads eslint.config.js
200
- → FlatCompat bridges @nextcloud/eslint-config (legacy → flat)
201
- → index.js loads base.js + vue.js
202
-
203
-
204
- For <template> block:
205
- vue-eslint-parser handles the HTML
206
- Rule: vue/html-indent ['error', 'tab']
207
- Wrong indent → RED UNDERLINE
208
-
209
- For <script> block:
210
- @babel/eslint-parser handles the JS
211
- Picks up .babelrc (preset-env + preset-typescript)
212
- Rule: indent ['error', 'tab']
213
- Wrong indent → RED UNDERLINE
214
-
215
-
216
- You press Ctrl+S
217
- editor.formatOnSave triggers ESLint formatter
218
- ESLint runs --fix internally
219
- → replaces wrong indentation with tabs in both blocks
220
- → fixes bracket spacing, trailing commas, etc.
221
- File saved with corrected formatting
222
- ```
223
-
224
- ---
225
-
226
- ## Key Reason This Works and nextcloud-vue Does Not
227
-
228
- | | OpenRegister | nextcloud-vue |
229
- |---|---|---|
230
- | `package.json` `"type"` field | not set (defaults to CommonJS) | `"type": "module"` |
231
- | `eslint.config.js` uses `require()` | works fine | **crashes** — `require` not defined in ES module scope |
232
- | Babel config file | `.babelrc` (JSON, no module issues) | `babel.config.js` (treated as ESM, crashes Babel sync load) |
233
- | ESLint server in VSCode | starts, lints, shows red lines | hangs on startup, times out, shows nothing |
234
-
235
- The fix for nextcloud-vue is to rename both files to `.cjs` so Node treats them as CommonJS regardless of `"type": "module"`, and ensure `babel.config.cjs` uses `module.exports` (not `export default`) with `sourceType: 'unambiguous'` at the top level.