@conduction/nextcloud-vue 2.76.0 → 2.78.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/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +229 -203
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +48 -6
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +104 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +11 -1
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useObjectPresence.js +15 -4
- package/dist/esm/composables/useObjectPresence.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +32 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +32 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +500 -75
- package/dist/esm/store/plugins/interactions.js +20 -10
- package/dist/esm/store/plugins/interactions.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +17 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/badgeVariants.js +32 -0
- package/dist/esm/utils/badgeVariants.js.map +1 -0
- package/dist/esm/utils/builtInFormatters.js +15 -7
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/groupAutocomplete.js +63 -1
- package/dist/esm/utils/groupAutocomplete.js.map +1 -1
- package/dist/esm/utils/lensAvailability.js +90 -0
- package/dist/esm/utils/lensAvailability.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +13 -6
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/recordInteractions.js +31 -13
- package/dist/esm/utils/recordInteractions.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +46 -3
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +1 -1
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +4 -4
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +46 -1
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4389 -2890
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +500 -75
- package/l10n/en.json +32 -1
- package/l10n/nl.json +32 -1
- package/package.json +1 -1
- package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnBoardView/CnBoardView.vue +2 -1
- package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +106 -9
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +87 -22
- package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
- package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
- package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
- package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
- package/src/components/CnFormPage/CnFormPage.vue +4 -1
- package/src/components/CnIndexPage/CnIndexPage.md +36 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +112 -19
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
- package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
- package/src/composables/useFormDraft.js +11 -1
- package/src/composables/useListView.js +4 -0
- package/src/composables/useObjectPresence.js +15 -4
- package/src/css/badge.css +31 -0
- package/src/css/kpi-card.css +51 -0
- package/src/css/look-board-detail.css +40 -2
- package/src/css/look-board-index.css +36 -0
- package/src/css/look-board.css +8 -0
- package/src/css/table.css +82 -17
- package/src/index.js +2 -1
- package/src/schemas/app-manifest-v2.schema.json +15 -11
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +17 -0
- package/src/utils/badgeVariants.js +29 -0
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +1 -1
- package/src/utils/lensAvailability.js +86 -0
- package/src/utils/personalLenses.js +13 -6
- package/src/utils/recordInteractions.js +31 -13
- package/src/utils/resolveFilterTokens.js +45 -2
- package/src/utils/schema.js +1 -1
- package/src/utils/sentinelTokens.js +4 -4
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/widgetVisibility.js +45 -0
|
@@ -533,7 +533,8 @@
|
|
|
533
533
|
<!-- Empty state -->
|
|
534
534
|
<div v-else-if="effectiveObjects.length === 0" class="cn-index-page__empty">
|
|
535
535
|
<slot name="empty">
|
|
536
|
-
<CnEmptyContent :name="resolvedEmptyText"
|
|
536
|
+
<CnEmptyContent :name="lensReasonText || resolvedEmptyText"
|
|
537
|
+
:data-lens-unavailable="lensReasonText ? 'true' : null">
|
|
537
538
|
<template #icon>
|
|
538
539
|
<CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
|
|
539
540
|
<DatabaseSearch v-else :size="64" />
|
|
@@ -573,14 +574,15 @@
|
|
|
573
574
|
@rowClick="onRowClick"
|
|
574
575
|
@rowAuxClick="onRowAuxClick"
|
|
575
576
|
@rowContextMenu="onRowContextMenu">
|
|
576
|
-
<!--
|
|
577
|
-
<template v-if="
|
|
578
|
-
<
|
|
579
|
-
v-if="row && row['@self'] && typeof row['@self'].
|
|
577
|
+
<!-- Follow column (showFollowColumn, or the deprecated showFavouriteColumn) -->
|
|
578
|
+
<template v-if="followColumnOn" #column-__follow="{ row }">
|
|
579
|
+
<CnFollowToggle
|
|
580
|
+
v-if="row && row['@self'] && typeof row['@self'].watching === 'boolean'"
|
|
581
|
+
compact
|
|
580
582
|
:register="typeof register === 'string' ? register : ''"
|
|
581
583
|
:schema="favouriteSchemaSlug"
|
|
582
584
|
:objectId="String(row['@self'].id || row.id || '')"
|
|
583
|
-
:
|
|
585
|
+
:watching="row['@self'].watching === true" />
|
|
584
586
|
</template>
|
|
585
587
|
|
|
586
588
|
<!-- Pass through column slots -->
|
|
@@ -955,7 +957,7 @@ import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
|
955
957
|
import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
|
|
956
958
|
import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
957
959
|
import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
|
|
958
|
-
import
|
|
960
|
+
import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
|
|
959
961
|
import { useContextMenu } from '../../composables/index.js'
|
|
960
962
|
import { useLook } from '../../composables/useLook.js'
|
|
961
963
|
import { copyKindsOf } from '../../composables/useObjectCopy.js'
|
|
@@ -968,6 +970,7 @@ import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actio
|
|
|
968
970
|
import { reportBindingProblems } from '../../utils/diagnostics.js'
|
|
969
971
|
import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
|
|
970
972
|
import { buildExportUrl } from '../../utils/indexExportHelpers.js'
|
|
973
|
+
import { lensUnavailableText } from '../../utils/lensAvailability.js'
|
|
971
974
|
import { openRowTarget } from '../../utils/linkNavigation.js'
|
|
972
975
|
import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
|
|
973
976
|
import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
|
|
@@ -1037,8 +1040,8 @@ const CALENDAR_PAGE_SIZE = 200
|
|
|
1037
1040
|
*/
|
|
1038
1041
|
const RANGE_SEPARATOR = '..'
|
|
1039
1042
|
|
|
1040
|
-
/** Key of the synthetic
|
|
1041
|
-
const
|
|
1043
|
+
/** Key of the synthetic follow column (`showFollowColumn`). */
|
|
1044
|
+
const FOLLOW_COLUMN_KEY = '__follow'
|
|
1042
1045
|
|
|
1043
1046
|
/**
|
|
1044
1047
|
* Whether a schema property wants a from/to pair rather than a value list.
|
|
@@ -1240,7 +1243,7 @@ export default {
|
|
|
1240
1243
|
|
|
1241
1244
|
components: {
|
|
1242
1245
|
CnBuildiqEditButton,
|
|
1243
|
-
|
|
1246
|
+
CnFollowToggle,
|
|
1244
1247
|
NcLoadingIcon,
|
|
1245
1248
|
CnEmptyContent,
|
|
1246
1249
|
NcActions,
|
|
@@ -1576,9 +1579,10 @@ export default {
|
|
|
1576
1579
|
},
|
|
1577
1580
|
|
|
1578
1581
|
/**
|
|
1579
|
-
* Personal lenses appended to the quick filters: any of `
|
|
1580
|
-
* (`
|
|
1581
|
-
*
|
|
1582
|
+
* Personal lenses appended to the quick filters: any of `watching`
|
|
1583
|
+
* (`_watching`, labelled Following), `recent` (`_recent`) and `unread`
|
|
1584
|
+
* (`_unread`). `favourite` is a deprecated alias of `watching`: a
|
|
1585
|
+
* favourite is a follow now, and asking for both gives one tab.
|
|
1582
1586
|
* They combine with every other filter. While Recent is active column
|
|
1583
1587
|
* sorting is off, because the lens owns the order. Manifest
|
|
1584
1588
|
* `config.personalLenses`.
|
|
@@ -1591,9 +1595,20 @@ export default {
|
|
|
1591
1595
|
},
|
|
1592
1596
|
|
|
1593
1597
|
/**
|
|
1594
|
-
* Add a first column with a
|
|
1595
|
-
* each row's `@self.
|
|
1596
|
-
* Manifest `config.
|
|
1598
|
+
* Add a first column with a follow toggle per row (`CnFollowToggle`,
|
|
1599
|
+
* compact), bound to each row's `@self.watching`. Clicking it does not
|
|
1600
|
+
* open the row. Manifest `config.showFollowColumn`.
|
|
1601
|
+
*/
|
|
1602
|
+
showFollowColumn: {
|
|
1603
|
+
type: Boolean,
|
|
1604
|
+
default: false,
|
|
1605
|
+
},
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Deprecated alias of `showFollowColumn`: the star column became the
|
|
1609
|
+
* follow column (OpenRegister `merge-follow-and-favourites`).
|
|
1610
|
+
*
|
|
1611
|
+
* @deprecated Use `showFollowColumn`.
|
|
1597
1612
|
*/
|
|
1598
1613
|
showFavouriteColumn: {
|
|
1599
1614
|
type: Boolean,
|
|
@@ -2088,6 +2103,34 @@ export default {
|
|
|
2088
2103
|
default: 'No items found',
|
|
2089
2104
|
},
|
|
2090
2105
|
|
|
2106
|
+
/**
|
|
2107
|
+
* Personal-lens report of the list on screen, for a host that fetches
|
|
2108
|
+
* itself: the response's `@self.lenses` (`{ recent: { available,
|
|
2109
|
+
* reason } }`). In self-fetch mode the page reads it from the store
|
|
2110
|
+
* and this prop is ignored. When a lens reports `available: false`
|
|
2111
|
+
* the empty state explains why instead of showing `emptyText`.
|
|
2112
|
+
*
|
|
2113
|
+
* @type {object|null}
|
|
2114
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
2115
|
+
*/
|
|
2116
|
+
lenses: {
|
|
2117
|
+
type: Object,
|
|
2118
|
+
default: null,
|
|
2119
|
+
},
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* App wording for an unavailable lens, keyed `<lens>.<reason>`
|
|
2123
|
+
* (`recent.audit-trail-disabled`) or `<reason>`; the specific key
|
|
2124
|
+
* wins, and both win over the library's object-neutral text.
|
|
2125
|
+
*
|
|
2126
|
+
* @type {{[key: string]: string}|null}
|
|
2127
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
2128
|
+
*/
|
|
2129
|
+
lensReasonTexts: {
|
|
2130
|
+
type: Object,
|
|
2131
|
+
default: null,
|
|
2132
|
+
},
|
|
2133
|
+
|
|
2091
2134
|
/** Accessible label for the loading spinner (NcLoadingIcon aria-label) */
|
|
2092
2135
|
loadingText: {
|
|
2093
2136
|
type: String,
|
|
@@ -3279,6 +3322,40 @@ export default {
|
|
|
3279
3322
|
return this.emptyText ? fn(this.emptyText) : this.emptyText
|
|
3280
3323
|
},
|
|
3281
3324
|
|
|
3325
|
+
/**
|
|
3326
|
+
* The personal-lens reports of the list on screen: the store's copy
|
|
3327
|
+
* of the latest response in self-fetch mode, else the `lenses` prop.
|
|
3328
|
+
*
|
|
3329
|
+
* @return {object}
|
|
3330
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
|
|
3331
|
+
*/
|
|
3332
|
+
effectiveLenses() {
|
|
3333
|
+
if (this.isNamedSource) {
|
|
3334
|
+
return {}
|
|
3335
|
+
}
|
|
3336
|
+
if (this.isSelfFetchMode) {
|
|
3337
|
+
return (this.list.lenses && this.list.lenses.value) || {}
|
|
3338
|
+
}
|
|
3339
|
+
return this.lenses || {}
|
|
3340
|
+
},
|
|
3341
|
+
|
|
3342
|
+
/**
|
|
3343
|
+
* Why the list is empty when a personal lens could not answer (the
|
|
3344
|
+
* audit trail is off, nobody is logged in, the history could not be
|
|
3345
|
+
* read), or `''` to keep the generic empty text.
|
|
3346
|
+
*
|
|
3347
|
+
* @return {string}
|
|
3348
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
3349
|
+
*/
|
|
3350
|
+
lensReasonText() {
|
|
3351
|
+
const text = lensUnavailableText(this.effectiveLenses, this.lensReasonTexts)
|
|
3352
|
+
if (!text) {
|
|
3353
|
+
return ''
|
|
3354
|
+
}
|
|
3355
|
+
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
3356
|
+
return fn(text)
|
|
3357
|
+
},
|
|
3358
|
+
|
|
3282
3359
|
/**
|
|
3283
3360
|
* Whether the host manifest editor is in edit mode (unwraps the injected
|
|
3284
3361
|
* `cnEditingBody`, which may be a Vue ref or a plain boolean). Drives the
|
|
@@ -4172,12 +4249,22 @@ export default {
|
|
|
4172
4249
|
if (this.recentLensActive) {
|
|
4173
4250
|
cols = cols.map((col) => (col && typeof col === 'object' ? { ...col, sortable: false } : col))
|
|
4174
4251
|
}
|
|
4175
|
-
if (this.
|
|
4176
|
-
cols = [{ key:
|
|
4252
|
+
if (this.followColumnOn && this.favouriteSchemaSlug !== '') {
|
|
4253
|
+
cols = [{ key: FOLLOW_COLUMN_KEY, label: '', sortable: false, width: '48px' }, ...cols]
|
|
4177
4254
|
}
|
|
4178
4255
|
return cols
|
|
4179
4256
|
},
|
|
4180
4257
|
|
|
4258
|
+
/**
|
|
4259
|
+
* Whether the follow column renders (`showFollowColumn`, or the deprecated `showFavouriteColumn`).
|
|
4260
|
+
*
|
|
4261
|
+
* @return {boolean}
|
|
4262
|
+
* @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-index-pages-offer-one-following-lens-and-a-follow-column
|
|
4263
|
+
*/
|
|
4264
|
+
followColumnOn() {
|
|
4265
|
+
return this.showFollowColumn || this.showFavouriteColumn
|
|
4266
|
+
},
|
|
4267
|
+
|
|
4181
4268
|
/**
|
|
4182
4269
|
* Whether the active quick filter is the Recent lens.
|
|
4183
4270
|
*
|
|
@@ -4192,7 +4279,7 @@ export default {
|
|
|
4192
4279
|
},
|
|
4193
4280
|
|
|
4194
4281
|
/**
|
|
4195
|
-
* Schema slug the
|
|
4282
|
+
* Schema slug the follow column calls with: the page's own slug in self-fetch
|
|
4196
4283
|
* mode, else the schema object's `slug`. Empty when neither is known.
|
|
4197
4284
|
*
|
|
4198
4285
|
* @return {string}
|
|
@@ -5069,6 +5156,7 @@ export default {
|
|
|
5069
5156
|
* stable key and a variant.
|
|
5070
5157
|
*
|
|
5071
5158
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
5159
|
+
* @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
|
|
5072
5160
|
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
|
|
5073
5161
|
*/
|
|
5074
5162
|
resolvedHeaderButtons() {
|
|
@@ -5081,6 +5169,11 @@ export default {
|
|
|
5081
5169
|
if (label === '' && button.action === 'add') {
|
|
5082
5170
|
label = this.resolvedAddLabel
|
|
5083
5171
|
}
|
|
5172
|
+
// The board's primary button reads "+ New request": the Add
|
|
5173
|
+
// button takes the plus unless the manifest names an icon.
|
|
5174
|
+
if (this.isBoardLook && button.action === 'add' && icon === '') {
|
|
5175
|
+
icon = 'Plus'
|
|
5176
|
+
}
|
|
5084
5177
|
// The board's header: Export reads "Download" with its icon and
|
|
5085
5178
|
// the Actions menu reads "Actions", without the manifest saying so.
|
|
5086
5179
|
if (this.isBoardLook && label === '' && button.action === 'export') {
|
|
@@ -149,6 +149,28 @@
|
|
|
149
149
|
@update:modelValue="onRelationChange(field, $event)"
|
|
150
150
|
@close="commitEdit" />
|
|
151
151
|
|
|
152
|
+
<!-- Nextcloud group or user (`referenceType: nextcloud-group`
|
|
153
|
+
/ `nextcloud-user`, one or a list): pick by display name,
|
|
154
|
+
store the group id or uid. The same search the create and
|
|
155
|
+
edit dialogs use. -->
|
|
156
|
+
<component
|
|
157
|
+
:is="isUserPicker(field) ? 'NcSelectUsers' : 'NcSelect'"
|
|
158
|
+
v-else-if="isPersonPicker(field)"
|
|
159
|
+
ref="activeEditor"
|
|
160
|
+
:inputLabel="field.label"
|
|
161
|
+
:labelOutside="true"
|
|
162
|
+
:options="personOptions[field.key] || []"
|
|
163
|
+
:modelValue="personSelected(field)"
|
|
164
|
+
:multiple="isPersonMultiple(field)"
|
|
165
|
+
:keepOpen="isPersonMultiple(field)"
|
|
166
|
+
:loading="personLoading === field.key"
|
|
167
|
+
:filterable="false"
|
|
168
|
+
:clearable="!field.required"
|
|
169
|
+
label="label"
|
|
170
|
+
@search="(q) => searchPeople(field, q)"
|
|
171
|
+
@update:modelValue="onPersonChange(field, $event)"
|
|
172
|
+
@close="isPersonMultiple(field) ? undefined : commitEdit()" />
|
|
173
|
+
|
|
152
174
|
<!-- Select -->
|
|
153
175
|
<NcSelect
|
|
154
176
|
v-else-if="field.widget === 'select'"
|
|
@@ -259,7 +281,17 @@
|
|
|
259
281
|
:value="displayValues[field.key]"
|
|
260
282
|
:raw="(objectData || {})[field.key]" />
|
|
261
283
|
<template v-else>
|
|
262
|
-
|
|
284
|
+
<!-- A Nextcloud group (or list): its display name, cached
|
|
285
|
+
per id; the id while the name loads or when unknown. -->
|
|
286
|
+
<span
|
|
287
|
+
v-if="isGroupPicker(field) && groupIdsOf(field).length"
|
|
288
|
+
class="cn-object-data-widget__groups">
|
|
289
|
+
<template v-for="(gid, gi) in groupIdsOf(field)" :key="gid">
|
|
290
|
+
<span v-if="gi > 0">, </span>
|
|
291
|
+
<CnGroupNameCell :gid="gid" />
|
|
292
|
+
</template>
|
|
293
|
+
</span>
|
|
294
|
+
<img v-else-if="isImageField(field) && rawOf(field)"
|
|
263
295
|
:src="rawOf(field)"
|
|
264
296
|
:alt="field.label"
|
|
265
297
|
class="cn-object-data-widget__image">
|
|
@@ -370,19 +402,22 @@
|
|
|
370
402
|
import axios from '@nextcloud/axios'
|
|
371
403
|
import { translate as t } from '@nextcloud/l10n'
|
|
372
404
|
import { generateUrl } from '@nextcloud/router'
|
|
373
|
-
import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
|
|
405
|
+
import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcSelectUsers, NcTextField } from '@nextcloud/vue'
|
|
374
406
|
import Check from 'vue-material-design-icons/Check.vue'
|
|
375
407
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
376
408
|
import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
|
|
377
409
|
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
378
410
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
411
|
+
import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
|
|
379
412
|
import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
|
|
380
413
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
381
414
|
import { useObjectStore } from '../../store/index.js'
|
|
415
|
+
import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
382
416
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
383
417
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
384
418
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
385
419
|
import { schemaRefSlug } from '../../utils/schemaRefSlug.js'
|
|
420
|
+
import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
|
|
386
421
|
import { CnIcon } from '../CnIcon/index.js'
|
|
387
422
|
import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
|
|
388
423
|
import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
|
|
@@ -427,6 +462,8 @@ export default {
|
|
|
427
462
|
NcLoadingIcon,
|
|
428
463
|
NcTextField,
|
|
429
464
|
NcSelect,
|
|
465
|
+
NcSelectUsers,
|
|
466
|
+
CnGroupNameCell,
|
|
430
467
|
NcCheckboxRadioSwitch,
|
|
431
468
|
NcActionButton,
|
|
432
469
|
CnWidgetWrapper,
|
|
@@ -806,6 +843,12 @@ export default {
|
|
|
806
843
|
relationOptions: {},
|
|
807
844
|
/** Whether relation picker options are being fetched. */
|
|
808
845
|
relationOptionsLoading: false,
|
|
846
|
+
/** Group and user picker options per field key ({ id, label }[]). */
|
|
847
|
+
personOptions: {},
|
|
848
|
+
/** Key of the group or user field whose options are loading, or null. */
|
|
849
|
+
personLoading: null,
|
|
850
|
+
/** Display names of users seen in a user picker, by uid. */
|
|
851
|
+
userLabels: {},
|
|
809
852
|
/** Whether the user expanded the widget to see every field. */
|
|
810
853
|
expanded: false,
|
|
811
854
|
/** Fields that fit the cell while collapsed; null when all fit. */
|
|
@@ -1719,6 +1762,11 @@ export default {
|
|
|
1719
1762
|
if (this.isSingleRelationField(field.key)) {
|
|
1720
1763
|
this.loadRelationOptions(field.key)
|
|
1721
1764
|
}
|
|
1765
|
+
// Group and user fields pick from Nextcloud's own directory.
|
|
1766
|
+
if (this.isPersonPicker(field)) {
|
|
1767
|
+
this.searchPeople(field, '')
|
|
1768
|
+
this.resolvePersonLabels(field, currentValue)
|
|
1769
|
+
}
|
|
1722
1770
|
|
|
1723
1771
|
this.$nextTick(() => {
|
|
1724
1772
|
// Focus the editor
|
|
@@ -1944,6 +1992,182 @@ export default {
|
|
|
1944
1992
|
return options.find((opt) => opt.id === val) || { id: val, label: String(val) }
|
|
1945
1993
|
},
|
|
1946
1994
|
|
|
1995
|
+
/**
|
|
1996
|
+
* Whether a field picks a Nextcloud group (widget `group` or
|
|
1997
|
+
* `group-multiselect`, from `referenceType: nextcloud-group`).
|
|
1998
|
+
*
|
|
1999
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2000
|
+
* @param {object} field Field descriptor.
|
|
2001
|
+
* @return {boolean}
|
|
2002
|
+
*/
|
|
2003
|
+
isGroupPicker(field) {
|
|
2004
|
+
return !!field && (field.widget === 'group' || field.widget === 'group-multiselect')
|
|
2005
|
+
},
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Whether a field picks a Nextcloud user (widget `user` or
|
|
2009
|
+
* `user-multiselect`, from `referenceType: nextcloud-user`).
|
|
2010
|
+
*
|
|
2011
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2012
|
+
* @param {object} field Field descriptor.
|
|
2013
|
+
* @return {boolean}
|
|
2014
|
+
*/
|
|
2015
|
+
isUserPicker(field) {
|
|
2016
|
+
return !!field && (field.widget === 'user' || field.widget === 'user-multiselect')
|
|
2017
|
+
},
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Whether a field picks a group or a user.
|
|
2021
|
+
*
|
|
2022
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2023
|
+
* @param {object} field Field descriptor.
|
|
2024
|
+
* @return {boolean}
|
|
2025
|
+
*/
|
|
2026
|
+
isPersonPicker(field) {
|
|
2027
|
+
return this.isGroupPicker(field) || this.isUserPicker(field)
|
|
2028
|
+
},
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Whether a group or user field holds a list.
|
|
2032
|
+
*
|
|
2033
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2034
|
+
* @param {object} field Field descriptor.
|
|
2035
|
+
* @return {boolean}
|
|
2036
|
+
*/
|
|
2037
|
+
isPersonMultiple(field) {
|
|
2038
|
+
return !!field && (field.widget === 'group-multiselect' || field.widget === 'user-multiselect')
|
|
2039
|
+
},
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* The group ids a group field holds, for the display mode.
|
|
2043
|
+
*
|
|
2044
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2045
|
+
* @param {object} field Field descriptor.
|
|
2046
|
+
* @return {string[]}
|
|
2047
|
+
*/
|
|
2048
|
+
groupIdsOf(field) {
|
|
2049
|
+
const raw = this.rawOf(field)
|
|
2050
|
+
const list = Array.isArray(raw) ? raw : [raw]
|
|
2051
|
+
return list.filter((v) => typeof v === 'string' && v !== '')
|
|
2052
|
+
},
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* The display name of a group id or uid, falling back to the id.
|
|
2056
|
+
*
|
|
2057
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2058
|
+
* @param {object} field Field descriptor.
|
|
2059
|
+
* @param {string} id The group id or uid.
|
|
2060
|
+
* @return {string}
|
|
2061
|
+
*/
|
|
2062
|
+
personLabel(field, id) {
|
|
2063
|
+
if (this.isGroupPicker(field)) {
|
|
2064
|
+
return groupDisplayName(id) || String(id)
|
|
2065
|
+
}
|
|
2066
|
+
return this.userLabels[id] || String(id)
|
|
2067
|
+
},
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* Search Nextcloud groups or users for a picker. Fails soft (the
|
|
2071
|
+
* helpers return `[]`), so the picker stays usable with what it has.
|
|
2072
|
+
*
|
|
2073
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2074
|
+
* @param {object} field Field descriptor.
|
|
2075
|
+
* @param {string} query The search term.
|
|
2076
|
+
* @return {Promise<void>}
|
|
2077
|
+
*/
|
|
2078
|
+
async searchPeople(field, query) {
|
|
2079
|
+
this.personLoading = field.key
|
|
2080
|
+
try {
|
|
2081
|
+
const options = this.isGroupPicker(field)
|
|
2082
|
+
? await searchNextcloudGroups(query || '')
|
|
2083
|
+
: await searchNextcloudUsers(query || '')
|
|
2084
|
+
const list = Array.isArray(options) ? options : []
|
|
2085
|
+
if (this.isUserPicker(field)) {
|
|
2086
|
+
const labels = { ...this.userLabels }
|
|
2087
|
+
for (const opt of list) {
|
|
2088
|
+
labels[opt.id] = opt.label
|
|
2089
|
+
}
|
|
2090
|
+
this.userLabels = labels
|
|
2091
|
+
}
|
|
2092
|
+
this.personOptions = { ...this.personOptions, [field.key]: list }
|
|
2093
|
+
} finally {
|
|
2094
|
+
if (this.personLoading === field.key) {
|
|
2095
|
+
this.personLoading = null
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
},
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Look up the display names of the value a group or user field holds,
|
|
2102
|
+
* so the open editor shows names rather than ids.
|
|
2103
|
+
*
|
|
2104
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2105
|
+
* @param {object} field Field descriptor.
|
|
2106
|
+
* @param {unknown} value The held value (an id or a list of ids).
|
|
2107
|
+
* @return {Promise<void>}
|
|
2108
|
+
*/
|
|
2109
|
+
async resolvePersonLabels(field, value) {
|
|
2110
|
+
const ids = (Array.isArray(value) ? value : [value])
|
|
2111
|
+
.filter((v) => typeof v === 'string' && v !== '')
|
|
2112
|
+
if (this.isGroupPicker(field)) {
|
|
2113
|
+
await Promise.all(ids.map((gid) => loadGroupDisplayName(gid)))
|
|
2114
|
+
return
|
|
2115
|
+
}
|
|
2116
|
+
const unknown = ids.filter((uid) => !this.userLabels[uid])
|
|
2117
|
+
const options = await Promise.all(unknown.map((uid) => resolveNextcloudUser(uid)))
|
|
2118
|
+
if (options.length > 0) {
|
|
2119
|
+
const labels = { ...this.userLabels }
|
|
2120
|
+
for (const opt of options) {
|
|
2121
|
+
if (opt && opt.id) {
|
|
2122
|
+
labels[opt.id] = opt.label || opt.id
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
this.userLabels = labels
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* The selected option(s) of a group or user picker.
|
|
2131
|
+
*
|
|
2132
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2133
|
+
* @param {object} field Field descriptor.
|
|
2134
|
+
* @return {object|object[]|null}
|
|
2135
|
+
*/
|
|
2136
|
+
personSelected(field) {
|
|
2137
|
+
const toOption = (id) => {
|
|
2138
|
+
const label = this.personLabel(field, id)
|
|
2139
|
+
return { id, label, displayName: label }
|
|
2140
|
+
}
|
|
2141
|
+
const v = this.editData[field.key]
|
|
2142
|
+
if (this.isPersonMultiple(field)) {
|
|
2143
|
+
return (Array.isArray(v) ? v : [])
|
|
2144
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
2145
|
+
.map(toOption)
|
|
2146
|
+
}
|
|
2147
|
+
return (typeof v === 'string' && v !== '') ? toOption(v) : null
|
|
2148
|
+
},
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Store a group or user choice as the id (or list of ids).
|
|
2152
|
+
*
|
|
2153
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2154
|
+
* @param {object} field Field descriptor.
|
|
2155
|
+
* @param {object|object[]|null} selected The chosen option(s).
|
|
2156
|
+
*/
|
|
2157
|
+
onPersonChange(field, selected) {
|
|
2158
|
+
if (this.isPersonMultiple(field)) {
|
|
2159
|
+
const ids = (Array.isArray(selected) ? selected : [])
|
|
2160
|
+
.map((opt) => (opt && typeof opt === 'object' ? opt.id : opt))
|
|
2161
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
2162
|
+
this.updateField(field.key, ids)
|
|
2163
|
+
return
|
|
2164
|
+
}
|
|
2165
|
+
if (selected && this.isUserPicker(field) && selected.id) {
|
|
2166
|
+
this.userLabels = { ...this.userLabels, [selected.id]: selected.label || selected.id }
|
|
2167
|
+
}
|
|
2168
|
+
this.updateField(field.key, selected && selected.id ? selected.id : null)
|
|
2169
|
+
},
|
|
2170
|
+
|
|
1947
2171
|
onSelectChange(field, option) {
|
|
1948
2172
|
this.updateField(field.key, option ? option.id : null)
|
|
1949
2173
|
},
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
v-if="browserRoot !== null"
|
|
9
9
|
:rootPath="browserRoot"
|
|
10
10
|
:rootLabel="browserRootLabel"
|
|
11
|
+
:uploadButton="uploadButton"
|
|
12
|
+
:dropOverlay="dropOverlay"
|
|
13
|
+
:dropHint="dropHint"
|
|
11
14
|
:rowActions="rowActions"
|
|
12
15
|
:newActions="newActions"
|
|
13
16
|
:linkedItems="linkedItems"
|
|
@@ -307,6 +310,12 @@ export default {
|
|
|
307
310
|
deleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
|
|
308
311
|
/** What the files browser's root crumb reads; null shows the folder's own name, as the Files app does. */
|
|
309
312
|
browserRootLabel: { type: String, default: null },
|
|
313
|
+
/** Forwarded to CnFilesBrowser: a primary "Add files" button beside the New menu. */
|
|
314
|
+
uploadButton: { type: Boolean, default: false },
|
|
315
|
+
/** Forwarded to CnFilesBrowser: the drop state drawn over the list ("Drop to add") and announced. */
|
|
316
|
+
dropOverlay: { type: Boolean, default: false },
|
|
317
|
+
/** Forwarded to CnFilesBrowser: "Or drag files onto this list." under the list. */
|
|
318
|
+
dropHint: { type: Boolean, default: false },
|
|
310
319
|
/**
|
|
311
320
|
* The host's own actions on each file row of the browser, forwarded
|
|
312
321
|
* to CnFilesBrowser's `rowActions`: declared like any manifest action
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
<div class="cn-kpi-card__body cn-stat-widget__body">
|
|
36
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
v-if="content.label || rangePresets.length"
|
|
38
|
+
class="cn-kpi-card__title cn-stat-widget__label"
|
|
39
|
+
:class="{ 'cn-kpi-card__title--badge-end': hasEndBadge }">
|
|
37
40
|
<!-- Board look: the icon is an 18px glyph before the label, and
|
|
38
41
|
only when the tile links to a filtered list. -->
|
|
39
42
|
<span
|
|
40
|
-
v-if="isBoardLook && isLinked && resolvedIcon"
|
|
43
|
+
v-if="isBoardLook && isLinked && resolvedIcon && !hasEndBadge"
|
|
41
44
|
class="cn-kpi-card__glyph cn-stat-widget__glyph"
|
|
42
45
|
data-testid="cn-stat-widget-glyph"
|
|
43
46
|
aria-hidden="true">
|
|
@@ -66,6 +69,17 @@
|
|
|
66
69
|
{{ effectiveTranslate(preset.label || preset.id) }}
|
|
67
70
|
</option>
|
|
68
71
|
</select>
|
|
72
|
+
<!-- Board look, `content.iconPlacement: "end"`: the icon sits in a
|
|
73
|
+
32px circle at the right of the label row, tinted by the
|
|
74
|
+
tile's state (DqDashboard). -->
|
|
75
|
+
<span
|
|
76
|
+
v-if="hasEndBadge"
|
|
77
|
+
class="cn-kpi-card__badge cn-stat-widget__badge-icon"
|
|
78
|
+
:class="'cn-kpi-card__badge--' + stateTone"
|
|
79
|
+
data-testid="cn-stat-widget-icon-badge"
|
|
80
|
+
aria-hidden="true">
|
|
81
|
+
<CnWidgetIcon :name="resolvedIcon" :size="18" />
|
|
82
|
+
</span>
|
|
69
83
|
</div>
|
|
70
84
|
|
|
71
85
|
<div class="cn-kpi-card__value-row cn-stat-widget__value-row">
|
|
@@ -1330,9 +1344,45 @@ export default {
|
|
|
1330
1344
|
if (layout === 'vertical' || layout === 'stacked') {
|
|
1331
1345
|
return layout
|
|
1332
1346
|
}
|
|
1347
|
+
// Board look: every KPI tile on the screens is stacked, so a tile
|
|
1348
|
+
// that names no layout takes it. An explicit `horizontal` stays.
|
|
1349
|
+
if (this.isBoardLook && layout === undefined) {
|
|
1350
|
+
return 'stacked'
|
|
1351
|
+
}
|
|
1333
1352
|
return 'horizontal'
|
|
1334
1353
|
},
|
|
1335
1354
|
|
|
1355
|
+
/**
|
|
1356
|
+
* Whether the board look draws the icon in a tinted circle at the
|
|
1357
|
+
* right of the label row (`content.iconPlacement: "end"`), as the
|
|
1358
|
+
* DqDashboard tiles. Off without the board look and without an icon.
|
|
1359
|
+
*
|
|
1360
|
+
* @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
|
|
1361
|
+
* @return {boolean}
|
|
1362
|
+
*/
|
|
1363
|
+
hasEndBadge() {
|
|
1364
|
+
return this.isBoardLook
|
|
1365
|
+
&& (this.content || {}).iconPlacement === 'end'
|
|
1366
|
+
&& Boolean(this.resolvedIcon)
|
|
1367
|
+
},
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* The tile's state as a tone name for the end badge: the variant the
|
|
1371
|
+
* value colour comes from (`success`, `warning`, `error`), else
|
|
1372
|
+
* `primary`. A custom `valueColor` has no tone and reads as primary.
|
|
1373
|
+
*
|
|
1374
|
+
* @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
|
|
1375
|
+
* @return {'primary'|'success'|'warning'|'error'}
|
|
1376
|
+
*/
|
|
1377
|
+
stateTone() {
|
|
1378
|
+
const color = this.variantColor
|
|
1379
|
+
if (!color) {
|
|
1380
|
+
return 'primary'
|
|
1381
|
+
}
|
|
1382
|
+
const tone = ['success', 'warning', 'error'].find((name) => VARIANT_COLORS[name] === color)
|
|
1383
|
+
return tone || 'primary'
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1336
1386
|
/**
|
|
1337
1387
|
* Whether the tile takes the stacked board look (`content.layout:
|
|
1338
1388
|
* "stacked"`): a plain muted label, the number at 34px in the text
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
|
|
12
12
|
<script>
|
|
13
|
+
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* CnStatusBadge — Color-coded pill badge for status, priority, or category display.
|
|
15
17
|
*
|
|
@@ -39,12 +41,14 @@ export default {
|
|
|
39
41
|
},
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Color variant: 'default', 'primary', 'success', 'warning', 'error',
|
|
44
|
+
* Color variant: 'default', 'primary', 'success', 'warning', 'error',
|
|
45
|
+
* 'info', or one of the two board tones without a Nextcloud role,
|
|
46
|
+
* 'purple' (work in progress) and 'teal' (handed on).
|
|
43
47
|
*/
|
|
44
48
|
variant: {
|
|
45
49
|
type: String,
|
|
46
50
|
default: 'default',
|
|
47
|
-
validator: (v) =>
|
|
51
|
+
validator: (v) => isBadgeVariant(v),
|
|
48
52
|
},
|
|
49
53
|
|
|
50
54
|
/** Size: 'small' or 'medium' */
|
|
@@ -370,6 +370,19 @@ export default {
|
|
|
370
370
|
default: undefined,
|
|
371
371
|
},
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* App wording for an unavailable personal lens (a `source.filter._recent`
|
|
375
|
+
* tile on an instance that does not log reads), keyed `<lens>.<reason>`
|
|
376
|
+
* or `<reason>`. Forwarded to CnDataTable, which shows the library's
|
|
377
|
+
* object-neutral text without it.
|
|
378
|
+
*
|
|
379
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
380
|
+
*/
|
|
381
|
+
lensReasonTexts: {
|
|
382
|
+
type: Object,
|
|
383
|
+
default: undefined,
|
|
384
|
+
},
|
|
385
|
+
|
|
373
386
|
/**
|
|
374
387
|
* Leading per-row icon: a static MDI icon name string, or a function
|
|
375
388
|
* `(row) => iconName`. Forwarded to CnDataTable.
|