@conduction/nextcloud-vue 0.1.0-beta.14 → 0.1.0-beta.16
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.
- package/dist/nextcloud-vue.cjs.js +7282 -3443
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +719 -100
- package/dist/nextcloud-vue.esm.js +7120 -3300
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +3 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +36 -3
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +34 -19
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +312 -36
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +983 -64
- package/src/components/CnAdvancedFormDialog/index.js +3 -0
- package/src/components/CnAppLoading/CnAppLoading.vue +93 -0
- package/src/components/CnAppLoading/index.js +3 -0
- package/src/components/CnAppNav/CnAppNav.vue +269 -0
- package/src/components/CnAppNav/index.js +3 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +201 -0
- package/src/components/CnAppRoot/index.js +3 -0
- package/src/components/CnColorPicker/CnColorPicker.vue +251 -0
- package/src/components/CnColorPicker/index.js +1 -0
- package/src/components/CnContextMenu/CnContextMenu.vue +41 -4
- package/src/components/CnDashboardPage/CnDashboardPage.vue +8 -0
- package/src/components/CnDependencyMissing/CnDependencyMissing.vue +152 -0
- package/src/components/CnDependencyMissing/index.js +3 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +27 -16
- package/src/components/CnIndexPage/CnIndexPage.vue +36 -6
- package/src/components/CnPageRenderer/CnPageRenderer.vue +278 -0
- package/src/components/CnPageRenderer/index.js +4 -0
- package/src/components/CnPageRenderer/pageTypes.js +37 -0
- package/src/components/CnRowActions/CnRowActions.vue +44 -3
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +4 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +103 -74
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +30 -2
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +16 -12
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +9 -4
- package/src/components/index.js +7 -1
- package/src/composables/index.js +2 -0
- package/src/composables/useAppManifest.js +115 -0
- package/src/composables/useAppStatus.js +107 -0
- package/src/css/CnSchemaFormDialog.css +22 -0
- package/src/index.js +24 -2
- package/src/schemas/app-manifest.schema.json +153 -0
- package/src/types/index.d.ts +9 -0
- package/src/types/manifest.d.ts +88 -0
- package/src/utils/index.js +1 -1
- package/src/utils/schema.js +157 -2
- package/src/utils/validateManifest.js +113 -0
|
@@ -36,13 +36,19 @@
|
|
|
36
36
|
@click.stop />
|
|
37
37
|
</div>
|
|
38
38
|
<div v-else class="cn-schema-form__name-display-container">
|
|
39
|
-
<
|
|
39
|
+
<LockOutline v-if="row._inherited"
|
|
40
|
+
:size="16"
|
|
41
|
+
class="cn-schema-form__lock-icon"
|
|
42
|
+
:title="t('nextcloud-vue', 'Inherited from parent schema (read-only)')" />
|
|
43
|
+
<AlertOutline v-else-if="isPropertyModified(row._key)"
|
|
40
44
|
:size="16"
|
|
41
45
|
class="cn-schema-form__warning-icon"
|
|
42
46
|
:title="t('nextcloud-vue', 'Property has been modified. Changes will only take effect after the schema is saved.')" />
|
|
43
47
|
<div class="cn-schema-form__name-with-chips">
|
|
44
48
|
<span class="cn-schema-form__property-name">{{ row._key }}</span>
|
|
45
49
|
<div class="cn-schema-form__inline-chips">
|
|
50
|
+
<span v-if="row._inherited"
|
|
51
|
+
class="cn-schema-form__property-chip cn-schema-form__chip-inherited">{{ t('nextcloud-vue', 'Inherited') }}</span>
|
|
46
52
|
<span v-if="isPropertyRequired(schema, row._key)"
|
|
47
53
|
class="cn-schema-form__property-chip cn-schema-form__chip-primary">{{ t('nextcloud-vue', 'Required') }}</span>
|
|
48
54
|
<span v-if="row.immutable"
|
|
@@ -80,6 +86,7 @@
|
|
|
80
86
|
|
|
81
87
|
<template #row-actions="{ row }">
|
|
82
88
|
<CnSchemaPropertyActions
|
|
89
|
+
v-if="!row._inherited"
|
|
83
90
|
:property-key="row._key"
|
|
84
91
|
:property="schema.properties[row._key]"
|
|
85
92
|
:schema-item="schema"
|
|
@@ -111,6 +118,7 @@ import CnSchemaPropertyActions from './CnSchemaPropertyActions.vue'
|
|
|
111
118
|
|
|
112
119
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
113
120
|
import AlertOutline from 'vue-material-design-icons/AlertOutline.vue'
|
|
121
|
+
import LockOutline from 'vue-material-design-icons/LockOutline.vue'
|
|
114
122
|
|
|
115
123
|
/**
|
|
116
124
|
* CnSchemaPropertiesTab — Properties table tab for CnSchemaFormDialog.
|
|
@@ -135,6 +143,7 @@ export default {
|
|
|
135
143
|
CnSchemaPropertyActions,
|
|
136
144
|
Plus,
|
|
137
145
|
AlertOutline,
|
|
146
|
+
LockOutline,
|
|
138
147
|
},
|
|
139
148
|
props: {
|
|
140
149
|
/** The full schema item (needs .properties, .required) */
|
|
@@ -161,6 +170,8 @@ export default {
|
|
|
161
170
|
sortedUserGroups: { type: Array, default: () => [] },
|
|
162
171
|
/** Whether groups are loading */
|
|
163
172
|
loadingGroups: { type: Boolean, default: false },
|
|
173
|
+
/** Properties inherited from parent schemas (allOf) — shown as locked/read-only rows */
|
|
174
|
+
inheritedProperties: { type: Object, default: () => ({}) },
|
|
164
175
|
},
|
|
165
176
|
data() {
|
|
166
177
|
return {
|
|
@@ -195,11 +206,24 @@ export default {
|
|
|
195
206
|
})
|
|
196
207
|
},
|
|
197
208
|
propertyRows() {
|
|
198
|
-
|
|
209
|
+
const ownProperties = this.schema.properties || {}
|
|
210
|
+
const inheritedRows = Object.entries(this.inheritedProperties || {})
|
|
211
|
+
.filter(([key]) => !(key in ownProperties))
|
|
212
|
+
.map(([key, prop]) => ({
|
|
213
|
+
_id: `inherited_${key}`,
|
|
214
|
+
_key: key,
|
|
215
|
+
_inherited: true,
|
|
216
|
+
...prop,
|
|
217
|
+
}))
|
|
218
|
+
|
|
219
|
+
const ownRows = this.sortedProperties.map(([key, prop]) => ({
|
|
199
220
|
_id: this.getStablePropertyId(key),
|
|
200
221
|
_key: key,
|
|
222
|
+
_inherited: false,
|
|
201
223
|
...prop,
|
|
202
224
|
}))
|
|
225
|
+
|
|
226
|
+
return [...inheritedRows, ...ownRows]
|
|
203
227
|
},
|
|
204
228
|
},
|
|
205
229
|
watch: {
|
|
@@ -258,6 +282,9 @@ export default {
|
|
|
258
282
|
},
|
|
259
283
|
|
|
260
284
|
getRowClass(row) {
|
|
285
|
+
if (row._inherited) {
|
|
286
|
+
return 'cn-schema-form__inherited-row'
|
|
287
|
+
}
|
|
261
288
|
const classes = []
|
|
262
289
|
if (this.selectedProperty === row._key) {
|
|
263
290
|
classes.push('cn-schema-form__selected-row')
|
|
@@ -273,6 +300,7 @@ export default {
|
|
|
273
300
|
},
|
|
274
301
|
|
|
275
302
|
onRowClick(row) {
|
|
303
|
+
if (row._inherited) return
|
|
276
304
|
if (this.selectedProperty === row._key) return
|
|
277
305
|
this.$emit('update:selected-property', row._key)
|
|
278
306
|
},
|
|
@@ -265,6 +265,7 @@
|
|
|
265
265
|
|
|
266
266
|
<script>
|
|
267
267
|
import _ from 'lodash'
|
|
268
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
268
269
|
import {
|
|
269
270
|
NcButton,
|
|
270
271
|
NcCheckboxRadioSwitch,
|
|
@@ -316,6 +317,8 @@ export default {
|
|
|
316
317
|
hasAnyPermissions: { type: Boolean, default: false },
|
|
317
318
|
/** Whether schema has restrictive permissions */
|
|
318
319
|
isRestrictiveSchema: { type: Boolean, default: false },
|
|
320
|
+
/** Properties inherited from parent schemas (allOf) */
|
|
321
|
+
inheritedProperties: { type: Object, default: () => ({}) },
|
|
319
322
|
},
|
|
320
323
|
data() {
|
|
321
324
|
return {
|
|
@@ -352,10 +355,10 @@ export default {
|
|
|
352
355
|
},
|
|
353
356
|
|
|
354
357
|
propertyOptions() {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}))
|
|
358
|
+
const ownKeys = Object.keys(this.schemaItem.properties || {}).filter(k => k !== '')
|
|
359
|
+
const inheritedKeys = Object.keys(this.inheritedProperties || {}).filter(k => k !== '')
|
|
360
|
+
const allKeys = [...new Set([...inheritedKeys, ...ownKeys])]
|
|
361
|
+
const schemaProps = allKeys.map(key => ({ id: key, label: key }))
|
|
359
362
|
const systemProps = [
|
|
360
363
|
{ id: '_organisation', label: t('nextcloud-vue', '_organisation (system)') },
|
|
361
364
|
{ id: '_owner', label: t('nextcloud-vue', '_owner (system)') },
|
|
@@ -396,6 +399,7 @@ export default {
|
|
|
396
399
|
},
|
|
397
400
|
},
|
|
398
401
|
methods: {
|
|
402
|
+
t,
|
|
399
403
|
capitalize: _.capitalize,
|
|
400
404
|
|
|
401
405
|
availablePropertyOptions(action, ruleIdx) {
|
|
@@ -510,10 +514,11 @@ export default {
|
|
|
510
514
|
},
|
|
511
515
|
|
|
512
516
|
removeCondition(action, originalIndex, propKey) {
|
|
513
|
-
const
|
|
514
|
-
if (match)
|
|
515
|
-
|
|
516
|
-
|
|
517
|
+
const rule = this.schema.authorization[action][originalIndex]
|
|
518
|
+
if (!rule.match) return
|
|
519
|
+
const updated = { ...rule.match }
|
|
520
|
+
delete updated[propKey]
|
|
521
|
+
this.$set(rule, 'match', updated)
|
|
517
522
|
},
|
|
518
523
|
|
|
519
524
|
// ─── Add-condition form state ─────────────────────────────────────
|
|
@@ -581,10 +586,9 @@ export default {
|
|
|
581
586
|
if (!conditionValue && conditionValue !== false) return
|
|
582
587
|
|
|
583
588
|
const rule = this.schema.authorization[action][originalIndex]
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
this.$set(rule.match, property, { [operator]: conditionValue })
|
|
589
|
+
// Replace the entire match object so Vue 2's property-level dep on `rule.match`
|
|
590
|
+
// fires and the condition table v-for updates correctly.
|
|
591
|
+
this.$set(rule, 'match', { ...(rule.match || {}), [property]: { [operator]: conditionValue } })
|
|
588
592
|
|
|
589
593
|
this.cancelAddCondition()
|
|
590
594
|
},
|
|
@@ -146,8 +146,9 @@ export default {
|
|
|
146
146
|
validator: (tabs) => tabs.length > 0 && tabs.every(t => t.id && t.title),
|
|
147
147
|
},
|
|
148
148
|
/**
|
|
149
|
-
* Existing item for edit mode. Pass null or undefined for create
|
|
150
|
-
*
|
|
149
|
+
* Existing item for edit mode. Pass null or undefined for plain create
|
|
150
|
+
* mode. A non-null object without an `id` is also treated as create mode
|
|
151
|
+
* — useful for pre-filled payloads (extend, clone, duplicate flows).
|
|
151
152
|
*
|
|
152
153
|
* @type {object|null}
|
|
153
154
|
*/
|
|
@@ -287,12 +288,16 @@ export default {
|
|
|
287
288
|
},
|
|
288
289
|
computed: {
|
|
289
290
|
/**
|
|
290
|
-
* Whether the dialog is in create mode (no existing item).
|
|
291
|
+
* Whether the dialog is in create mode (no existing persisted item).
|
|
292
|
+
*
|
|
293
|
+
* An item is considered "existing" only when it has an `id`. A non-null
|
|
294
|
+
* item without an `id` (e.g. a pre-filled payload for extend/clone flows)
|
|
295
|
+
* is treated as create mode.
|
|
291
296
|
*
|
|
292
297
|
* @return {boolean}
|
|
293
298
|
*/
|
|
294
299
|
isCreateMode() {
|
|
295
|
-
return !this.item
|
|
300
|
+
return !this.item || !this.item.id
|
|
296
301
|
},
|
|
297
302
|
/**
|
|
298
303
|
* Resolved dialog title. Uses dialogTitle prop if provided,
|
package/src/components/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export { CnMassActionBar } from './CnMassActionBar/index.js'
|
|
|
18
18
|
export { CnDeleteDialog } from './CnDeleteDialog/index.js'
|
|
19
19
|
export { CnCopyDialog } from './CnCopyDialog/index.js'
|
|
20
20
|
export { CnFormDialog } from './CnFormDialog/index.js'
|
|
21
|
-
export { CnAdvancedFormDialog } from './CnAdvancedFormDialog/index.js'
|
|
21
|
+
export { CnAdvancedFormDialog, CnPropertiesTab, CnMetadataTab, CnPropertyValueCell } from './CnAdvancedFormDialog/index.js'
|
|
22
22
|
export { CnMassDeleteDialog } from './CnMassDeleteDialog/index.js'
|
|
23
23
|
export { CnMassCopyDialog } from './CnMassCopyDialog/index.js'
|
|
24
24
|
export { CnKpiGrid } from './CnKpiGrid/index.js'
|
|
@@ -46,6 +46,7 @@ export { CnDetailCard } from './CnDetailCard/index.js'
|
|
|
46
46
|
export { CnCard } from './CnCard/index.js'
|
|
47
47
|
export { CnStatsPanel } from './CnStatsPanel/index.js'
|
|
48
48
|
export { CnJsonViewer } from './CnJsonViewer/index.js'
|
|
49
|
+
export { CnColorPicker } from './CnColorPicker/index.js'
|
|
49
50
|
export { CnDetailGrid } from './CnDetailGrid/index.js'
|
|
50
51
|
export { CnProgressBar } from './CnProgressBar/index.js'
|
|
51
52
|
export { CnChartWidget } from './CnChartWidget/index.js'
|
|
@@ -55,3 +56,8 @@ export { CnTableWidget } from './CnTableWidget/index.js'
|
|
|
55
56
|
export { CnNoteCard } from './CnNoteCard/index.js'
|
|
56
57
|
export { CnObjectDataWidget } from './CnObjectDataWidget/index.js'
|
|
57
58
|
export { CnObjectMetadataWidget } from './CnObjectMetadataWidget/index.js'
|
|
59
|
+
export { CnPageRenderer, defaultPageTypes } from './CnPageRenderer/index.js'
|
|
60
|
+
export { CnAppNav } from './CnAppNav/index.js'
|
|
61
|
+
export { CnAppLoading } from './CnAppLoading/index.js'
|
|
62
|
+
export { CnDependencyMissing } from './CnDependencyMissing/index.js'
|
|
63
|
+
export { CnAppRoot } from './CnAppRoot/index.js'
|
package/src/composables/index.js
CHANGED
|
@@ -3,3 +3,5 @@ export { useDetailView } from './useDetailView.js'
|
|
|
3
3
|
export { useSubResource } from './useSubResource.js'
|
|
4
4
|
export { useDashboardView } from './useDashboardView.js'
|
|
5
5
|
export { useContextMenu } from './useContextMenu.js'
|
|
6
|
+
export { useAppManifest } from './useAppManifest.js'
|
|
7
|
+
export { useAppStatus } from './useAppStatus.js'
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import axios from '@nextcloud/axios'
|
|
3
|
+
import { generateUrl } from '@nextcloud/router'
|
|
4
|
+
import { validateManifest } from '../utils/validateManifest.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Composable that loads and validates a Conduction app manifest.
|
|
8
|
+
*
|
|
9
|
+
* The composable implements the three-phase flow specified in
|
|
10
|
+
* REQ-JMR-002 of the json-manifest-renderer capability:
|
|
11
|
+
*
|
|
12
|
+
* 1. Synchronous bundled load — `bundledManifest` is the immediate value.
|
|
13
|
+
* 2. Async backend merge — fetches `/index.php/apps/{appId}/api/manifest`
|
|
14
|
+
* and deep-merges any 200 response over the bundled manifest. 4xx /
|
|
15
|
+
* 5xx / network errors are silently ignored so apps work without a
|
|
16
|
+
* backend endpoint.
|
|
17
|
+
* 3. Validation — the merged result is validated against
|
|
18
|
+
* `app-manifest.schema.json`. On failure, the bundled manifest is
|
|
19
|
+
* kept and a `console.warn` is emitted with the error list.
|
|
20
|
+
*
|
|
21
|
+
* The returned manifest is reactive, so the future "app builder" backend
|
|
22
|
+
* can hot-swap the manifest without a page reload.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} appId Nextcloud app ID. Used to build the default
|
|
25
|
+
* backend endpoint URL via `@nextcloud/router`.
|
|
26
|
+
* @param {object} bundledManifest The manifest shipped with the app (the
|
|
27
|
+
* default value, available synchronously).
|
|
28
|
+
* @param {object} [options] Configuration options.
|
|
29
|
+
* @param {string} [options.endpoint] Override the backend fetch URL.
|
|
30
|
+
* Useful for tests and alternative-host deployments.
|
|
31
|
+
* @param {Function} [options.fetcher] Override the fetch function. Must
|
|
32
|
+
* return a promise resolving to `{ status: number, data: object }`.
|
|
33
|
+
* Defaults to `axios.get` from `@nextcloud/axios` (which inherits the
|
|
34
|
+
* Nextcloud CSRF token automatically).
|
|
35
|
+
* @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null> }}
|
|
36
|
+
*
|
|
37
|
+
* @example Basic usage (Composition API)
|
|
38
|
+
* const { manifest, isLoading } = useAppManifest('decidesk', bundled)
|
|
39
|
+
*
|
|
40
|
+
* @example Inside an Options API component
|
|
41
|
+
* export default {
|
|
42
|
+
* setup() {
|
|
43
|
+
* return useAppManifest('decidesk', bundled)
|
|
44
|
+
* },
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* @example Custom endpoint and fetcher (e.g. for tests)
|
|
48
|
+
* useAppManifest('decidesk', bundled, {
|
|
49
|
+
* endpoint: '/custom/manifest/url',
|
|
50
|
+
* fetcher: (url) => Promise.resolve({ status: 200, data: { ... } }),
|
|
51
|
+
* })
|
|
52
|
+
*/
|
|
53
|
+
export function useAppManifest(appId, bundledManifest, options = {}) {
|
|
54
|
+
const manifest = ref(bundledManifest)
|
|
55
|
+
const isLoading = ref(true)
|
|
56
|
+
const validationErrors = ref(null)
|
|
57
|
+
|
|
58
|
+
const endpoint = options.endpoint ?? generateUrl(`/apps/${appId}/api/manifest`)
|
|
59
|
+
const fetcher = options.fetcher ?? ((url) => axios.get(url))
|
|
60
|
+
|
|
61
|
+
;(async () => {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetcher(endpoint)
|
|
64
|
+
if (!response || response.status !== 200 || !response.data) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const merged = deepMerge(bundledManifest, response.data)
|
|
68
|
+
const result = validateManifest(merged)
|
|
69
|
+
if (!result.valid) {
|
|
70
|
+
validationErrors.value = result.errors
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.warn(
|
|
73
|
+
'[useAppManifest] Backend manifest failed schema validation; falling back to bundled manifest.',
|
|
74
|
+
result.errors,
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
manifest.value = merged
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Silent fallback on 404, network errors, non-200 responses.
|
|
81
|
+
// Apps without a backend endpoint should keep working.
|
|
82
|
+
} finally {
|
|
83
|
+
isLoading.value = false
|
|
84
|
+
}
|
|
85
|
+
})()
|
|
86
|
+
|
|
87
|
+
return { manifest, isLoading, validationErrors }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deep-merge `source` into `target`, returning a new object. Plain
|
|
92
|
+
* objects are merged recursively; arrays are replaced (not concatenated)
|
|
93
|
+
* to match typical deep-merge semantics expected by manifest overrides.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} target Base object.
|
|
96
|
+
* @param {object} source Object whose values take precedence.
|
|
97
|
+
* @return {object} New merged object.
|
|
98
|
+
*/
|
|
99
|
+
function deepMerge(target, source) {
|
|
100
|
+
if (!isPlainObject(target)) return source
|
|
101
|
+
if (!isPlainObject(source)) return source
|
|
102
|
+
const out = { ...target }
|
|
103
|
+
for (const key of Object.keys(source)) {
|
|
104
|
+
if (isPlainObject(source[key]) && isPlainObject(target[key])) {
|
|
105
|
+
out[key] = deepMerge(target[key], source[key])
|
|
106
|
+
} else {
|
|
107
|
+
out[key] = source[key]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isPlainObject(value) {
|
|
114
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
115
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { getCapabilities } from '@nextcloud/capabilities'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-`appId` cache of status results. Populated lazily on first call.
|
|
6
|
+
*
|
|
7
|
+
* The Vue refs themselves are stored in the cache; subsequent calls for
|
|
8
|
+
* the same `appId` return the same refs so all consumers share state.
|
|
9
|
+
*
|
|
10
|
+
* Module-level lifetime — survives until the page is reloaded.
|
|
11
|
+
*/
|
|
12
|
+
const cache = new Map()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Composable that reports whether a given Nextcloud app is installed
|
|
16
|
+
* and enabled.
|
|
17
|
+
*
|
|
18
|
+
* Implements REQ-JMR-012 of the json-manifest-renderer capability:
|
|
19
|
+
* - generic over `appId` so callers can check `openregister`,
|
|
20
|
+
* `opencatalogi`, or any other Nextcloud app
|
|
21
|
+
* - results are cached per `appId` for the page lifetime; repeated
|
|
22
|
+
* calls reuse the same refs without re-checking
|
|
23
|
+
* - on error the composable falls back to `{ installed: false,
|
|
24
|
+
* enabled: false }` and logs a `console.warn`, so a failed
|
|
25
|
+
* lookup never crashes the app shell
|
|
26
|
+
*
|
|
27
|
+
* Detection order:
|
|
28
|
+
* 1. `OC.appswebroots[appId]` — a global object Nextcloud injects on
|
|
29
|
+
* every authenticated page load that contains a key for every app
|
|
30
|
+
* enabled for the current user. This is the most reliable signal:
|
|
31
|
+
* it doesn't require the target app to opt into the capabilities
|
|
32
|
+
* API, and it reflects per-user enablement.
|
|
33
|
+
* 2. `getCapabilities()[appId]` — falls back to the capabilities
|
|
34
|
+
* bootstrap when `OC` is not available (rare; mostly for tests).
|
|
35
|
+
* Apps that implement `\OCP\Capabilities\ICapability` advertise
|
|
36
|
+
* themselves here.
|
|
37
|
+
*
|
|
38
|
+
* Most Conduction / OpenRegister-backed apps do NOT register a
|
|
39
|
+
* capability key, which is why the appswebroots check has to come
|
|
40
|
+
* first — capabilities alone would report every Conduction app as
|
|
41
|
+
* "missing" even when they are installed and enabled.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} appId Nextcloud app id (e.g. `"openregister"`).
|
|
44
|
+
* @return {{ installed: import('vue').Ref<boolean>, enabled: import('vue').Ref<boolean>, loading: import('vue').Ref<boolean> }}
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const { installed, enabled, loading } = useAppStatus('openregister')
|
|
48
|
+
* // After loading.value flips to false, installed.value is the answer.
|
|
49
|
+
*/
|
|
50
|
+
export function useAppStatus(appId) {
|
|
51
|
+
if (cache.has(appId)) {
|
|
52
|
+
return cache.get(appId)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const installed = ref(false)
|
|
56
|
+
const enabled = ref(false)
|
|
57
|
+
const loading = ref(true)
|
|
58
|
+
|
|
59
|
+
const result = { installed, enabled, loading }
|
|
60
|
+
cache.set(appId, result)
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Primary check: the global `OC.appswebroots` map. NC populates
|
|
64
|
+
// this with one key per app enabled for the current user. Tests
|
|
65
|
+
// running outside a real Nextcloud page won't have it, in which
|
|
66
|
+
// case we fall through to the capabilities check.
|
|
67
|
+
const ocAppsWebRoots = (typeof OC !== 'undefined' && OC && OC.appswebroots) || null
|
|
68
|
+
if (ocAppsWebRoots && Object.prototype.hasOwnProperty.call(ocAppsWebRoots, appId)) {
|
|
69
|
+
installed.value = true
|
|
70
|
+
enabled.value = true
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Secondary check: capabilities API. Only apps that implement
|
|
75
|
+
// `ICapability` advertise themselves here.
|
|
76
|
+
const capabilities = getCapabilities()
|
|
77
|
+
if (
|
|
78
|
+
capabilities
|
|
79
|
+
&& typeof capabilities === 'object'
|
|
80
|
+
&& Object.prototype.hasOwnProperty.call(capabilities, appId)
|
|
81
|
+
) {
|
|
82
|
+
installed.value = true
|
|
83
|
+
enabled.value = true
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.warn(
|
|
88
|
+
`[useAppStatus] Failed to determine status for "${appId}":`,
|
|
89
|
+
err,
|
|
90
|
+
)
|
|
91
|
+
// installed and enabled stay false
|
|
92
|
+
} finally {
|
|
93
|
+
loading.value = false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Test-only helper to reset the per-`appId` cache. Not exported from
|
|
101
|
+
* the package barrel — only the test suite imports it directly.
|
|
102
|
+
*
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
export function __resetAppStatusCacheForTests() {
|
|
106
|
+
cache.clear()
|
|
107
|
+
}
|
|
@@ -173,6 +173,17 @@
|
|
|
173
173
|
border-left: 3px solid var(--color-primary);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
.cn-schema-form__viewTable tbody tr.cn-schema-form__inherited-row {
|
|
177
|
+
background-color: var(--color-background-dark);
|
|
178
|
+
border-left: 3px solid var(--color-text-maxcontrast);
|
|
179
|
+
cursor: default;
|
|
180
|
+
opacity: 0.8;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.cn-schema-form__viewTable tbody tr.cn-schema-form__inherited-row:hover {
|
|
184
|
+
background-color: var(--color-background-dark);
|
|
185
|
+
}
|
|
186
|
+
|
|
176
187
|
/* Form editor */
|
|
177
188
|
.cn-schema-form__form-editor {
|
|
178
189
|
display: flex;
|
|
@@ -193,6 +204,11 @@
|
|
|
193
204
|
flex-shrink: 0;
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
.cn-schema-form__lock-icon {
|
|
208
|
+
color: var(--color-text-maxcontrast);
|
|
209
|
+
flex-shrink: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
196
212
|
/* Table column widths */
|
|
197
213
|
.cn-schema-form__tableColumnActions {
|
|
198
214
|
width: 150px;
|
|
@@ -512,6 +528,12 @@
|
|
|
512
528
|
color: var(--color-success-text);
|
|
513
529
|
}
|
|
514
530
|
|
|
531
|
+
.cn-schema-form__chip-inherited {
|
|
532
|
+
background-color: var(--color-background-dark);
|
|
533
|
+
color: var(--color-text-maxcontrast);
|
|
534
|
+
border: 1px solid var(--color-border);
|
|
535
|
+
}
|
|
536
|
+
|
|
515
537
|
.cn-schema-form__properties-warning {
|
|
516
538
|
margin-top: 15px;
|
|
517
539
|
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// CSS — auto-imported so consumers get styles with components
|
|
2
2
|
import './css/index.css'
|
|
3
3
|
|
|
4
|
+
// Re-export every Nc* component from @nextcloud/vue so consumer apps
|
|
5
|
+
// can import all Nextcloud-Vue + Conduction components from a single
|
|
6
|
+
// barrel, per ADR-004: "NEVER import from @nextcloud/vue directly —
|
|
7
|
+
// use @conduction/nextcloud-vue which re-exports all". Wildcard form
|
|
8
|
+
// is intentional so the barrel stays in sync without per-component
|
|
9
|
+
// edits when @nextcloud/vue adds new components. check-docs.js skips
|
|
10
|
+
// `export *` (its regex only matches the named-export form), so no
|
|
11
|
+
// docs are required for these pass-through re-exports — the source
|
|
12
|
+
// of truth is the upstream @nextcloud/vue documentation.
|
|
13
|
+
export * from '@nextcloud/vue'
|
|
14
|
+
|
|
4
15
|
// Components
|
|
5
16
|
export {
|
|
6
17
|
CnDataTable,
|
|
@@ -24,6 +35,9 @@ export {
|
|
|
24
35
|
CnCopyDialog,
|
|
25
36
|
CnFormDialog,
|
|
26
37
|
CnAdvancedFormDialog,
|
|
38
|
+
CnPropertiesTab,
|
|
39
|
+
CnMetadataTab,
|
|
40
|
+
CnPropertyValueCell,
|
|
27
41
|
CnMassDeleteDialog,
|
|
28
42
|
CnMassCopyDialog,
|
|
29
43
|
CnKpiGrid,
|
|
@@ -48,6 +62,7 @@ export {
|
|
|
48
62
|
CnCard,
|
|
49
63
|
CnStatsPanel,
|
|
50
64
|
CnJsonViewer,
|
|
65
|
+
CnColorPicker,
|
|
51
66
|
CnDetailGrid,
|
|
52
67
|
CnProgressBar,
|
|
53
68
|
CnChartWidget,
|
|
@@ -60,6 +75,12 @@ export {
|
|
|
60
75
|
CnNoteCard,
|
|
61
76
|
CnObjectDataWidget,
|
|
62
77
|
CnObjectMetadataWidget,
|
|
78
|
+
CnPageRenderer,
|
|
79
|
+
defaultPageTypes,
|
|
80
|
+
CnAppNav,
|
|
81
|
+
CnAppLoading,
|
|
82
|
+
CnDependencyMissing,
|
|
83
|
+
CnAppRoot,
|
|
63
84
|
registerIcons,
|
|
64
85
|
} from './components/index.js'
|
|
65
86
|
|
|
@@ -84,12 +105,13 @@ export {
|
|
|
84
105
|
} from './store/plugins/index.js'
|
|
85
106
|
|
|
86
107
|
// Composables
|
|
87
|
-
export { useListView, useDetailView, useSubResource, useDashboardView, useContextMenu } from './composables/index.js'
|
|
108
|
+
export { useListView, useDetailView, useSubResource, useDashboardView, useContextMenu, useAppManifest, useAppStatus } from './composables/index.js'
|
|
88
109
|
|
|
89
110
|
// Localization
|
|
90
111
|
export { registerTranslations } from './l10n/index.js'
|
|
91
112
|
|
|
92
113
|
// Utilities
|
|
93
114
|
export { buildHeaders, buildQueryString, parseResponseError, networkError, genericError } from './utils/index.js'
|
|
94
|
-
export { columnsFromSchema, formatValue, filtersFromSchema, fieldsFromSchema } from './utils/index.js'
|
|
115
|
+
export { columnsFromSchema, formatValue, filtersFromSchema, fieldsFromSchema, validateValue } from './utils/index.js'
|
|
116
|
+
export { validateManifest } from './utils/validateManifest.js'
|
|
95
117
|
export { filterWidgetsByVisibility, isWidgetVisible, getCurrentUserId, getCurrentUserGroups, resetVisibilityCache } from './utils/index.js'
|