@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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NcPopover
|
|
3
|
+
:shown.sync="open"
|
|
4
|
+
:disabled="disabled"
|
|
5
|
+
:triggers="[]"
|
|
6
|
+
popup-role="dialog"
|
|
7
|
+
popover-base-class="cn-color-picker__popper">
|
|
8
|
+
<template #trigger>
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
class="cn-color-picker__swatch"
|
|
12
|
+
:class="{ 'cn-color-picker__swatch--disabled': disabled }"
|
|
13
|
+
:style="swatchStyle"
|
|
14
|
+
:disabled="disabled"
|
|
15
|
+
:title="t('nextcloud-vue', 'Open color picker')"
|
|
16
|
+
:aria-label="t('nextcloud-vue', 'Open color picker')"
|
|
17
|
+
@click="open = !open" />
|
|
18
|
+
</template>
|
|
19
|
+
<ChromeColorPicker
|
|
20
|
+
ref="picker"
|
|
21
|
+
v-bind="$attrs"
|
|
22
|
+
class="cn-color-picker__chrome"
|
|
23
|
+
:class="{ 'cn-color-picker__chrome--locked-mode': mode !== null }"
|
|
24
|
+
:value="value"
|
|
25
|
+
v-on="$listeners" />
|
|
26
|
+
</NcPopover>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
31
|
+
import { NcPopover } from '@nextcloud/vue'
|
|
32
|
+
import { Chrome as ChromeColorPicker } from 'vue-color'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* CnColorPicker — A swatch-button trigger that opens a themed `Chrome` color
|
|
36
|
+
* picker (vue-color) inside a popover.
|
|
37
|
+
*
|
|
38
|
+
* The active color is shown as a square swatch (with a checker pattern behind
|
|
39
|
+
* it so alpha colors render correctly). Clicking the swatch toggles the
|
|
40
|
+
* popover. All props and listeners except `value` and `disabled` are
|
|
41
|
+
* forwarded to the underlying `Chrome` picker, so the full vue-color API
|
|
42
|
+
* stays available (`disable-alpha`, `disable-fields`, `@input`, etc.).
|
|
43
|
+
*
|
|
44
|
+
* The `value` prop accepts any of vue-color's color formats: a CSS color
|
|
45
|
+
* string (`'#abcdef'`, `'rgba(...)'`, ...) or a color object.
|
|
46
|
+
*
|
|
47
|
+
* @event input Forwarded from `Chrome`. Payload: vue-color color object
|
|
48
|
+
* `{ hex, hex8, rgba, hsl, hsv, a, source }`.
|
|
49
|
+
*/
|
|
50
|
+
export default {
|
|
51
|
+
name: 'CnColorPicker',
|
|
52
|
+
|
|
53
|
+
components: {
|
|
54
|
+
NcPopover,
|
|
55
|
+
ChromeColorPicker,
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
inheritAttrs: false,
|
|
59
|
+
|
|
60
|
+
props: {
|
|
61
|
+
/**
|
|
62
|
+
* Current color. Accepts any of vue-color's input formats: CSS color
|
|
63
|
+
* string or color object. `null`/empty renders a transparent swatch.
|
|
64
|
+
*/
|
|
65
|
+
value: {
|
|
66
|
+
type: [String, Object],
|
|
67
|
+
default: null,
|
|
68
|
+
},
|
|
69
|
+
/** Disables the swatch trigger and prevents the popover from opening. */
|
|
70
|
+
disabled: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Lock the picker's numeric input fields to a single mode and hide the
|
|
76
|
+
* mode-toggle button. One of `'hex'`, `'rgb'`, `'hsl'`. When `null`
|
|
77
|
+
* (default) the user can switch modes themselves. The actual fields
|
|
78
|
+
* shown for `'rgb'`/`'hsl'` include alpha when `disable-alpha` is
|
|
79
|
+
* `false` (i.e. they become RGBA / HSLA).
|
|
80
|
+
*/
|
|
81
|
+
mode: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: null,
|
|
84
|
+
validator: (v) => v === null || ['hex', 'rgb', 'hsl'].includes(v),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
data() {
|
|
89
|
+
return {
|
|
90
|
+
open: false,
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
computed: {
|
|
95
|
+
/**
|
|
96
|
+
* CSS background for the swatch. Layers a solid color over a checker
|
|
97
|
+
* pattern so alpha values are visible. Falls back to just the checker
|
|
98
|
+
* when no value is set.
|
|
99
|
+
*/
|
|
100
|
+
swatchStyle() {
|
|
101
|
+
const c = typeof this.value === 'string'
|
|
102
|
+
? this.value
|
|
103
|
+
: (this.value?.hex8 || this.value?.hex)
|
|
104
|
+
if (!c) return {}
|
|
105
|
+
// Layer the solid fill on top of the four-gradient checker. Each
|
|
106
|
+
// checker layer needs its own offset so the squares alternate; if
|
|
107
|
+
// they all share `0 0` the pattern collapses to a single square.
|
|
108
|
+
const fill = `linear-gradient(${c}, ${c})`
|
|
109
|
+
return {
|
|
110
|
+
backgroundImage: `${fill}, var(--cn-color-picker-checker)`,
|
|
111
|
+
backgroundSize: '100% 100%, 8px 8px, 8px 8px, 8px 8px, 8px 8px',
|
|
112
|
+
backgroundPosition: '0 0, 0 0, 0 4px, 4px -4px, -4px 0',
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
watch: {
|
|
118
|
+
mode: {
|
|
119
|
+
immediate: true,
|
|
120
|
+
handler() {
|
|
121
|
+
// Wait for the picker ref to exist (initial mount + when the
|
|
122
|
+
// popover first renders the picker).
|
|
123
|
+
this.$nextTick(() => this.applyMode())
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
open(isOpen) {
|
|
127
|
+
// Re-apply on every open: vue-color resets `fieldsIndex` if the
|
|
128
|
+
// component is unmounted/remounted by the popover.
|
|
129
|
+
if (isOpen) this.$nextTick(() => this.applyMode())
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
methods: {
|
|
134
|
+
t,
|
|
135
|
+
|
|
136
|
+
/** Pin the Chrome picker's `fieldsIndex` to the requested mode. */
|
|
137
|
+
applyMode() {
|
|
138
|
+
if (!this.mode) return
|
|
139
|
+
const idx = { hex: 0, rgb: 1, hsl: 2 }[this.mode]
|
|
140
|
+
const picker = this.$refs.picker
|
|
141
|
+
if (picker && picker.fieldsIndex !== idx) {
|
|
142
|
+
picker.fieldsIndex = idx
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<style scoped>
|
|
150
|
+
.cn-color-picker__swatch {
|
|
151
|
+
--cn-color-picker-checker:
|
|
152
|
+
linear-gradient(45deg, var(--color-background-dark) 25%, transparent 25%),
|
|
153
|
+
linear-gradient(-45deg, var(--color-background-dark) 25%, transparent 25%),
|
|
154
|
+
linear-gradient(45deg, transparent 75%, var(--color-background-dark) 75%),
|
|
155
|
+
linear-gradient(-45deg, transparent 75%, var(--color-background-dark) 75%);
|
|
156
|
+
display: inline-block;
|
|
157
|
+
width: 32px;
|
|
158
|
+
height: 32px;
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
padding: 0;
|
|
161
|
+
border: 1px solid var(--color-border);
|
|
162
|
+
border-radius: var(--border-radius);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
background-image: var(--cn-color-picker-checker);
|
|
165
|
+
background-size: 8px 8px;
|
|
166
|
+
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
position: relative;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.cn-color-picker__swatch:focus-visible {
|
|
172
|
+
outline: 2px solid var(--color-primary-element);
|
|
173
|
+
outline-offset: 2px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cn-color-picker__swatch--disabled {
|
|
177
|
+
cursor: not-allowed;
|
|
178
|
+
opacity: 0.6;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Strip Chrome's hardcoded light-mode palette so it adopts the active theme.
|
|
182
|
+
The class lands on the picker's root via Vue's class merging, so target it
|
|
183
|
+
directly — NOT as a descendant. */
|
|
184
|
+
.cn-color-picker__chrome {
|
|
185
|
+
background: var(--color-main-background);
|
|
186
|
+
background-color: var(--color-main-background);
|
|
187
|
+
box-shadow: none;
|
|
188
|
+
color: var(--color-main-text);
|
|
189
|
+
font-family: var(--font-face);
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.cn-color-picker__chrome :deep(.vc-chrome-body),
|
|
194
|
+
.cn-color-picker__chrome :deep(.vc-chrome-saturation-wrap),
|
|
195
|
+
.cn-color-picker__chrome :deep(.vc-chrome-color-wrap),
|
|
196
|
+
.cn-color-picker__chrome :deep(.vc-chrome-active-color) {
|
|
197
|
+
background-color: var(--color-main-background);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.cn-color-picker__chrome :deep(.vc-chrome-toggle-icon-highlight) {
|
|
201
|
+
background: var(--color-background-hover);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.cn-color-picker__chrome :deep(.vc-hue-picker),
|
|
205
|
+
.cn-color-picker__chrome :deep(.vc-alpha-picker) {
|
|
206
|
+
background-color: var(--color-main-background);
|
|
207
|
+
box-shadow: 0 1px 4px 0 var(--color-box-shadow);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.cn-color-picker__chrome :deep(.vc-chrome-fields .vc-input__input) {
|
|
211
|
+
background-color: var(--color-main-background);
|
|
212
|
+
color: var(--color-main-text);
|
|
213
|
+
box-shadow: inset 0 0 0 1px var(--color-border);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.cn-color-picker__chrome :deep(.vc-chrome-fields .vc-input__label) {
|
|
217
|
+
color: var(--color-text-maxcontrast);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.cn-color-picker__chrome :deep(.vc-chrome-toggle-icon svg path) {
|
|
221
|
+
fill: var(--color-main-text);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* When `mode` is set, lock the field-mode toggle so the user can't switch
|
|
225
|
+
between hex/rgb/hsl. */
|
|
226
|
+
.cn-color-picker__chrome--locked-mode :deep(.vc-chrome-toggle-btn) {
|
|
227
|
+
display: none;
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
230
|
+
|
|
231
|
+
<!-- Non-scoped overrides: NcPopover renders into a portal at document.body,
|
|
232
|
+
so scoped styles can't reach it. Targeted by `popoverBaseClass`. -->
|
|
233
|
+
<style>
|
|
234
|
+
.cn-color-picker__popper .v-popper__inner,
|
|
235
|
+
.cn-color-picker__popper .v-popper__wrapper {
|
|
236
|
+
background: var(--color-main-background);
|
|
237
|
+
background-color: var(--color-main-background);
|
|
238
|
+
color: var(--color-main-text);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.cn-color-picker__popper .v-popper__arrow-inner,
|
|
242
|
+
.cn-color-picker__popper .v-popper__arrow-outer {
|
|
243
|
+
border-color: var(--color-main-background);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Default NcPopover style sets `top: -9px` with specificity (0,4,1) using a
|
|
247
|
+
hashed class name; bump with !important rather than depend on the hash. */
|
|
248
|
+
.cn-color-picker__popper .v-popper__arrow-container {
|
|
249
|
+
top: -10px !important;
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnColorPicker } from './CnColorPicker.vue'
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
@close="onClose">
|
|
9
9
|
<!-- Dynamic actions from array prop -->
|
|
10
10
|
<NcActionButton
|
|
11
|
-
v-for="action in
|
|
11
|
+
v-for="action in visibleActions"
|
|
12
12
|
:key="action.label"
|
|
13
|
+
:title="resolveTitle(action)"
|
|
13
14
|
:disabled="resolveDisabled(action)"
|
|
14
15
|
:class="{ 'cn-row-action--destructive': action.destructive }"
|
|
15
16
|
close-after-click
|
|
@@ -77,9 +78,13 @@ export default {
|
|
|
77
78
|
},
|
|
78
79
|
/**
|
|
79
80
|
* Action definitions rendered as NcActionButton items.
|
|
80
|
-
* Same format as CnRowActions: `{ label, icon?, handler?, disabled?, destructive? }`.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
81
|
+
* Same format as CnRowActions: `{ label, icon?, handler?, disabled?, visible?, title?, destructive? }`.
|
|
82
|
+
* `visible` (boolean | (targetItem) => boolean) hides the entry when falsy
|
|
83
|
+
* (default: shown). `title` (string | (targetItem) => string) renders as
|
|
84
|
+
* a native tooltip — useful for explaining why an entry is disabled.
|
|
85
|
+
* When the entire array is empty (or all entries are filtered out), only
|
|
86
|
+
* the default slot content is rendered.
|
|
87
|
+
* @type {Array<{label: string, icon?: object, handler?: Function, disabled?: boolean | Function, visible?: boolean | Function, title?: string | Function, destructive?: boolean}>}
|
|
83
88
|
*/
|
|
84
89
|
actions: {
|
|
85
90
|
type: Array,
|
|
@@ -102,6 +107,23 @@ export default {
|
|
|
102
107
|
}
|
|
103
108
|
},
|
|
104
109
|
|
|
110
|
+
computed: {
|
|
111
|
+
/**
|
|
112
|
+
* Filter actions by their `visible` predicate. Entries without
|
|
113
|
+
* `visible` are always shown (backwards compatible).
|
|
114
|
+
* @return {Array} Visible actions for the current targetItem.
|
|
115
|
+
*/
|
|
116
|
+
visibleActions() {
|
|
117
|
+
return this.actions.filter((action) => {
|
|
118
|
+
if (action.visible === undefined) return true
|
|
119
|
+
if (typeof action.visible === 'function') {
|
|
120
|
+
return !!action.visible(this.targetItem)
|
|
121
|
+
}
|
|
122
|
+
return !!action.visible
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
105
127
|
watch: {
|
|
106
128
|
open(val) {
|
|
107
129
|
this.internalOpen = val
|
|
@@ -119,6 +141,21 @@ export default {
|
|
|
119
141
|
return !!action.disabled
|
|
120
142
|
},
|
|
121
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the `title` field on an action descriptor — supports both
|
|
146
|
+
* a static string and a function `(targetItem) => string`. Returns
|
|
147
|
+
* undefined when no title is provided so the attribute isn't rendered.
|
|
148
|
+
*
|
|
149
|
+
* @param {object} action The action descriptor.
|
|
150
|
+
* @return {string|undefined} The tooltip text, or undefined.
|
|
151
|
+
*/
|
|
152
|
+
resolveTitle(action) {
|
|
153
|
+
if (typeof action.title === 'function') {
|
|
154
|
+
return action.title(this.targetItem) || undefined
|
|
155
|
+
}
|
|
156
|
+
return action.title || undefined
|
|
157
|
+
},
|
|
158
|
+
|
|
122
159
|
onAction(action) {
|
|
123
160
|
if (action.handler && typeof action.handler === 'function') {
|
|
124
161
|
action.handler(this.targetItem)
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
24
24
|
<div class="cn-dashboard-page__header-actions">
|
|
25
|
+
<!-- Public slot. Documented in CLAUDE.md and used by every
|
|
26
|
+
existing consumer (decidesk, mydash, opencatalogi,
|
|
27
|
+
pipelinq, procest). -->
|
|
28
|
+
<slot name="header-actions" />
|
|
29
|
+
<!-- Back-compat alias: original slot name shipped before
|
|
30
|
+
CLAUDE.md was updated. Render alongside so any
|
|
31
|
+
stragglers still work; consumers should prefer
|
|
32
|
+
#header-actions. -->
|
|
25
33
|
<slot name="actions" />
|
|
26
34
|
<NcButton
|
|
27
35
|
v-if="allowEdit"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnDependencyMissing — full-page screen shown when one or more apps
|
|
3
|
+
declared in `manifest.dependencies` are not installed or not enabled.
|
|
4
|
+
|
|
5
|
+
CnAppRoot mounts this in its dependency-check phase (between loading
|
|
6
|
+
and shell). Apps can override CnAppRoot's #dependency-missing slot to
|
|
7
|
+
customise the screen.
|
|
8
|
+
|
|
9
|
+
See REQ-JMR-011 of the json-manifest-renderer specification.
|
|
10
|
+
-->
|
|
11
|
+
<template>
|
|
12
|
+
<div class="cn-dependency-missing">
|
|
13
|
+
<div class="cn-dependency-missing__inner">
|
|
14
|
+
<h1 class="cn-dependency-missing__heading">
|
|
15
|
+
{{ heading }}
|
|
16
|
+
</h1>
|
|
17
|
+
<p class="cn-dependency-missing__intro">
|
|
18
|
+
{{ intro }}
|
|
19
|
+
</p>
|
|
20
|
+
<ul class="cn-dependency-missing__list">
|
|
21
|
+
<li
|
|
22
|
+
v-for="dep in dependencies"
|
|
23
|
+
:key="dep.id"
|
|
24
|
+
class="cn-dependency-missing__item">
|
|
25
|
+
<span class="cn-dependency-missing__item-name">{{ dep.name || dep.id }}</span>
|
|
26
|
+
<a
|
|
27
|
+
class="cn-dependency-missing__item-link"
|
|
28
|
+
:href="resolveLink(dep)"
|
|
29
|
+
target="_self">
|
|
30
|
+
{{ dep.enabled === false ? enableLabel : installLabel }}
|
|
31
|
+
</a>
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script>
|
|
39
|
+
export default {
|
|
40
|
+
name: 'CnDependencyMissing',
|
|
41
|
+
|
|
42
|
+
props: {
|
|
43
|
+
/**
|
|
44
|
+
* Array of missing dependencies. Each entry:
|
|
45
|
+
* { id, name?, installUrl?, enabled? }
|
|
46
|
+
* - `id` is the Nextcloud app id (matches the entries in
|
|
47
|
+
* manifest.dependencies)
|
|
48
|
+
* - `name` is a human-readable label; `id` is used as a fallback
|
|
49
|
+
* - `installUrl` overrides the default install/enable URL when
|
|
50
|
+
* set; otherwise the default Nextcloud apps page is used
|
|
51
|
+
* - `enabled` discriminates the link label: `false` means the
|
|
52
|
+
* app is installed but disabled; otherwise it's not installed
|
|
53
|
+
*
|
|
54
|
+
* @type {Array<{id: string, name?: string, installUrl?: string, enabled?: boolean}>}
|
|
55
|
+
*/
|
|
56
|
+
dependencies: {
|
|
57
|
+
type: Array,
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Optional name of the host app, included in the default heading.
|
|
62
|
+
*
|
|
63
|
+
* @type {string}
|
|
64
|
+
*/
|
|
65
|
+
appName: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: '',
|
|
68
|
+
},
|
|
69
|
+
/** Heading text. Override for localisation. */
|
|
70
|
+
heading: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: 'Required apps are missing',
|
|
73
|
+
},
|
|
74
|
+
/** Introductory text under the heading. */
|
|
75
|
+
intro: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: 'This app needs the following Nextcloud apps to be installed and enabled.',
|
|
78
|
+
},
|
|
79
|
+
/** Label for the install link. */
|
|
80
|
+
installLabel: {
|
|
81
|
+
type: String,
|
|
82
|
+
default: 'Install',
|
|
83
|
+
},
|
|
84
|
+
/** Label for the enable link (used when dep.enabled === false). */
|
|
85
|
+
enableLabel: {
|
|
86
|
+
type: String,
|
|
87
|
+
default: 'Enable',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
methods: {
|
|
92
|
+
resolveLink(dep) {
|
|
93
|
+
if (dep.installUrl) return dep.installUrl
|
|
94
|
+
return '/index.php/settings/apps'
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<style>
|
|
101
|
+
.cn-dependency-missing {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
width: 100%;
|
|
106
|
+
min-height: 100vh;
|
|
107
|
+
background: var(--color-main-background);
|
|
108
|
+
color: var(--color-main-text);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.cn-dependency-missing__inner {
|
|
112
|
+
max-width: 600px;
|
|
113
|
+
padding: calc(4 * var(--default-grid-baseline));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.cn-dependency-missing__heading {
|
|
117
|
+
margin: 0 0 calc(2 * var(--default-grid-baseline));
|
|
118
|
+
font-size: 1.5em;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cn-dependency-missing__intro {
|
|
122
|
+
margin: 0 0 calc(3 * var(--default-grid-baseline));
|
|
123
|
+
color: var(--color-text-maxcontrast);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.cn-dependency-missing__list {
|
|
127
|
+
margin: 0;
|
|
128
|
+
padding: 0;
|
|
129
|
+
list-style: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cn-dependency-missing__item {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
padding: calc(2 * var(--default-grid-baseline)) 0;
|
|
137
|
+
border-bottom: 1px solid var(--color-border);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.cn-dependency-missing__item:last-child {
|
|
141
|
+
border-bottom: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.cn-dependency-missing__item-name {
|
|
145
|
+
font-weight: bold;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.cn-dependency-missing__item-link {
|
|
149
|
+
color: var(--color-primary-element);
|
|
150
|
+
text-decoration: underline;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
@@ -24,23 +24,34 @@
|
|
|
24
24
|
<div class="cn-detail-page" :style="{ maxWidth: maxWidth }">
|
|
25
25
|
<!-- Header -->
|
|
26
26
|
<div class="cn-detail-page__header">
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
<!-- Header (left block) — overridable via #header slot. Default
|
|
28
|
+
renders the icon + title + description. The right-hand
|
|
29
|
+
#actions slot remains separate so headerComponent and
|
|
30
|
+
actionsComponent can be replaced independently. -->
|
|
31
|
+
<slot
|
|
32
|
+
name="header"
|
|
33
|
+
:title="title"
|
|
34
|
+
:description="description"
|
|
35
|
+
:icon="icon"
|
|
36
|
+
:icon-size="iconSize">
|
|
37
|
+
<div class="cn-detail-page__header-left">
|
|
38
|
+
<slot name="icon">
|
|
39
|
+
<CnIcon
|
|
40
|
+
v-if="icon"
|
|
41
|
+
:name="icon"
|
|
42
|
+
:size="iconSize"
|
|
43
|
+
class="cn-detail-page__icon" />
|
|
44
|
+
</slot>
|
|
45
|
+
<div class="cn-detail-page__header-text">
|
|
46
|
+
<h2 v-if="title" class="cn-detail-page__title">
|
|
47
|
+
{{ title }}
|
|
48
|
+
</h2>
|
|
49
|
+
<p v-if="description" class="cn-detail-page__description">
|
|
50
|
+
{{ description }}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
42
53
|
</div>
|
|
43
|
-
</
|
|
54
|
+
</slot>
|
|
44
55
|
<div class="cn-detail-page__header-actions">
|
|
45
56
|
<slot name="actions" />
|
|
46
57
|
</div>
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cn-index-page">
|
|
3
|
-
<!-- Header
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<!-- Header — overridable via #header slot. Default renders CnPageHeader
|
|
4
|
+
when showTitle is true (existing behaviour, hidden by default). -->
|
|
5
|
+
<slot
|
|
6
|
+
name="header"
|
|
6
7
|
:title="title"
|
|
7
8
|
:description="description"
|
|
8
|
-
:icon="resolvedIcon"
|
|
9
|
+
:icon="resolvedIcon"
|
|
10
|
+
:show-title="showTitle">
|
|
11
|
+
<CnPageHeader
|
|
12
|
+
v-if="showTitle"
|
|
13
|
+
:title="title"
|
|
14
|
+
:description="description"
|
|
15
|
+
:icon="resolvedIcon" />
|
|
16
|
+
</slot>
|
|
9
17
|
|
|
10
18
|
<!-- Optional content below header, above actions bar -->
|
|
11
19
|
<div v-if="$scopedSlots['below-header']" class="cn-index-page__below-header">
|
|
@@ -548,6 +556,17 @@ export default {
|
|
|
548
556
|
type: Boolean,
|
|
549
557
|
default: false,
|
|
550
558
|
},
|
|
559
|
+
/**
|
|
560
|
+
* Whether to add a View action to row actions. The action emits a
|
|
561
|
+
* dedicated `view` event — independent of `row-click`. Bind `@view`
|
|
562
|
+
* to handle "open detail" and `@row-click` to handle row click
|
|
563
|
+
* (selection, expand, etc.); they may share a handler when the app
|
|
564
|
+
* wants click-to-view, but they are conceptually distinct.
|
|
565
|
+
*/
|
|
566
|
+
showViewAction: {
|
|
567
|
+
type: Boolean,
|
|
568
|
+
default: true,
|
|
569
|
+
},
|
|
551
570
|
/** Whether to add an Edit action to row actions */
|
|
552
571
|
showEditAction: {
|
|
553
572
|
type: Boolean,
|
|
@@ -670,12 +689,12 @@ export default {
|
|
|
670
689
|
/** Built-in row actions based on show*Action props */
|
|
671
690
|
defaultActions() {
|
|
672
691
|
const builtIn = []
|
|
673
|
-
if (this
|
|
692
|
+
if (this.showViewAction) {
|
|
674
693
|
builtIn.push({
|
|
675
694
|
label: 'View',
|
|
676
695
|
icon: this.schemaIconComponent,
|
|
677
696
|
handler: (row) => {
|
|
678
|
-
this.
|
|
697
|
+
this.onView(row)
|
|
679
698
|
},
|
|
680
699
|
})
|
|
681
700
|
}
|
|
@@ -765,6 +784,17 @@ export default {
|
|
|
765
784
|
this.$emit('row-click', row)
|
|
766
785
|
},
|
|
767
786
|
|
|
787
|
+
/**
|
|
788
|
+
* Handle the built-in View action — emits a dedicated `view` event.
|
|
789
|
+
* Kept distinct from `row-click` because the two are conceptually
|
|
790
|
+
* different: a row click might mean select/expand/drilldown, while
|
|
791
|
+
* View always means "open the detail view of this row".
|
|
792
|
+
* @param {object} row The row whose View action was triggered
|
|
793
|
+
*/
|
|
794
|
+
onView(row) {
|
|
795
|
+
this.$emit('view', row)
|
|
796
|
+
},
|
|
797
|
+
|
|
768
798
|
/**
|
|
769
799
|
* Handle the Add button click. If the consumer listens to @add,
|
|
770
800
|
* emit the event (backward compatible). Otherwise open the form dialog.
|