@conduction/nextcloud-vue 2.121.0 → 2.123.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.
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +1 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue.js +1 -1
- package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue2.js +18 -4
- package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue3.js +63 -32
- package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +35 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +11 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +13 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +37 -5
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +40 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +71 -65
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue.js +1 -1
- package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js +20 -0
- package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js +2 -1
- package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +2 -0
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +49 -44
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
- package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js +31 -0
- package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js.map +1 -1
- package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js +4 -3
- package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +34 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -0
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +2 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +1 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +31 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +14 -4
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -1
- package/dist/esm/composables/flowGraphLayout.js +36 -7
- package/dist/esm/composables/flowGraphLayout.js.map +1 -1
- package/dist/esm/composables/useFlowStore.js +7 -3
- package/dist/esm/composables/useFlowStore.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +670 -316
- package/dist/esm/utils/manifestTranslate.js +12 -3
- package/dist/esm/utils/manifestTranslate.js.map +1 -1
- package/dist/esm/utils/routeFilters.js +3 -3
- package/dist/esm/utils/routeFilters.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1218 -847
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +670 -316
- package/l10n/en.json +3 -1
- package/l10n/nl.json +3 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +1 -0
- package/src/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue +50 -5
- package/src/components/CnChartWidget/CnChartWidget.vue +10 -8
- package/src/components/CnDetailPage/CnDetailPage.md +10 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +42 -1
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +39 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +95 -37
- package/src/components/CnFlowDetail/CnFlowDetail.vue +21 -0
- package/src/components/CnFlowsPage/CnFlowBoard.vue +35 -18
- package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
- package/src/components/CnGraphCanvas/CnGraphCanvas.vue +34 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +34 -2
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +1 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +3 -0
- package/src/components/CnSectionsWidget/CnSectionsWidget.vue +53 -2
- package/src/composables/flowGraphLayout.js +35 -6
- package/src/composables/useFlowStore.js +7 -3
- package/src/css/detail-page.css +6 -0
- package/src/css/look-board-detail.css +53 -6
- package/src/css/look-board-index.css +151 -12
- package/src/css/look-board.css +116 -4
- package/src/utils/manifestTranslate.js +12 -3
- package/src/utils/routeFilters.js +3 -3
package/l10n/en.json
CHANGED
|
@@ -2219,7 +2219,9 @@
|
|
|
2219
2219
|
"Nothing to check.": "Nothing to check.",
|
|
2220
2220
|
"This could not be read: {reason}": "This could not be read: {reason}",
|
|
2221
2221
|
"Still open": "Still open",
|
|
2222
|
-
"The checklist could not be read.": "The checklist could not be read."
|
|
2222
|
+
"The checklist could not be read.": "The checklist could not be read.",
|
|
2223
|
+
"Flag it with a reason, so it stands out to everyone who works on it.": "Flag it with a reason, so it stands out to everyone who works on it.",
|
|
2224
|
+
"Clear the flag with a reason once it no longer needs attention.": "Clear the flag with a reason once it no longer needs attention."
|
|
2223
2225
|
},
|
|
2224
2226
|
"plurals": {
|
|
2225
2227
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2469,7 +2469,9 @@
|
|
|
2469
2469
|
"Nothing to check.": "Er is niets te controleren.",
|
|
2470
2470
|
"This could not be read: {reason}": "Dit kon niet worden gelezen: {reason}",
|
|
2471
2471
|
"Still open": "Nog open",
|
|
2472
|
-
"The checklist could not be read.": "De lijst kon niet worden gelezen."
|
|
2472
|
+
"The checklist could not be read.": "De lijst kon niet worden gelezen.",
|
|
2473
|
+
"Flag it with a reason, so it stands out to everyone who works on it.": "Markeer het met een reden, zodat het opvalt bij iedereen die eraan werkt.",
|
|
2474
|
+
"Clear the flag with a reason once it no longer needs attention.": "Haal de markering weg met een reden zodra het geen aandacht meer nodig heeft."
|
|
2473
2475
|
},
|
|
2474
2476
|
"plurals": {
|
|
2475
2477
|
"{count} member": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.123.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -2278,6 +2278,7 @@ export default {
|
|
|
2278
2278
|
getManifest: () => (this.manifestEditor ? this.manifestEditor.source.value : this.manifest),
|
|
2279
2279
|
getLanguage: () => this.language || getLanguage(),
|
|
2280
2280
|
translate: (text, vars) => this.translate(text, vars),
|
|
2281
|
+
library: (text, vars) => t('nextcloud-vue', text, vars),
|
|
2281
2282
|
})
|
|
2282
2283
|
},
|
|
2283
2284
|
|
|
@@ -40,7 +40,26 @@
|
|
|
40
40
|
data-testid="cn-attention-flag-review-due">{{ label('reviewDue') }}</span>
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
|
-
<
|
|
43
|
+
<template v-if="behindButton && !composing">
|
|
44
|
+
<h4 class="cn-attention-flag__heading" data-testid="cn-attention-flag-heading">
|
|
45
|
+
{{ reasonLabel }}
|
|
46
|
+
</h4>
|
|
47
|
+
<p class="cn-attention-flag__hint" data-testid="cn-attention-flag-hint">
|
|
48
|
+
{{ raised ? label('hintClear') : label('hintRaise') }}
|
|
49
|
+
</p>
|
|
50
|
+
<NcButton
|
|
51
|
+
variant="secondary"
|
|
52
|
+
wide
|
|
53
|
+
data-testid="cn-attention-flag-compose"
|
|
54
|
+
@click="composing = true">
|
|
55
|
+
<template #icon>
|
|
56
|
+
<FlagOutline :size="20" />
|
|
57
|
+
</template>
|
|
58
|
+
{{ raised ? label('clear') : label('raise') }}
|
|
59
|
+
</NcButton>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<div v-else class="cn-attention-flag__act">
|
|
44
63
|
<NcTextField
|
|
45
64
|
:modelValue="reason"
|
|
46
65
|
:label="reasonLabel"
|
|
@@ -84,6 +103,7 @@ import { emit } from '@nextcloud/event-bus'
|
|
|
84
103
|
import { translate as t } from '@nextcloud/l10n'
|
|
85
104
|
import { generateUrl } from '@nextcloud/router'
|
|
86
105
|
import { NcButton, NcTextField } from '@nextcloud/vue'
|
|
106
|
+
import FlagOutline from 'vue-material-design-icons/FlagOutline.vue'
|
|
87
107
|
import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
|
|
88
108
|
import { dispatchObjectsChanged } from '../../utils/objectSignals.js'
|
|
89
109
|
import { readPath } from '../../utils/readPath.js'
|
|
@@ -100,6 +120,8 @@ function defaultLabels() {
|
|
|
100
120
|
reviewDue: t('nextcloud-vue', 'Due for review'),
|
|
101
121
|
reasonRaise: t('nextcloud-vue', 'Why this needs attention'),
|
|
102
122
|
reasonClear: t('nextcloud-vue', 'Why it no longer needs attention'),
|
|
123
|
+
hintRaise: t('nextcloud-vue', 'Flag it with a reason, so it stands out to everyone who works on it.'),
|
|
124
|
+
hintClear: t('nextcloud-vue', 'Clear the flag with a reason once it no longer needs attention.'),
|
|
103
125
|
raise: t('nextcloud-vue', 'Flag for attention'),
|
|
104
126
|
clear: t('nextcloud-vue', 'Clear the flag'),
|
|
105
127
|
raised: t('nextcloud-vue', 'Raised'),
|
|
@@ -130,7 +152,8 @@ const LEVELS = {
|
|
|
130
152
|
* "clearPath": "/clear",
|
|
131
153
|
* "markersPath": "attentionMarkers",
|
|
132
154
|
* "panelLabels": { "item-files": "Files" },
|
|
133
|
-
* "assessment": { "path": "riskAssessment", "levelMap": { "high": "High" }, "reviewDateField": "reviewDate" }
|
|
155
|
+
* "assessment": { "path": "riskAssessment", "levelMap": { "high": "High" }, "reviewDateField": "reviewDate" },
|
|
156
|
+
* "behindButton": true
|
|
134
157
|
* }
|
|
135
158
|
* }
|
|
136
159
|
* ```
|
|
@@ -142,7 +165,9 @@ const LEVELS = {
|
|
|
142
165
|
* as written. Markers and the assessment are read off the record the page
|
|
143
166
|
* already holds. An assessment the reader may not see is absent from the
|
|
144
167
|
* record, so it is absent here rather than blank. A failed read draws
|
|
145
|
-
* nothing.
|
|
168
|
+
* nothing. With `behindButton` the strip shows the act as a heading, a hint
|
|
169
|
+
* (`labels.hintRaise` / `labels.hintClear`) and one button, and the reason
|
|
170
|
+
* field opens when that button is pressed. After a raise or a clear the widget emits `object-changed`, says
|
|
146
171
|
* so to every list (`dispatchObjectsChanged`) and asks the page to refresh.
|
|
147
172
|
*
|
|
148
173
|
* @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-record-can-carry-an-attention-flag
|
|
@@ -150,7 +175,7 @@ const LEVELS = {
|
|
|
150
175
|
export default {
|
|
151
176
|
name: 'CnAttentionFlagWidget',
|
|
152
177
|
|
|
153
|
-
components: { NcButton, NcTextField },
|
|
178
|
+
components: { FlagOutline, NcButton, NcTextField },
|
|
154
179
|
|
|
155
180
|
inject: {
|
|
156
181
|
cnTranslate: { default: () => (key) => key },
|
|
@@ -162,7 +187,8 @@ export default {
|
|
|
162
187
|
* `raisePath` (default `/raise`), `clearPath` (default `/clear`),
|
|
163
188
|
* `labels` (overrides of the default words), `markersPath`,
|
|
164
189
|
* `panelLabels` (marker tab id to label) and `assessment`
|
|
165
|
-
* (`{ path, levelMap?, levelField?, groundField?, reviewDateField? }`)
|
|
190
|
+
* (`{ path, levelMap?, levelField?, groundField?, reviewDateField? }`)
|
|
191
|
+
* and `behindButton` (the reason field opens from a button).
|
|
166
192
|
*/
|
|
167
193
|
content: {
|
|
168
194
|
type: Object,
|
|
@@ -205,6 +231,7 @@ export default {
|
|
|
205
231
|
history: [],
|
|
206
232
|
reason: '',
|
|
207
233
|
refusal: '',
|
|
234
|
+
composing: false,
|
|
208
235
|
}
|
|
209
236
|
},
|
|
210
237
|
|
|
@@ -222,6 +249,11 @@ export default {
|
|
|
222
249
|
return interpolateUrlTokens(raw, { objectId: this.objectId, object: this.objectData || {} })
|
|
223
250
|
},
|
|
224
251
|
|
|
252
|
+
/** @return {boolean} Whether the reason field waits behind a button. */
|
|
253
|
+
behindButton() {
|
|
254
|
+
return this.content?.behindButton === true
|
|
255
|
+
},
|
|
256
|
+
|
|
225
257
|
/** @return {string} What the raise or clear was for, in the flag's own words. */
|
|
226
258
|
flagReason() {
|
|
227
259
|
return String(this.flag?.reason ?? '')
|
|
@@ -370,6 +402,7 @@ export default {
|
|
|
370
402
|
const response = await axios.post(generateUrl(this.endpoint + tail), { reason })
|
|
371
403
|
this.apply(response?.data)
|
|
372
404
|
this.reason = ''
|
|
405
|
+
this.composing = false
|
|
373
406
|
this.announce()
|
|
374
407
|
} catch (error) {
|
|
375
408
|
const said = String(error?.response?.data?.error ?? error?.response?.data?.message ?? '')
|
|
@@ -450,6 +483,18 @@ function hasPassed(date) {
|
|
|
450
483
|
font-weight: bold;
|
|
451
484
|
}
|
|
452
485
|
|
|
486
|
+
.cn-attention-flag__heading {
|
|
487
|
+
margin: 0;
|
|
488
|
+
font-size: 14px;
|
|
489
|
+
font-weight: bold;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.cn-attention-flag__hint {
|
|
493
|
+
margin: 0 0 8px;
|
|
494
|
+
color: var(--color-text-maxcontrast);
|
|
495
|
+
font-size: 13px;
|
|
496
|
+
}
|
|
497
|
+
|
|
453
498
|
.cn-attention-flag__muted {
|
|
454
499
|
color: var(--color-text-maxcontrast);
|
|
455
500
|
}
|
|
@@ -2908,9 +2908,11 @@ export default {
|
|
|
2908
2908
|
}
|
|
2909
2909
|
|
|
2910
2910
|
/* A cell is a span: its ink also goes to --nldesign-color-on-surface, which a
|
|
2911
|
-
theme that forces the body ink onto every span (thematiq) reads. The
|
|
2912
|
-
levels
|
|
2913
|
-
|
|
2911
|
+
theme that forces the body ink onto every span (thematiq) reads. The four
|
|
2912
|
+
levels are the boards' tones (DqProcesanalyse): the light primary, the
|
|
2913
|
+
primary at 35% and at 70%, and level 1 inverted. Levels 1 to 3 take a dark
|
|
2914
|
+
ink; the board's white on level 3 is about 3.1:1, and 70% is as deep as the
|
|
2915
|
+
dark ink stays at 4.5:1 in Nextcloud's dark theme. */
|
|
2914
2916
|
.cn-chart-widget__matrix-cell--1 {
|
|
2915
2917
|
--nldesign-color-on-surface: var(--color-primary-element-light-text);
|
|
2916
2918
|
background: var(--color-primary-element-light);
|
|
@@ -2918,20 +2920,20 @@ export default {
|
|
|
2918
2920
|
}
|
|
2919
2921
|
|
|
2920
2922
|
.cn-chart-widget__matrix-cell--2 {
|
|
2921
|
-
background: color-mix(in srgb, var(--color-primary-element)
|
|
2923
|
+
background: color-mix(in srgb, var(--color-primary-element) 35%, var(--color-main-background));
|
|
2922
2924
|
color: var(--color-main-text);
|
|
2923
2925
|
}
|
|
2924
2926
|
|
|
2925
2927
|
.cn-chart-widget__matrix-cell--3 {
|
|
2926
2928
|
--nldesign-color-on-surface: var(--color-main-text);
|
|
2927
|
-
background: color-mix(in srgb, var(--color-primary-element)
|
|
2929
|
+
background: color-mix(in srgb, var(--color-primary-element) 70%, var(--color-main-background));
|
|
2928
2930
|
color: var(--color-main-text);
|
|
2929
2931
|
}
|
|
2930
2932
|
|
|
2931
2933
|
.cn-chart-widget__matrix-cell--4 {
|
|
2932
|
-
--nldesign-color-on-surface: var(--color-primary-element-
|
|
2933
|
-
background: var(--color-primary-element);
|
|
2934
|
-
color: var(--color-primary-element-
|
|
2934
|
+
--nldesign-color-on-surface: var(--color-primary-element-light);
|
|
2935
|
+
background: var(--color-primary-element-light-text);
|
|
2936
|
+
color: var(--color-primary-element-light);
|
|
2935
2937
|
}
|
|
2936
2938
|
|
|
2937
2939
|
.cn-chart-widget__empty {
|
|
@@ -432,6 +432,16 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
432
432
|
| `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label }`; "Other" with a written purpose is always added); `url` is the app endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}` filled in). Nothing of the record shows until the endpoint accepts, and the same record is not asked again in the same browser session. A refusal keeps the record closed. `null` asks nothing. |
|
|
433
433
|
| `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
|
|
434
434
|
|
|
435
|
+
## A body widget's own cards in the side column
|
|
436
|
+
|
|
437
|
+
A widget in the body can show cards about what is selected in it in the side column, in place of the generic side cards (DqDocumentenBeheer: "Wie mag dit zien: <file>" and "Controlegetal" while files are ticked). The page provides `cnDetailSideColumn`:
|
|
438
|
+
|
|
439
|
+
| Member | Meaning |
|
|
440
|
+
|---|---|
|
|
441
|
+
| `available` | Whether the page draws a side column. Without one the widget keeps its cards where they were. |
|
|
442
|
+
| `target` | A selector for the slot at the top of the side column, to `<Teleport>` the cards to. |
|
|
443
|
+
| `claim(owner, on)` | Start (`true`) or stop (`false`) showing cards there. While any owner claims, the generic side cards stand aside; they come back when the last one lets go. Release on unmount. |
|
|
444
|
+
|
|
435
445
|
## The record as a place
|
|
436
446
|
|
|
437
447
|
`listNavigation`, `primaryAction` and `tabInAddress` make a record somewhere a handler stays. All three are off by default. See [the reference page](../../../docs/components/cn-detail-page.md#the-record-as-a-place).
|
|
@@ -1088,8 +1088,14 @@
|
|
|
1088
1088
|
class="cn-detail-page__side"
|
|
1089
1089
|
:aria-label="sideColumnLabel"
|
|
1090
1090
|
data-testid="cn-detail-page-side">
|
|
1091
|
+
<!-- Where a body widget puts its own cards about what is selected in
|
|
1092
|
+
it (`cnDetailSideColumn`); while it does, the generic cards
|
|
1093
|
+
below stand aside. -->
|
|
1094
|
+
<div :id="sideTakeoverId"
|
|
1095
|
+
class="cn-detail-page__side-takeover"
|
|
1096
|
+
data-testid="cn-detail-page-side-takeover" />
|
|
1091
1097
|
<div
|
|
1092
|
-
v-for="widget in sideColumnWidgets"
|
|
1098
|
+
v-for="widget in sideClaims.length > 0 ? [] : sideColumnWidgets"
|
|
1093
1099
|
:key="`side-${widget.id}`"
|
|
1094
1100
|
class="cn-detail-page__side-item"
|
|
1095
1101
|
:class="{ 'cn-detail-page__side-item--notice': isBoard && widget.type === 'banner', 'cn-detail-page__side-item--history': isBoard && widget.id === sideHistoryId, 'cn-detail-page__side-item--with-identifier': isBoard && widget.id === sideHistoryId && identifierLine !== '' }"
|
|
@@ -1421,6 +1427,9 @@ const INTEGRATION_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page',
|
|
|
1421
1427
|
/** The session storage key that lists the records whose access purpose is recorded. */
|
|
1422
1428
|
const ACCESS_PURPOSE_SESSION_KEY = 'cn-access-purposes'
|
|
1423
1429
|
|
|
1430
|
+
/** Counter for unique side column slot ids across instances. */
|
|
1431
|
+
let sideTakeoverCounter = 0
|
|
1432
|
+
|
|
1424
1433
|
/**
|
|
1425
1434
|
* The records whose access purpose this browser session recorded. Empty when
|
|
1426
1435
|
* session storage cannot be read (a private window, blocked site data).
|
|
@@ -1612,7 +1621,22 @@ export default {
|
|
|
1612
1621
|
const tabsLabel = computed(() => this.resolvedTabsLabel)
|
|
1613
1622
|
const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))
|
|
1614
1623
|
const canCheckItems = computed(() => this.recordCanCheckItems)
|
|
1624
|
+
const sideAvailable = computed(() => this.showsSideColumn)
|
|
1615
1625
|
return {
|
|
1626
|
+
// The side column, for a widget in the body that describes what is
|
|
1627
|
+
// selected in it (a files list's cards about the selected file).
|
|
1628
|
+
// `available` says whether the page draws a side column; `target`
|
|
1629
|
+
// is the selector to teleport the cards to; `claim(owner, on)`
|
|
1630
|
+
// puts the generic side cards aside while an owner shows its own.
|
|
1631
|
+
cnDetailSideColumn: {
|
|
1632
|
+
get available() {
|
|
1633
|
+
return sideAvailable.value
|
|
1634
|
+
},
|
|
1635
|
+
|
|
1636
|
+
target: `#${this.sideTakeoverId}`,
|
|
1637
|
+
claim: (owner, on) => this.claimSideColumn(owner, on),
|
|
1638
|
+
},
|
|
1639
|
+
|
|
1616
1640
|
// The page's own look reaches the tabs, the cards and the activity
|
|
1617
1641
|
// below it, also when it came from the `look` prop and not the app.
|
|
1618
1642
|
cnLook: computed(() => (isBoard.value ? 'board' : 'nextcloud')),
|
|
@@ -3051,6 +3075,10 @@ export default {
|
|
|
3051
3075
|
|
|
3052
3076
|
data() {
|
|
3053
3077
|
return {
|
|
3078
|
+
/** The id of the side column's slot for a body widget's own cards. */
|
|
3079
|
+
sideTakeoverId: `cn-detail-side-takeover-${++sideTakeoverCounter}`,
|
|
3080
|
+
/** The body widgets that show their own cards in the side column now. */
|
|
3081
|
+
sideClaims: [],
|
|
3054
3082
|
/** The session keys of the records whose access purpose is recorded. */
|
|
3055
3083
|
accessPurposeGranted: readGrantedPurposes(),
|
|
3056
3084
|
/** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */
|
|
@@ -5347,6 +5375,19 @@ export default {
|
|
|
5347
5375
|
},
|
|
5348
5376
|
|
|
5349
5377
|
methods: {
|
|
5378
|
+
/**
|
|
5379
|
+
* A body widget starts or stops showing its own cards in the side
|
|
5380
|
+
* column. While any does, the generic side cards stand aside.
|
|
5381
|
+
*
|
|
5382
|
+
* @param {string|number} owner Who claims, unique per widget.
|
|
5383
|
+
* @param {boolean} on Whether it shows its cards now.
|
|
5384
|
+
* @return {void}
|
|
5385
|
+
*/
|
|
5386
|
+
claimSideColumn(owner, on) {
|
|
5387
|
+
const rest = this.sideClaims.filter((id) => id !== owner)
|
|
5388
|
+
this.sideClaims = on ? [...rest, owner] : rest
|
|
5389
|
+
},
|
|
5390
|
+
|
|
5350
5391
|
/**
|
|
5351
5392
|
* A text of the access purpose gate through the app's translate, '' when unset.
|
|
5352
5393
|
*
|
|
@@ -305,6 +305,32 @@
|
|
|
305
305
|
@selectObject="onSelectObject" />
|
|
306
306
|
</CnWidgetWrapper>
|
|
307
307
|
|
|
308
|
+
<!-- A titled `sections` widget in the side column or a tab stack: its
|
|
309
|
+
sections stack inside one titled card, as the other cards there
|
|
310
|
+
are drawn. A grid cell has its own card, and `content.cards` makes
|
|
311
|
+
every section a card of its own, so both stay as they are. -->
|
|
312
|
+
<CnWidgetWrapper
|
|
313
|
+
v-else-if="renderer && wrapsSections"
|
|
314
|
+
:title="widget.title"
|
|
315
|
+
:subtitle="widgetSubtitle"
|
|
316
|
+
:showTitle="effectiveShowCardTitle"
|
|
317
|
+
titleIconPosition="left"
|
|
318
|
+
:showActions="false"
|
|
319
|
+
:showRefresh="false"
|
|
320
|
+
:showRequestFeature="false"
|
|
321
|
+
:headerLink="cardHeaderLink"
|
|
322
|
+
class="cn-detail-page__sections-card"
|
|
323
|
+
data-testid="cn-detail-widget-sections-card">
|
|
324
|
+
<template v-if="widget.icon" #title-icon>
|
|
325
|
+
<CnIcon :name="widget.icon" :size="20" />
|
|
326
|
+
</template>
|
|
327
|
+
<component
|
|
328
|
+
:is="renderer"
|
|
329
|
+
ref="renderer"
|
|
330
|
+
v-bind="rendererProps"
|
|
331
|
+
@selectObject="onSelectObject" />
|
|
332
|
+
</CnWidgetWrapper>
|
|
333
|
+
|
|
308
334
|
<!-- Every other content-driven catalog widget, and every bare-mode
|
|
309
335
|
catalog widget. These self-fetch from OpenRegister. The object
|
|
310
336
|
context is forwarded too (the same shape CnWidgetGrid merges on the
|
|
@@ -857,6 +883,19 @@ export default {
|
|
|
857
883
|
return typeof entry === 'object' && entry.chrome === 'card'
|
|
858
884
|
},
|
|
859
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Whether a `sections` widget gets one titled card: on a card surface
|
|
888
|
+
* outside a body grid cell, with a title, and not in `cards` mode.
|
|
889
|
+
*
|
|
890
|
+
* @return {boolean}
|
|
891
|
+
*/
|
|
892
|
+
wrapsSections() {
|
|
893
|
+
if (this.isBare || this.inGridCell || !this.widget || this.widget.type !== 'sections' || !this.widget.title) {
|
|
894
|
+
return false
|
|
895
|
+
}
|
|
896
|
+
return !(this.widget.content && this.widget.content.cards)
|
|
897
|
+
},
|
|
898
|
+
|
|
860
899
|
/** @return {boolean} true when the registry entry declares `card`. */
|
|
861
900
|
isCard() {
|
|
862
901
|
return isCardWidgetDef(this.widget)
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
sidebar) is left out, and Show in Files covers it. -->
|
|
281
281
|
<div v-else
|
|
282
282
|
class="cn-files-browser__body"
|
|
283
|
-
:class="{ 'cn-files-browser__body--with-side': sideNode !== null }">
|
|
283
|
+
:class="{ 'cn-files-browser__body--with-side': sideNode !== null && !sideInPage }">
|
|
284
284
|
<table class="cn-files-browser__table" data-testid="cn-files-browser-table">
|
|
285
285
|
<thead>
|
|
286
286
|
<tr>
|
|
@@ -560,43 +560,50 @@
|
|
|
560
560
|
</tbody>
|
|
561
561
|
</table>
|
|
562
562
|
|
|
563
|
-
<!-- `sideCards`: cards about the last selected file, beside the list
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
class="cn-files-browser__side
|
|
571
|
-
:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
@click="runHostAction(action, sideNode)">
|
|
592
|
-
<template v-if="action.icon" #icon>
|
|
593
|
-
<CnIcon :name="action.icon" :size="20" />
|
|
563
|
+
<!-- `sideCards`: cards about the last selected file, beside the list,
|
|
564
|
+
or in the detail page's side column (`sideCardsPlacement`). -->
|
|
565
|
+
<Teleport v-if="sideNode !== null"
|
|
566
|
+
:to="pageSideTarget"
|
|
567
|
+
:disabled="!sideInPage"
|
|
568
|
+
defer>
|
|
569
|
+
<aside
|
|
570
|
+
class="cn-files-browser__side"
|
|
571
|
+
:class="{ 'cn-files-browser__side--in-page': sideInPage }"
|
|
572
|
+
:aria-label="t('nextcloud-vue', 'About {name}', { name: sideNode.basename })"
|
|
573
|
+
data-testid="cn-files-browser-side">
|
|
574
|
+
<section v-for="card in sideCardsFor(sideNode)"
|
|
575
|
+
:key="card.id"
|
|
576
|
+
class="cn-files-browser__side-card"
|
|
577
|
+
:data-testid="`cn-files-browser-side-card-${card.id}`">
|
|
578
|
+
<h3 class="cn-files-browser__side-title">
|
|
579
|
+
{{ card.title }}
|
|
580
|
+
</h3>
|
|
581
|
+
<dl v-if="card.fields.length > 0" class="cn-files-browser__side-facts">
|
|
582
|
+
<template v-for="field in card.fields" :key="field.key">
|
|
583
|
+
<dt>{{ field.label }}</dt>
|
|
584
|
+
<dd :class="field.tone ? `cn-files-browser__tone cn-files-browser__tone--${field.tone}` : ''">
|
|
585
|
+
<CnCellRenderer
|
|
586
|
+
:value="field.value"
|
|
587
|
+
:formatter="field.formatter || null"
|
|
588
|
+
:formatterOptions="field.formatterOptions || null"
|
|
589
|
+
:row="sideNode" />
|
|
590
|
+
</dd>
|
|
594
591
|
</template>
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
592
|
+
</dl>
|
|
593
|
+
<div v-if="card.actions.length > 0" class="cn-files-browser__side-actions">
|
|
594
|
+
<NcButton v-for="action in card.actions"
|
|
595
|
+
:key="action.id"
|
|
596
|
+
:data-testid="`cn-files-browser-side-action-${action.id}`"
|
|
597
|
+
@click="runHostAction(action, sideNode)">
|
|
598
|
+
<template v-if="action.icon" #icon>
|
|
599
|
+
<CnIcon :name="action.icon" :size="20" />
|
|
600
|
+
</template>
|
|
601
|
+
{{ hostLabel(action) }}
|
|
602
|
+
</NcButton>
|
|
603
|
+
</div>
|
|
604
|
+
</section>
|
|
605
|
+
</aside>
|
|
606
|
+
</Teleport>
|
|
600
607
|
</div>
|
|
601
608
|
|
|
602
609
|
<p v-if="dropHint && canChange && !loading && error === ''"
|
|
@@ -819,6 +826,8 @@ export default {
|
|
|
819
826
|
// action or column the manifest declares is authored in English as
|
|
820
827
|
// the catalogue key, and read through this in the user's language.
|
|
821
828
|
cnTranslate: { default: () => (key) => key },
|
|
829
|
+
// The detail page's side column (CnDetailPage), for side cards placed there.
|
|
830
|
+
cnDetailSideColumn: { default: null },
|
|
822
831
|
},
|
|
823
832
|
|
|
824
833
|
props: {
|
|
@@ -1213,6 +1222,17 @@ export default {
|
|
|
1213
1222
|
default: () => [],
|
|
1214
1223
|
},
|
|
1215
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Where the side cards stand: `beside` (default) next to the list, or
|
|
1227
|
+
* `page-side` in the detail page's side column, in place of its
|
|
1228
|
+
* generic cards while a file is selected. Without a page side column
|
|
1229
|
+
* they stay beside the list.
|
|
1230
|
+
*/
|
|
1231
|
+
sideCardsPlacement: {
|
|
1232
|
+
type: String,
|
|
1233
|
+
default: 'beside',
|
|
1234
|
+
},
|
|
1235
|
+
|
|
1216
1236
|
/**
|
|
1217
1237
|
* The key of a declared column to group the files on: a heading row
|
|
1218
1238
|
* per value, in the order each is first met, with its count. Files
|
|
@@ -1487,6 +1507,21 @@ export default {
|
|
|
1487
1507
|
* @return {object|null}
|
|
1488
1508
|
* @spec openspec/changes/files-browser-side-cards/specs/files-browser/spec.md#requirement-cards-beside-the-list-describe-the-selected-file
|
|
1489
1509
|
*/
|
|
1510
|
+
/** @return {boolean} Whether the side cards fill the page's side column now. */
|
|
1511
|
+
sideClaimed() {
|
|
1512
|
+
return this.sideInPage && this.sideNode !== null
|
|
1513
|
+
},
|
|
1514
|
+
|
|
1515
|
+
/** @return {boolean} Whether the side cards stand in the page's side column. */
|
|
1516
|
+
sideInPage() {
|
|
1517
|
+
return this.sideCardsPlacement === 'page-side' && this.cnDetailSideColumn !== null && this.cnDetailSideColumn.available === true
|
|
1518
|
+
},
|
|
1519
|
+
|
|
1520
|
+
/** @return {string} Where the side cards are teleported to in the page. */
|
|
1521
|
+
pageSideTarget() {
|
|
1522
|
+
return this.cnDetailSideColumn?.target || 'body'
|
|
1523
|
+
},
|
|
1524
|
+
|
|
1490
1525
|
sideNode() {
|
|
1491
1526
|
if (!Array.isArray(this.sideCards) || this.sideCards.length === 0 || this.selectedIds.length === 0) {
|
|
1492
1527
|
return null
|
|
@@ -1658,6 +1693,17 @@ export default {
|
|
|
1658
1693
|
},
|
|
1659
1694
|
|
|
1660
1695
|
watch: {
|
|
1696
|
+
/**
|
|
1697
|
+
* Tell the page whether the side cards stand in its side column now,
|
|
1698
|
+
* so its generic cards stand aside meanwhile.
|
|
1699
|
+
*/
|
|
1700
|
+
sideClaimed: {
|
|
1701
|
+
immediate: true,
|
|
1702
|
+
handler(on) {
|
|
1703
|
+
this.cnDetailSideColumn?.claim?.(this.$.uid, on)
|
|
1704
|
+
},
|
|
1705
|
+
},
|
|
1706
|
+
|
|
1661
1707
|
rootPath(next) {
|
|
1662
1708
|
this.currentPath = next
|
|
1663
1709
|
this.refresh()
|
|
@@ -1681,6 +1727,7 @@ export default {
|
|
|
1681
1727
|
},
|
|
1682
1728
|
|
|
1683
1729
|
beforeUnmount() {
|
|
1730
|
+
this.cnDetailSideColumn?.claim?.(this.$.uid, false)
|
|
1684
1731
|
for (const channel of FOLDER_CHANGED_CHANNELS) {
|
|
1685
1732
|
unsubscribe(channel, this.onFolderChanged)
|
|
1686
1733
|
}
|
|
@@ -3072,6 +3119,17 @@ export default {
|
|
|
3072
3119
|
gap: 12px;
|
|
3073
3120
|
}
|
|
3074
3121
|
|
|
3122
|
+
/* In the detail page's side column the cards take its gap and its card
|
|
3123
|
+
anatomy, as the page's own side cards are drawn. */
|
|
3124
|
+
.cn-files-browser__side--in-page {
|
|
3125
|
+
gap: calc(5 * var(--default-grid-baseline));
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
.cn-files-browser__side--in-page .cn-files-browser__side-card {
|
|
3129
|
+
padding: var(--cn-board-card-padding, 16px 18px);
|
|
3130
|
+
border-radius: var(--cn-board-card-radius, var(--border-radius-large));
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3075
3133
|
.cn-files-browser__side-card {
|
|
3076
3134
|
display: flex;
|
|
3077
3135
|
flex-direction: column;
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
:edges="canvasEdgesWithRunState"
|
|
203
203
|
:minZoom="minZoom"
|
|
204
204
|
:maxZoom="maxZoom"
|
|
205
|
+
:fitMaxZoom="fitMaxZoom"
|
|
205
206
|
@zoomChange="zoom = $event"
|
|
206
207
|
@nodeSelect="onNodeSelect"
|
|
207
208
|
@nodeActivate="onNodeActivate"
|
|
@@ -448,6 +449,25 @@ export default {
|
|
|
448
449
|
default: '',
|
|
449
450
|
},
|
|
450
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Which way the automatic layout runs: `right` (default) left to
|
|
454
|
+
* right, `down` top to bottom with branches side by side.
|
|
455
|
+
*/
|
|
456
|
+
layoutDirection: {
|
|
457
|
+
type: String,
|
|
458
|
+
default: 'right',
|
|
459
|
+
validator: (value) => ['right', 'down'].includes(value),
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The most the first fit may zoom in. 0 lets it zoom to `maxZoom`; 1
|
|
464
|
+
* keeps a small graph at its own size instead of blowing it up.
|
|
465
|
+
*/
|
|
466
|
+
fitMaxZoom: {
|
|
467
|
+
type: Number,
|
|
468
|
+
default: 0,
|
|
469
|
+
},
|
|
470
|
+
|
|
451
471
|
/**
|
|
452
472
|
* Show Save, Run and Check in the canvas toolbar. Off when the host
|
|
453
473
|
* draws them itself, as the board look does in its page header.
|
|
@@ -1523,6 +1543,7 @@ export default {
|
|
|
1523
1543
|
* @return {void}
|
|
1524
1544
|
*/
|
|
1525
1545
|
created() {
|
|
1546
|
+
this.store.layoutDirection = this.layoutDirection
|
|
1526
1547
|
this.store.beginBootstrap(this.run)
|
|
1527
1548
|
},
|
|
1528
1549
|
|