@conduction/nextcloud-vue 2.90.0 → 2.92.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/CnDataTable/CnDataTable.vue2.js +10 -5
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +15 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnDialog/CnDialog.vue2.js +1 -1
- package/dist/esm/components/CnDialog/CnDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnDialog/CnDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnDialogHeader/CnDialogHeader.vue2.js +1 -1
- package/dist/esm/components/CnDialogHeader/CnDialogHeader.vue2.js.map +1 -1
- package/dist/esm/components/CnDialogHeader/CnDialogHeader.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +18 -10
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +16 -8
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +16 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +6 -3
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +22 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +21 -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 +328 -114
- package/dist/esm/utils/boardDate.js +35 -1
- package/dist/esm/utils/boardDate.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +12 -3
- package/dist/esm/utils/fillTemplate.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 +11569 -10871
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +328 -114
- package/l10n/en.json +22 -1
- package/l10n/nl.json +21 -1
- package/package.json +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +10 -5
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +16 -2
- package/src/components/CnDialog/CnDialog.vue +1 -1
- package/src/components/CnDialog/index.js +7 -0
- package/src/components/CnDialogHeader/CnDialogHeader.vue +1 -1
- package/src/components/CnDialogHeader/index.js +7 -0
- package/src/components/CnFormDialog/CnFormDialog.md +1 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +13 -0
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +17 -2
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
- package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +6 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
- package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
- package/src/components/index.js +2 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +59 -2
- package/src/utils/boardDate.js +34 -0
- package/src/utils/fillTemplate.js +12 -3
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -187,7 +187,7 @@ import { resolveTabCount } from '../../utils/detailActionModel.js'
|
|
|
187
187
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
188
188
|
import { isActivityWidget } from '../../utils/headerMeta.js'
|
|
189
189
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
190
|
-
import { evaluateVisibleWhen, evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
|
|
190
|
+
import { evaluateVisibleWhen, evaluateVisibleWhenLocal, evaluateWorkspaceCondition, isLocallyDecidableVisibleWhen, isWorkspaceCondition } from '../../utils/visibleWhen.js'
|
|
191
191
|
import { widgetTitleOf } from '../../utils/widgetDispatch.js'
|
|
192
192
|
import { CnActionsMenu } from '../CnActionsMenu/index.js'
|
|
193
193
|
|
|
@@ -331,6 +331,13 @@ export default {
|
|
|
331
331
|
* `visibleWhen` reads them as `@context.<name>`. Null on any other page.
|
|
332
332
|
*/
|
|
333
333
|
cnDetailVisibilityRows: { default: null },
|
|
334
|
+
/**
|
|
335
|
+
* The page's workspace bag (CnDashboardPage provides it), which a
|
|
336
|
+
* page control such as a chips filter writes. A widget in a stack is
|
|
337
|
+
* left out while its workspace `visibleWhen` does not hold. Null off a
|
|
338
|
+
* dashboard.
|
|
339
|
+
*/
|
|
340
|
+
cnWorkspaceContext: { default: null },
|
|
334
341
|
/**
|
|
335
342
|
* The host's translate (CnAppRoot provides `cnTranslate`), so a tab
|
|
336
343
|
* label written in the manifest reads in the user's language.
|
|
@@ -588,7 +595,8 @@ export default {
|
|
|
588
595
|
// widget is skipped. The first that resolves is the panel's.
|
|
589
596
|
const ids = this.tabWidgetIds(tab)
|
|
590
597
|
const widgets = ids.length > 1
|
|
591
|
-
? ids.map((id) => this.availableWidgets.find((w) => w && w.id === id) || null)
|
|
598
|
+
? ids.map((id) => this.availableWidgets.find((w) => w && w.id === id) || null)
|
|
599
|
+
.filter((w) => w && !this.hiddenByWorkspace(w))
|
|
592
600
|
: []
|
|
593
601
|
const widget = ids.length > 1
|
|
594
602
|
? (widgets[0] || null)
|
|
@@ -837,6 +845,29 @@ export default {
|
|
|
837
845
|
},
|
|
838
846
|
|
|
839
847
|
methods: {
|
|
848
|
+
/**
|
|
849
|
+
* Whether a page control hides a widget of a stack right now: its
|
|
850
|
+
* `visibleWhen` (on the widget, its content or its props) is a
|
|
851
|
+
* workspace condition that does not hold. Off a dashboard there is no
|
|
852
|
+
* workspace, and nothing is hidden. Other conditions are not looked at
|
|
853
|
+
* here.
|
|
854
|
+
*
|
|
855
|
+
* @param {object} widget The widget definition.
|
|
856
|
+
* @return {boolean} True when the widget must not draw.
|
|
857
|
+
* @spec openspec/changes/tabs-stack-follows-page-controls/specs/tabs-widget/spec.md#requirement-a-widget-in-a-tab-stack-follows-the-page-controls
|
|
858
|
+
*/
|
|
859
|
+
hiddenByWorkspace(widget) {
|
|
860
|
+
const cond = widget.visibleWhen || widget.content?.visibleWhen || widget.props?.visibleWhen || null
|
|
861
|
+
if (!isWorkspaceCondition(cond)) {
|
|
862
|
+
return false
|
|
863
|
+
}
|
|
864
|
+
const bag = this.cnWorkspaceContext
|
|
865
|
+
if (!bag || typeof bag !== 'object') {
|
|
866
|
+
return false
|
|
867
|
+
}
|
|
868
|
+
return !evaluateWorkspaceCondition(cond, 'value' in bag ? bag.value : bag)
|
|
869
|
+
},
|
|
870
|
+
|
|
840
871
|
/**
|
|
841
872
|
* The grid a stack lays its widgets out in, as one column span (of 12)
|
|
842
873
|
* per widget in order, or null for the plain vertical stack. A tab says
|
|
@@ -60,6 +60,17 @@
|
|
|
60
60
|
:style="contentStyle">
|
|
61
61
|
{{ placeholderText }}
|
|
62
62
|
</span>
|
|
63
|
+
<a
|
|
64
|
+
v-if="link"
|
|
65
|
+
class="cn-text-widget__link"
|
|
66
|
+
data-testid="cn-text-widget-link"
|
|
67
|
+
:href="link.url"
|
|
68
|
+
:target="link.external ? '_blank' : null"
|
|
69
|
+
:rel="link.external ? 'noopener noreferrer' : null"
|
|
70
|
+
@click="openLink">
|
|
71
|
+
{{ link.label }}
|
|
72
|
+
<ChevronRight :size="16" />
|
|
73
|
+
</a>
|
|
63
74
|
</div>
|
|
64
75
|
</template>
|
|
65
76
|
|
|
@@ -67,6 +78,8 @@
|
|
|
67
78
|
import { translate as t } from '@nextcloud/l10n'
|
|
68
79
|
import DOMPurify from 'dompurify'
|
|
69
80
|
import { Marked } from 'marked'
|
|
81
|
+
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
82
|
+
import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
|
|
70
83
|
import { isPlaceholderCell } from '../../utils/textTable.js'
|
|
71
84
|
|
|
72
85
|
// Per-module `Marked` instance so configuration is scoped here and does NOT
|
|
@@ -120,13 +133,23 @@ function ensureDomPurifyHook() {
|
|
|
120
133
|
export default {
|
|
121
134
|
name: 'CnTextWidget',
|
|
122
135
|
|
|
136
|
+
components: { ChevronRight },
|
|
137
|
+
|
|
138
|
+
inject: {
|
|
139
|
+
/** The app's translate function, provided by CnAppRoot. Identity outside one. */
|
|
140
|
+
cnTranslate: { default: () => (key) => key },
|
|
141
|
+
},
|
|
142
|
+
|
|
123
143
|
props: {
|
|
124
144
|
/**
|
|
125
145
|
* Persisted widget content blob: `{text, fontSize, color,
|
|
126
|
-
* backgroundColor, textAlign, contentMode, tableMode, tableData}`.
|
|
146
|
+
* backgroundColor, textAlign, contentMode, tableMode, tableData, link}`.
|
|
127
147
|
* All keys are optional; missing keys collapse to documented defaults.
|
|
148
|
+
* `text` and `link.label` go through the app's translate function.
|
|
149
|
+
* `link` is `{label, route}` (a page id, or a path starting with `/`) or
|
|
150
|
+
* `{label, href}` (an external URL), drawn under the text.
|
|
128
151
|
*
|
|
129
|
-
* @type {{text: string, fontSize: string, color: string, backgroundColor: string, textAlign: string, contentMode: string, tableMode: boolean, tableData: object}}
|
|
152
|
+
* @type {{text: string, fontSize: string, color: string, backgroundColor: string, textAlign: string, contentMode: string, tableMode: boolean, tableData: object, link: {label: string, route: string, href: string}}}
|
|
130
153
|
*/
|
|
131
154
|
content: {
|
|
132
155
|
type: Object,
|
|
@@ -135,9 +158,34 @@ export default {
|
|
|
135
158
|
},
|
|
136
159
|
|
|
137
160
|
computed: {
|
|
138
|
-
/** The author text, or '' when absent / non-string. */
|
|
161
|
+
/** The author text through the app's translate function, or '' when absent / non-string. */
|
|
139
162
|
text() {
|
|
140
|
-
return typeof this.content?.text === 'string' ? this.content.text : ''
|
|
163
|
+
return typeof this.content?.text === 'string' ? this.tr(this.content.text) : ''
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The link under the text, from `content.link`: `{ label, route }`
|
|
168
|
+
* (a page id, or a router path when it starts with `/`) or
|
|
169
|
+
* `{ label, href }` (an external URL, opened in a new tab). The label
|
|
170
|
+
* goes through the app's translate function. Null without a label or
|
|
171
|
+
* a target.
|
|
172
|
+
*
|
|
173
|
+
* @return {{label: string, url: string, location: (object|null), external: boolean}|null} The link.
|
|
174
|
+
*/
|
|
175
|
+
link() {
|
|
176
|
+
const link = this.content?.link
|
|
177
|
+
if (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
if (typeof link.route === 'string' && link.route !== '') {
|
|
181
|
+
const location = link.route.startsWith('/') ? { path: link.route } : { name: link.route }
|
|
182
|
+
const url = resolveHref(location, this.$router)
|
|
183
|
+
return url ? { label: this.tr(link.label), url, location, external: false } : null
|
|
184
|
+
}
|
|
185
|
+
if (typeof link.href === 'string' && link.href !== '') {
|
|
186
|
+
return { label: this.tr(link.label), url: link.href, location: null, external: true }
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
141
189
|
},
|
|
142
190
|
|
|
143
191
|
/** Whether the text has any non-whitespace content. */
|
|
@@ -253,6 +301,7 @@ export default {
|
|
|
253
301
|
'justify-content': 'center',
|
|
254
302
|
overflow: 'auto',
|
|
255
303
|
'background-color': this.backgroundColor,
|
|
304
|
+
...(this.link ? { 'flex-direction': 'column', gap: '8px', 'align-items': 'flex-start' } : {}),
|
|
256
305
|
}
|
|
257
306
|
},
|
|
258
307
|
|
|
@@ -287,6 +336,27 @@ export default {
|
|
|
287
336
|
},
|
|
288
337
|
|
|
289
338
|
methods: {
|
|
339
|
+
/**
|
|
340
|
+
* A manifest string through the app's translate function.
|
|
341
|
+
*
|
|
342
|
+
* @param {string} text The string.
|
|
343
|
+
* @return {string} The translated string.
|
|
344
|
+
*/
|
|
345
|
+
tr(text) {
|
|
346
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Route a plain click on an in-app link, so the page does not reload.
|
|
351
|
+
*
|
|
352
|
+
* @param {MouseEvent} event The click.
|
|
353
|
+
*/
|
|
354
|
+
openLink(event) {
|
|
355
|
+
if (this.link && !this.link.external) {
|
|
356
|
+
followLinkClick(event, this.link.location, this.$router)
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
290
360
|
/**
|
|
291
361
|
* Run cell text through DOMPurify so the table-mode render path has
|
|
292
362
|
* the same XSS-stripping guarantees as the text-mode path.
|
|
@@ -518,4 +588,19 @@ export default {
|
|
|
518
588
|
color: var(--color-text-maxcontrast);
|
|
519
589
|
font-style: italic;
|
|
520
590
|
}
|
|
591
|
+
|
|
592
|
+
.cn-text-widget__link {
|
|
593
|
+
display: inline-flex;
|
|
594
|
+
align-items: center;
|
|
595
|
+
gap: 2px;
|
|
596
|
+
font-size: 14px;
|
|
597
|
+
font-weight: 600;
|
|
598
|
+
color: var(--color-primary-element);
|
|
599
|
+
text-decoration: none;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.cn-text-widget__link:hover,
|
|
603
|
+
.cn-text-widget__link:focus-visible {
|
|
604
|
+
text-decoration: underline;
|
|
605
|
+
}
|
|
521
606
|
</style>
|
package/src/components/index.js
CHANGED
|
@@ -43,6 +43,8 @@ export { CnDeleteDialog } from './CnDeleteDialog/index.js'
|
|
|
43
43
|
export { CnCopyDialog } from './CnCopyDialog/index.js'
|
|
44
44
|
export { CnFieldHelper } from './CnFieldHelper/index.js'
|
|
45
45
|
export { CnFormDialog } from './CnFormDialog/index.js'
|
|
46
|
+
export { CnDialog } from './CnDialog/index.js'
|
|
47
|
+
export { CnDialogHeader } from './CnDialogHeader/index.js'
|
|
46
48
|
export { CnFormBuilder } from './CnFormBuilder/index.js'
|
|
47
49
|
export { CnAdvancedFormDialog, CnMetadataTab, CnPropertiesTab, CnPropertyValueCell } from './CnAdvancedFormDialog/index.js'
|
|
48
50
|
export { CnMassDeleteDialog } from './CnMassDeleteDialog/index.js'
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.108.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1651,6 +1651,59 @@
|
|
|
1651
1651
|
}
|
|
1652
1652
|
}
|
|
1653
1653
|
},
|
|
1654
|
+
"linkModule": {
|
|
1655
|
+
"type": "object",
|
|
1656
|
+
"required": ["id", "label"],
|
|
1657
|
+
"additionalProperties": false,
|
|
1658
|
+
"description": "One module on a type='links' page. The app decides whether it is in the menu (`enabled`, or `enabledIf` against runtime). Every card opens; a module that is off links to the admin settings (`settings`).",
|
|
1659
|
+
"properties": {
|
|
1660
|
+
"id": { "type": "string", "minLength": 1, "description": "Unique within the page's modules." },
|
|
1661
|
+
"label": { "type": "string", "minLength": 1, "description": "i18n key / the module's title." },
|
|
1662
|
+
"description": { "type": "string", "description": "i18n key / one or two lines under the title." },
|
|
1663
|
+
"icon": { "type": "string", "description": "MDI icon name." },
|
|
1664
|
+
"enabled": { "type": "boolean", "description": "Whether the module is in the menu. Wins over enabledIf." },
|
|
1665
|
+
"enabledIf": { "$ref": "#/$defs/visibleIfCondition", "description": "Condition, evaluated like visibleIf, that says the module is in the menu." },
|
|
1666
|
+
"route": { "type": "string", "minLength": 1, "description": "Where Open goes: a route name or a path starting with '/'. Defaults to the first page." },
|
|
1667
|
+
"params": { "type": "object", "additionalProperties": { "type": ["string", "number"] } },
|
|
1668
|
+
"query": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1669
|
+
"pages": {
|
|
1670
|
+
"type": "array",
|
|
1671
|
+
"description": "The module's pages, drawn as links and named in the 'N pages' line.",
|
|
1672
|
+
"items": {
|
|
1673
|
+
"type": "object",
|
|
1674
|
+
"required": ["label"],
|
|
1675
|
+
"additionalProperties": false,
|
|
1676
|
+
"anyOf": [{ "required": ["route"] }, { "required": ["href"] }],
|
|
1677
|
+
"properties": {
|
|
1678
|
+
"label": { "type": "string", "minLength": 1 },
|
|
1679
|
+
"route": { "type": "string", "minLength": 1 },
|
|
1680
|
+
"params": { "type": "object", "additionalProperties": { "type": ["string", "number"] } },
|
|
1681
|
+
"query": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1682
|
+
"href": { "type": "string", "minLength": 1 },
|
|
1683
|
+
"highlight": { "type": "boolean", "description": "Named in the line of a module with more than four pages ('15 pages, including ...')." }
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
"settings": {
|
|
1688
|
+
"type": "object",
|
|
1689
|
+
"additionalProperties": false,
|
|
1690
|
+
"anyOf": [{ "required": ["route"] }, { "required": ["href"] }],
|
|
1691
|
+
"description": "Where an admin puts a module that is off in the menu (the admin settings), shown on its card as '<prefix> <label>'.",
|
|
1692
|
+
"properties": {
|
|
1693
|
+
"label": { "type": "string", "description": "i18n key / link text. Defaults to 'Admin settings'." },
|
|
1694
|
+
"prefix": { "type": "string", "description": "i18n key / text before the link. Defaults to 'Put it in the menu:'." },
|
|
1695
|
+
"route": { "type": "string", "minLength": 1 },
|
|
1696
|
+
"params": { "type": "object", "additionalProperties": { "type": ["string", "number"] } },
|
|
1697
|
+
"query": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1698
|
+
"href": { "type": "string", "minLength": 1 },
|
|
1699
|
+
"permission": { "type": "string" },
|
|
1700
|
+
"visibleIf": { "$ref": "#/$defs/visibleIfCondition" }
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
"permission": { "type": "string", "description": "Permission the user must hold for the module to render." },
|
|
1704
|
+
"visibleIf": { "$ref": "#/$defs/visibleIfCondition", "description": "Display condition, evaluated the way a menu entry's is." }
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1654
1707
|
"linkCard": {
|
|
1655
1708
|
"type": "object",
|
|
1656
1709
|
"required": ["id", "label"],
|
|
@@ -4751,7 +4804,11 @@
|
|
|
4751
4804
|
"description": { "type": "string" },
|
|
4752
4805
|
"emptyLabel": { "type": "string" },
|
|
4753
4806
|
"categories": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 } },
|
|
4754
|
-
"cards": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/linkCard" } }
|
|
4807
|
+
"cards": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/linkCard" } },
|
|
4808
|
+
"modules": { "type": "array", "items": { "$ref": "#/$defs/linkModule" }, "description": "Module cards drawn above the link cards (board PqModules): per module an icon, title, in-the-menu/off badge, description, its pages, an Open button and, when off, a link to the admin settings." },
|
|
4809
|
+
"modulesTitle": { "type": "string", "description": "i18n key / heading of the modules section. Defaults to 'Modules'." },
|
|
4810
|
+
"footnote": { "type": "string", "description": "i18n key / a note under the module cards." },
|
|
4811
|
+
"groupStyle": { "type": "string", "enum": ["cards", "list"], "default": "cards", "description": "cards: a grid of link cards per group. list: one card per group with a row per link (icon, label, description, chevron or external-link icon)." }
|
|
4755
4812
|
}
|
|
4756
4813
|
}
|
|
4757
4814
|
},
|
package/src/utils/boardDate.js
CHANGED
|
@@ -88,6 +88,40 @@ export function formatBoardDate(value, now = new Date(), locale = boardDateLocal
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A date with its short weekday, the way an agenda writes it: "wo 7 okt", or
|
|
93
|
+
* "wo 14 feb 2024" outside the current year. With `today` set, today's date
|
|
94
|
+
* reads "Vandaag, wo 7 okt".
|
|
95
|
+
*
|
|
96
|
+
* @param {unknown} value A Date, a timestamp or an ISO string.
|
|
97
|
+
* @param {Date} [now] The reference moment (defaults to the current time).
|
|
98
|
+
* @param {object} [options] Options.
|
|
99
|
+
* @param {boolean} [options.today] Put "Today, " before today's date.
|
|
100
|
+
* @param {string} [options.locale] BCP 47 tag (defaults to `boardDateLocale()`).
|
|
101
|
+
* @return {string} The date, or '' when the value is not a date.
|
|
102
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
|
|
103
|
+
*/
|
|
104
|
+
export function formatBoardWeekday(value, now = new Date(), options = {}) {
|
|
105
|
+
const date = parseDateValue(value)
|
|
106
|
+
if (date === null) {
|
|
107
|
+
return ''
|
|
108
|
+
}
|
|
109
|
+
const parts = { weekday: 'short', day: 'numeric', month: 'short' }
|
|
110
|
+
if (date.getFullYear() !== now.getFullYear()) {
|
|
111
|
+
parts.year = 'numeric'
|
|
112
|
+
}
|
|
113
|
+
let text
|
|
114
|
+
try {
|
|
115
|
+
text = new Intl.DateTimeFormat(options.locale || boardDateLocale(), parts).format(date)
|
|
116
|
+
} catch {
|
|
117
|
+
text = new Intl.DateTimeFormat(undefined, parts).format(date)
|
|
118
|
+
}
|
|
119
|
+
if (options.today === true && ageInDays(date, now) === 0) {
|
|
120
|
+
return t('nextcloud-vue', 'Today, {date}', { date: text })
|
|
121
|
+
}
|
|
122
|
+
return text
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
126
|
* Whole calendar days from the date until `now` (0 today, 1 yesterday), or
|
|
93
127
|
* null when the value is not a date. A future date gives a negative number.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
2
2
|
// SPDX-License-Identifier: EUPL-1.2
|
|
3
3
|
|
|
4
|
-
import { boardDateLocale, formatBoardDate, formatRelativeDate } from './boardDate.js'
|
|
4
|
+
import { boardDateLocale, formatBoardDate, formatBoardWeekday, formatRelativeDate, formatTimeOfDay } from './boardDate.js'
|
|
5
5
|
import { parseDateValue } from './dateVariant.js'
|
|
6
6
|
import { formatMoney } from './formatMetric.js'
|
|
7
7
|
import { readPath } from './readPath.js'
|
|
@@ -36,7 +36,7 @@ export function formatBoardDateTime(value, now = new Date()) {
|
|
|
36
36
|
* Format one token value.
|
|
37
37
|
*
|
|
38
38
|
* @param {unknown} value The raw value.
|
|
39
|
-
* @param {string} format `date`, `datetime`, `relative`, `relativetime`, `days` or '' (as is).
|
|
39
|
+
* @param {string} format `date`, `datetime`, `weekday`, `time`, `relative`, `relativetime`, `days` or '' (as is).
|
|
40
40
|
* @param {Date} now The reference moment.
|
|
41
41
|
* @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).
|
|
42
42
|
* @param {object|null} [object] The row, for a currency read from a field.
|
|
@@ -53,6 +53,13 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
|
|
|
53
53
|
if (format === 'datetime') {
|
|
54
54
|
return formatBoardDateTime(value, now)
|
|
55
55
|
}
|
|
56
|
+
if (format === 'weekday') {
|
|
57
|
+
return formatBoardWeekday(value, now, { today: arg === 'today' })
|
|
58
|
+
}
|
|
59
|
+
if (format === 'time') {
|
|
60
|
+
const date = parseDateValue(value)
|
|
61
|
+
return date === null ? '' : formatTimeOfDay(date)
|
|
62
|
+
}
|
|
56
63
|
if (format === 'currency') {
|
|
57
64
|
// `{amount|currency:EUR}` names the code, `{amount|currency:price.code}`
|
|
58
65
|
// reads it from the row, `{amount|currency}` is the app's reporting
|
|
@@ -79,7 +86,9 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
|
|
|
79
86
|
* `"Termijn {deadline|date}"` from an object.
|
|
80
87
|
*
|
|
81
88
|
* Every `{path}` token is a dot path into the object; `{path|date}` and
|
|
82
|
-
* `{path|datetime}` format it as the boards write dates, `{path|
|
|
89
|
+
* `{path|datetime}` format it as the boards write dates, `{path|weekday}` adds
|
|
90
|
+
* the short weekday ("wo 7 okt"; `{path|weekday:today}` reads "Vandaag, wo 7 okt"
|
|
91
|
+
* for today), `{path|time}` is the time of day ("08:45"), `{path|currency}`,
|
|
83
92
|
* `{path|currency:EUR}` and `{path|currency:field}` write an amount in a
|
|
84
93
|
* currency (a code, or the row field that holds it; without either, or when
|
|
85
94
|
* the row field is empty, the app's reporting currency), `{path|relative}` and
|