@conduction/nextcloud-vue 2.93.0 → 2.95.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/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +46 -28
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +91 -36
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +115 -72
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +32 -3
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
- package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
- package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
- package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
- package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +51 -4
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +230 -25
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +20 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +9 -0
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +18 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +18 -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 +712 -330
- package/dist/esm/utils/dueText.js +49 -8
- package/dist/esm/utils/dueText.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8275 -6302
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +712 -330
- package/l10n/en.json +18 -1
- package/l10n/nl.json +18 -1
- package/package.json +1 -1
- package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
- package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +96 -2
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +39 -10
- package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
- package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
- package/src/components/CnDeviceFramesWidget/index.js +29 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
- package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
- package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
- package/src/components/CnPageHeader/CnPageHeader.md +11 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
- package/src/components/CnPagination/CnPagination.vue +10 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +84 -4
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
- package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
- package/src/components/CnTabs/CnTabs.vue +39 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +31 -2
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +9 -0
- package/src/css/dialog.css +14 -0
- package/src/css/look-board-index.css +27 -1
- package/src/css/look-board.css +27 -0
- package/src/css/page-header.css +16 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +27 -15
- package/src/utils/dueText.js +49 -8
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-device-frames-widget" data-testid="cn-device-frames-widget">
|
|
8
|
+
<section
|
|
9
|
+
v-for="frame in frames"
|
|
10
|
+
:key="frame.key"
|
|
11
|
+
class="cn-device-frames-widget__frame"
|
|
12
|
+
:style="frameStyle"
|
|
13
|
+
:aria-labelledby="headingId(frame.key)"
|
|
14
|
+
:data-testid="`cn-device-frame-${frame.key}`">
|
|
15
|
+
<header class="cn-device-frames-widget__head">
|
|
16
|
+
<span
|
|
17
|
+
v-if="frame.status"
|
|
18
|
+
class="cn-device-frames-widget__status"
|
|
19
|
+
:class="`cn-device-frames-widget__status--${frame.status.tone}`"
|
|
20
|
+
data-testid="cn-device-frame-status">
|
|
21
|
+
<component :is="frame.status.icon" :size="16" aria-hidden="true" />
|
|
22
|
+
{{ frame.status.text }}
|
|
23
|
+
</span>
|
|
24
|
+
<h3 :id="headingId(frame.key)" class="cn-device-frames-widget__title">
|
|
25
|
+
{{ frame.title }}
|
|
26
|
+
</h3>
|
|
27
|
+
</header>
|
|
28
|
+
<div class="cn-device-frames-widget__body">
|
|
29
|
+
<CnSectionsWidget
|
|
30
|
+
:content="{ sections: frame.sections, cards: false, hideEmpty: true }"
|
|
31
|
+
:availableWidgets="availableWidgets"
|
|
32
|
+
:objectId="objectId"
|
|
33
|
+
:objectData="objectData"
|
|
34
|
+
:objectType="objectType"
|
|
35
|
+
:schemaObject="schemaObject"
|
|
36
|
+
:register="register"
|
|
37
|
+
:schema="schema"
|
|
38
|
+
:store="store"
|
|
39
|
+
:surface="surface"
|
|
40
|
+
:integrationContext="integrationContext"
|
|
41
|
+
:cnRegistry="cnRegistry" />
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
import { markRaw } from 'vue'
|
|
49
|
+
import CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'
|
|
50
|
+
import CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'
|
|
51
|
+
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
52
|
+
import CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'
|
|
53
|
+
|
|
54
|
+
/** Counter for unique heading ids across instances. */
|
|
55
|
+
let instanceCounter = 0
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CnDeviceFramesWidget - the `device-frames` widget: several small pages side
|
|
59
|
+
* by side, each in a phone frame with its own header (board DqInspectie,
|
|
60
|
+
* "Inspectie in het veld": the day offline, a checklist question with a
|
|
61
|
+
* photo, and the sync queue).
|
|
62
|
+
*
|
|
63
|
+
* Each frame stacks widgets of the surface, as the `sections` widget does,
|
|
64
|
+
* under a header with the frame's title and an optional status line. The
|
|
65
|
+
* status can follow the connection ("Offline · synced at 07:42" while the
|
|
66
|
+
* device is offline). The frames wrap onto the next row when the page is
|
|
67
|
+
* narrower, and a frame takes the full width on a phone.
|
|
68
|
+
*
|
|
69
|
+
* ```json
|
|
70
|
+
* {
|
|
71
|
+
* "type": "device-frames",
|
|
72
|
+
* "content": {
|
|
73
|
+
* "width": 350,
|
|
74
|
+
* "status": { "followConnection": true, "offlineText": "Offline · synced at 07:42", "onlineText": "Online" },
|
|
75
|
+
* "frames": [
|
|
76
|
+
* { "key": "today", "title": "Today", "widgetIds": ["today-runs", "offline-note"] },
|
|
77
|
+
* { "key": "question", "title": "Kerkplein 7", "widgetIds": ["checklist-run"] },
|
|
78
|
+
* { "key": "sync", "title": "Synchronise", "widgetIds": ["sync-queue"] }
|
|
79
|
+
* ]
|
|
80
|
+
* }
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames
|
|
85
|
+
*/
|
|
86
|
+
export default {
|
|
87
|
+
name: 'CnDeviceFramesWidget',
|
|
88
|
+
|
|
89
|
+
components: { CnSectionsWidget },
|
|
90
|
+
|
|
91
|
+
inject: {
|
|
92
|
+
cnTranslate: { default: () => (key) => key },
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
props: {
|
|
96
|
+
/**
|
|
97
|
+
* The widget configuration.
|
|
98
|
+
*
|
|
99
|
+
* - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names
|
|
100
|
+
* widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.
|
|
101
|
+
* - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,
|
|
102
|
+
* followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads
|
|
103
|
+
* `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`
|
|
104
|
+
* (default while offline), `success` or `info`.
|
|
105
|
+
* - `width` — the frame width in pixels (default 350).
|
|
106
|
+
*
|
|
107
|
+
* @type {{frames?: Array<object>, status?: object, width?: number}}
|
|
108
|
+
*/
|
|
109
|
+
content: {
|
|
110
|
+
type: Object,
|
|
111
|
+
default: () => ({}),
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** Sibling widget definitions on the surface, for `widgetIds`. */
|
|
115
|
+
availableWidgets: {
|
|
116
|
+
type: Array,
|
|
117
|
+
default: () => [],
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** The bound record's id, forwarded to the children. */
|
|
121
|
+
objectId: {
|
|
122
|
+
type: [String, Number],
|
|
123
|
+
default: '',
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/** The loaded record, forwarded to the children. */
|
|
127
|
+
objectData: {
|
|
128
|
+
type: Object,
|
|
129
|
+
default: null,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** The resolved object-type slug. */
|
|
133
|
+
objectType: {
|
|
134
|
+
type: String,
|
|
135
|
+
default: '',
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/** The resolved JSON Schema object. */
|
|
139
|
+
schemaObject: {
|
|
140
|
+
type: Object,
|
|
141
|
+
default: null,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/** OpenRegister register slug of the surface. */
|
|
145
|
+
register: {
|
|
146
|
+
type: [String, Object],
|
|
147
|
+
default: '',
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** OpenRegister schema slug of the surface. */
|
|
151
|
+
schema: {
|
|
152
|
+
type: [String, Object],
|
|
153
|
+
default: '',
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/** The effective object store. */
|
|
157
|
+
store: {
|
|
158
|
+
type: Object,
|
|
159
|
+
default: null,
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/** Rendering surface forwarded to the children. */
|
|
163
|
+
surface: {
|
|
164
|
+
type: String,
|
|
165
|
+
default: 'app-dashboard',
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/** Object context forwarded to integration children. */
|
|
169
|
+
integrationContext: {
|
|
170
|
+
type: Object,
|
|
171
|
+
default: null,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/** The consumer's component registry, for custom child widget types. */
|
|
175
|
+
cnRegistry: {
|
|
176
|
+
type: Object,
|
|
177
|
+
default: () => ({}),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
data() {
|
|
182
|
+
instanceCounter += 1
|
|
183
|
+
return {
|
|
184
|
+
uid: instanceCounter,
|
|
185
|
+
online: typeof navigator === 'undefined' || navigator.onLine !== false,
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
computed: {
|
|
190
|
+
/** @return {object} The frame width. */
|
|
191
|
+
frameStyle() {
|
|
192
|
+
const width = Number(this.content?.width)
|
|
193
|
+
return { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The frames to draw: a key, a translated title, the sections and the
|
|
198
|
+
* status line.
|
|
199
|
+
*
|
|
200
|
+
* @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}
|
|
201
|
+
*/
|
|
202
|
+
frames() {
|
|
203
|
+
const list = Array.isArray(this.content?.frames) ? this.content.frames : []
|
|
204
|
+
return list
|
|
205
|
+
.filter((frame) => frame && typeof frame === 'object')
|
|
206
|
+
.map((frame, index) => {
|
|
207
|
+
const sections = Array.isArray(frame.sections)
|
|
208
|
+
? frame.sections
|
|
209
|
+
: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))
|
|
210
|
+
return {
|
|
211
|
+
key: String(frame.key || `frame-${index}`).replace(/[^\w-]/g, '-'),
|
|
212
|
+
title: this.tr(frame.title || ''),
|
|
213
|
+
sections,
|
|
214
|
+
status: this.statusOf(frame.status || this.content?.status || null),
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
mounted() {
|
|
221
|
+
if (typeof window !== 'undefined') {
|
|
222
|
+
window.addEventListener('online', this.onConnection)
|
|
223
|
+
window.addEventListener('offline', this.onConnection)
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
beforeUnmount() {
|
|
228
|
+
if (typeof window !== 'undefined') {
|
|
229
|
+
window.removeEventListener('online', this.onConnection)
|
|
230
|
+
window.removeEventListener('offline', this.onConnection)
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
methods: {
|
|
235
|
+
/**
|
|
236
|
+
* Translate through the app's translate.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} key The source string.
|
|
239
|
+
* @return {string}
|
|
240
|
+
*/
|
|
241
|
+
tr(key) {
|
|
242
|
+
return key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The status line of a frame, or null.
|
|
247
|
+
*
|
|
248
|
+
* @param {object|null} status The configured status.
|
|
249
|
+
* @return {{text: string, tone: string, icon: object}|null}
|
|
250
|
+
*/
|
|
251
|
+
statusOf(status) {
|
|
252
|
+
if (!status || typeof status !== 'object') {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
let text = status.text
|
|
256
|
+
let tone = status.tone
|
|
257
|
+
if (status.followConnection === true) {
|
|
258
|
+
text = this.online ? status.onlineText : status.offlineText
|
|
259
|
+
tone = tone || (this.online ? 'success' : 'warning')
|
|
260
|
+
}
|
|
261
|
+
if (typeof text !== 'string' || text === '') {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
const known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'
|
|
265
|
+
const icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)
|
|
266
|
+
return { text: this.tr(text), tone: known, icon: markRaw(icon) }
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/** Follow the browser's connection state. */
|
|
270
|
+
onConnection() {
|
|
271
|
+
this.online = typeof navigator === 'undefined' || navigator.onLine !== false
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The id of a frame's heading.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} key The frame key.
|
|
278
|
+
* @return {string}
|
|
279
|
+
*/
|
|
280
|
+
headingId(key) {
|
|
281
|
+
return `cn-device-frame-${this.uid}-${key}`
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
</script>
|
|
286
|
+
|
|
287
|
+
<style scoped>
|
|
288
|
+
.cn-device-frames-widget {
|
|
289
|
+
display: flex;
|
|
290
|
+
flex-wrap: wrap;
|
|
291
|
+
align-items: flex-start;
|
|
292
|
+
gap: 20px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.cn-device-frames-widget__frame {
|
|
296
|
+
display: flex;
|
|
297
|
+
flex: none;
|
|
298
|
+
flex-direction: column;
|
|
299
|
+
width: var(--cn-device-frame-width, 350px);
|
|
300
|
+
max-width: 100%;
|
|
301
|
+
min-height: 600px;
|
|
302
|
+
box-sizing: border-box;
|
|
303
|
+
overflow: hidden;
|
|
304
|
+
border: 10px solid var(--color-main-text);
|
|
305
|
+
border-radius: 36px;
|
|
306
|
+
background: var(--color-background-hover);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.cn-device-frames-widget__head {
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
gap: 6px;
|
|
313
|
+
padding: 14px 18px 10px;
|
|
314
|
+
border-bottom: 1px solid var(--color-border);
|
|
315
|
+
background: var(--color-main-background);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.cn-device-frames-widget__status {
|
|
319
|
+
display: inline-flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
gap: 6px;
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
font-weight: 600;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.cn-device-frames-widget__status--warning {
|
|
327
|
+
color: var(--color-warning-text, var(--color-main-text));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.cn-device-frames-widget__status--success {
|
|
331
|
+
color: var(--color-success-text, var(--color-main-text));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.cn-device-frames-widget__status--info {
|
|
335
|
+
color: var(--color-text-maxcontrast);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.cn-device-frames-widget__title {
|
|
339
|
+
margin: 0;
|
|
340
|
+
font-size: 20px;
|
|
341
|
+
font-weight: 700;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.cn-device-frames-widget__body {
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
gap: 12px;
|
|
348
|
+
padding: 14px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* On a phone the frame is the screen: no bezel, the full width. */
|
|
352
|
+
@media (max-width: 640px) {
|
|
353
|
+
.cn-device-frames-widget__frame {
|
|
354
|
+
width: 100%;
|
|
355
|
+
min-height: 0;
|
|
356
|
+
border-width: 0;
|
|
357
|
+
border-radius: var(--cn-board-card-radius, 12px);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnDeviceFramesWidget — self-registers the `device-frames` widget type
|
|
6
|
+
* (several small pages side by side, each in a phone frame) into the shared
|
|
7
|
+
* dashboardWidgetRegistry at module load. A container: it renders other
|
|
8
|
+
* widgets of the surface, so the host hands it their definitions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnDeviceFramesWidget from './CnDeviceFramesWidget.vue'
|
|
12
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('device-frames', {
|
|
15
|
+
renderer: CnDeviceFramesWidget,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
frames: [],
|
|
19
|
+
width: 350,
|
|
20
|
+
},
|
|
21
|
+
displayName: 'Device frames',
|
|
22
|
+
icon: 'CellphoneText',
|
|
23
|
+
container: true,
|
|
24
|
+
ownsTitle: true,
|
|
25
|
+
surfaces: ['app-dashboard', 'detail-page'],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export { CnDeviceFramesWidget }
|
|
29
|
+
export default CnDeviceFramesWidget
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
<template v-for="block in blocks" :key="block.key">
|
|
19
19
|
<dl
|
|
20
20
|
v-if="block.kind === 'rows'"
|
|
21
|
-
class="cn-facts-widget__rows"
|
|
21
|
+
class="cn-facts-widget__rows"
|
|
22
|
+
:class="{ 'cn-facts-widget__rows--columns': twoColumns }">
|
|
22
23
|
<div
|
|
23
24
|
v-for="row in block.rows"
|
|
24
25
|
:key="row.key"
|
|
@@ -268,6 +269,7 @@ export default {
|
|
|
268
269
|
* - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
|
|
269
270
|
* - `link` — `{ label, route? | href? }` at the foot.
|
|
270
271
|
* - `hideEmpty` — drop an entry whose value is empty (default true).
|
|
272
|
+
* - `columns` — `2` lays the rows out in two columns with the label above the value (board DqContact); a row with `parts` joins them like a line (an address on one row).
|
|
271
273
|
* - `emptyText` — shown when no entry has a value.
|
|
272
274
|
*
|
|
273
275
|
* @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}
|
|
@@ -310,6 +312,16 @@ export default {
|
|
|
310
312
|
},
|
|
311
313
|
|
|
312
314
|
computed: {
|
|
315
|
+
/**
|
|
316
|
+
* Whether the rows sit in two columns, label above value.
|
|
317
|
+
*
|
|
318
|
+
* @spec openspec/changes/facts-columns/specs/facts-widget/spec.md#requirement-facts-rows-can-sit-in-two-columns
|
|
319
|
+
* @return {boolean}
|
|
320
|
+
*/
|
|
321
|
+
twoColumns() {
|
|
322
|
+
return Number(this.content?.columns) === 2
|
|
323
|
+
},
|
|
324
|
+
|
|
313
325
|
/**
|
|
314
326
|
* The token context: the detail object (prop, else the injected context)
|
|
315
327
|
* and the page workspace.
|
|
@@ -546,7 +558,7 @@ export default {
|
|
|
546
558
|
if (kind === 'lead') {
|
|
547
559
|
return this.partsValue(record, field.title)
|
|
548
560
|
}
|
|
549
|
-
if ((kind === 'line' || kind === 'text') && Array.isArray(field.parts)) {
|
|
561
|
+
if ((kind === 'line' || kind === 'text' || kind === 'row') && Array.isArray(field.parts)) {
|
|
550
562
|
return this.lineValue(record, field)
|
|
551
563
|
}
|
|
552
564
|
if ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {
|
|
@@ -924,6 +936,11 @@ export default {
|
|
|
924
936
|
if (!spec.path && typeof spec.text === 'string') {
|
|
925
937
|
return this.tr(spec.text)
|
|
926
938
|
}
|
|
939
|
+
// A group of parts with its own separator ("Parkstraat 24" inside
|
|
940
|
+
// "Parkstraat 24, 3421 AB Zuiddrecht").
|
|
941
|
+
if (!spec.path && Array.isArray(spec.parts)) {
|
|
942
|
+
return this.lineValue(record, spec)
|
|
943
|
+
}
|
|
927
944
|
const value = this.format(readPath(record, spec.path), spec, record)
|
|
928
945
|
if (value === '') {
|
|
929
946
|
return ''
|
|
@@ -1240,4 +1257,31 @@ export default {
|
|
|
1240
1257
|
color: var(--color-primary-element);
|
|
1241
1258
|
text-decoration: underline;
|
|
1242
1259
|
}
|
|
1260
|
+
|
|
1261
|
+
/* `columns: 2` (board DqContact): two columns, the label above the value, a
|
|
1262
|
+
hairline under each fact; one column on a phone. */
|
|
1263
|
+
.cn-facts-widget__rows--columns {
|
|
1264
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1265
|
+
gap: 14px 24px;
|
|
1266
|
+
font-size: 15px;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.cn-facts-widget__rows--columns .cn-facts-widget__row {
|
|
1270
|
+
display: flex;
|
|
1271
|
+
flex-direction: column;
|
|
1272
|
+
gap: 3px;
|
|
1273
|
+
min-width: 0;
|
|
1274
|
+
padding-bottom: 10px;
|
|
1275
|
+
border-bottom: 1px solid var(--color-border);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.cn-facts-widget__rows--columns .cn-facts-widget__label.cn-facts-widget__label {
|
|
1279
|
+
font-size: 13px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
@media (max-width: 640px) {
|
|
1283
|
+
.cn-facts-widget__rows--columns {
|
|
1284
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1243
1287
|
</style>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnHeaderSearchLabel — the app's own words on the search field in Nextcloud's
|
|
3
|
+
header (screens-board-top-bar-search). Renders nothing of its own.
|
|
4
|
+
|
|
5
|
+
Under the board look the screens give the top bar's search field a line that
|
|
6
|
+
names what the app holds ("Zoek zaken, documenten of contacten"), where
|
|
7
|
+
Nextcloud shows its global "Search apps, files, tags, messages …". Nextcloud
|
|
8
|
+
offers no way for an app to set that line, so this component replaces the
|
|
9
|
+
TEXT of the field's label and nothing else:
|
|
10
|
+
- it changes only the text of `.unified-search-input__label` (which is also
|
|
11
|
+
the search button's accessible name); it never adds, moves or removes a node;
|
|
12
|
+
- it writes the text again when Nextcloud re-renders the field, and only
|
|
13
|
+
when the text differs, so its own write never loops;
|
|
14
|
+
- on unmount it puts Nextcloud's text back;
|
|
15
|
+
- with no header or no such label it does nothing.
|
|
16
|
+
|
|
17
|
+
The search Nextcloud opens is unchanged: this is the resting field's line.
|
|
18
|
+
-->
|
|
19
|
+
<script>
|
|
20
|
+
const LABEL_SELECTOR = '.unified-search-input__label'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Put the app's own line on the search field in Nextcloud's header.
|
|
24
|
+
*
|
|
25
|
+
* @spec openspec/changes/screens-board-top-bar-search/specs/layout-components/spec.md#requirement-the-board-look-names-what-the-header-search-finds
|
|
26
|
+
*/
|
|
27
|
+
export default {
|
|
28
|
+
name: 'CnHeaderSearchLabel',
|
|
29
|
+
|
|
30
|
+
props: {
|
|
31
|
+
/**
|
|
32
|
+
* The line to show, already translated.
|
|
33
|
+
*
|
|
34
|
+
* @type {string}
|
|
35
|
+
*/
|
|
36
|
+
label: {
|
|
37
|
+
type: String,
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
data() {
|
|
43
|
+
return { observer: null, original: null, labelNode: null }
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
watch: {
|
|
47
|
+
label() {
|
|
48
|
+
this.apply()
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
mounted() {
|
|
53
|
+
const header = document.getElementById('header')
|
|
54
|
+
if (!header) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
this.apply()
|
|
58
|
+
if (typeof MutationObserver === 'function') {
|
|
59
|
+
this.observer = new MutationObserver(() => this.apply())
|
|
60
|
+
this.observer.observe(header, { childList: true, subtree: true, characterData: true })
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
beforeUnmount() {
|
|
65
|
+
this.observer?.disconnect()
|
|
66
|
+
this.observer = null
|
|
67
|
+
if (this.labelNode && this.original !== null && this.labelNode.isConnected) {
|
|
68
|
+
this.labelNode.textContent = this.original
|
|
69
|
+
}
|
|
70
|
+
this.labelNode = null
|
|
71
|
+
this.original = null
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
methods: {
|
|
75
|
+
/**
|
|
76
|
+
* Write the line into the label when it shows something else.
|
|
77
|
+
* Remembers the first text Nextcloud drew in each label node, to put back.
|
|
78
|
+
*
|
|
79
|
+
* @return {void}
|
|
80
|
+
*/
|
|
81
|
+
apply() {
|
|
82
|
+
const node = document.querySelector('#header ' + LABEL_SELECTOR)
|
|
83
|
+
if (!node || !this.label) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (node !== this.labelNode) {
|
|
87
|
+
this.labelNode = node
|
|
88
|
+
this.original = node.textContent
|
|
89
|
+
}
|
|
90
|
+
if (node.textContent.trim() === this.label) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
// Keep Nextcloud's own text node where there is one, so its renderer still holds the live node.
|
|
94
|
+
const text = node.childNodes.length === 1 && node.firstChild.nodeType === 3 ? node.firstChild : null
|
|
95
|
+
if (text) {
|
|
96
|
+
text.data = this.label
|
|
97
|
+
} else {
|
|
98
|
+
node.textContent = this.label
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
render() {
|
|
104
|
+
return null
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
</script>
|