@conduction/nextcloud-vue 2.82.0 → 2.84.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/CnActionsBar/CnActionsBar.vue2.js +2 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +14 -0
- 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/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
- 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/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
- package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
- package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
- package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +4 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
- 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/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +16 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +41 -4
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +900 -537
- package/dist/esm/utils/buildManifest.js +37 -3
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/fetchAggregate.js +25 -2
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/switchValues.js +62 -0
- package/dist/esm/utils/switchValues.js.map +1 -0
- 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 +36711 -34909
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +900 -537
- package/l10n/en.json +16 -1
- package/l10n/nl.json +41 -4
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
- package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +11 -0
- package/src/css/index.css +1 -0
- package/src/css/look-board-reports.css +175 -0
- package/src/css/look-board.css +44 -1
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/schemas/app-manifest-v2.schema.json +24 -3
- package/src/utils/buildManifest.js +36 -2
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/fetchAggregate.js +24 -1
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/switchValues.js +59 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1253,6 +1253,7 @@ import {
|
|
|
1253
1253
|
stageEntry,
|
|
1254
1254
|
stageOf,
|
|
1255
1255
|
} from '../../utils/detailActionModel.js'
|
|
1256
|
+
import { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData } from '../../utils/detailWidgetVisibility.js'
|
|
1256
1257
|
import { reportBindingProblems } from '../../utils/diagnostics.js'
|
|
1257
1258
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
1258
1259
|
import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
|
|
@@ -1260,6 +1261,7 @@ import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
|
|
|
1260
1261
|
import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
|
|
1261
1262
|
import { setReadState } from '../../utils/recordInteractions.js'
|
|
1262
1263
|
import { slotRenders } from '../../utils/slotContent.js'
|
|
1264
|
+
import { evaluateVisibleWhen, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
|
|
1263
1265
|
import {
|
|
1264
1266
|
isCardWidgetDef,
|
|
1265
1267
|
isContentOnlyWidgetDef,
|
|
@@ -1512,8 +1514,8 @@ export default {
|
|
|
1512
1514
|
|
|
1513
1515
|
/**
|
|
1514
1516
|
* Offer the board look's tab list name to the tabs widget below this page
|
|
1515
|
-
* (`config.tabsLabel`, default "<type> parts")
|
|
1516
|
-
* widget reads
|
|
1517
|
+
* (`config.tabsLabel`, default "<type> parts"), and the records
|
|
1518
|
+
* `visibilityContext` names. Getter objects, so the widget reads them live.
|
|
1517
1519
|
*
|
|
1518
1520
|
* @return {object} The provided values.
|
|
1519
1521
|
* @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip
|
|
@@ -1521,6 +1523,7 @@ export default {
|
|
|
1521
1523
|
provide() {
|
|
1522
1524
|
const isBoard = computed(() => this.isBoard)
|
|
1523
1525
|
const tabsLabel = computed(() => this.resolvedTabsLabel)
|
|
1526
|
+
const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))
|
|
1524
1527
|
return {
|
|
1525
1528
|
// The page's own look reaches the tabs, the cards and the activity
|
|
1526
1529
|
// below it, also when it came from the `look` prop and not the app.
|
|
@@ -1530,6 +1533,15 @@ export default {
|
|
|
1530
1533
|
return tabsLabel.value
|
|
1531
1534
|
},
|
|
1532
1535
|
},
|
|
1536
|
+
|
|
1537
|
+
// The records `visibilityContext` names, keyed `@context.<name>`, so a tab's
|
|
1538
|
+
// `visibleWhen` reads the case type as a grid widget's does. Null
|
|
1539
|
+
// while they are on their way.
|
|
1540
|
+
cnDetailVisibilityRows: {
|
|
1541
|
+
get value() {
|
|
1542
|
+
return visibilityRows.value
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1533
1545
|
}
|
|
1534
1546
|
},
|
|
1535
1547
|
|
|
@@ -2572,6 +2584,25 @@ export default {
|
|
|
2572
2584
|
default: '',
|
|
2573
2585
|
},
|
|
2574
2586
|
|
|
2587
|
+
/**
|
|
2588
|
+
* Records the page reads beside the loaded one, so a `visibleWhen`
|
|
2589
|
+
* can ask about them (manifest `config.visibilityContext`). Each entry
|
|
2590
|
+
* is `{ <name>: { field, register, schema } }`: `field` is the
|
|
2591
|
+
* property of the loaded record holding the other record's id, and a
|
|
2592
|
+
* condition reads that record as `@context.<name>.<path>`. A case page that
|
|
2593
|
+
* declares `{ caseType: { field: "caseType", register: "dossiq",
|
|
2594
|
+
* schema: "caseType" } }` can show a widget for some case types only
|
|
2595
|
+
* with `{ field: "@context.caseType.category", op: "in", value: [...] }`.
|
|
2596
|
+
* A record that cannot be read is absent, so its condition fails safe.
|
|
2597
|
+
* Empty (the default) reads nothing extra.
|
|
2598
|
+
*
|
|
2599
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
2600
|
+
*/
|
|
2601
|
+
visibilityContext: {
|
|
2602
|
+
type: Object,
|
|
2603
|
+
default: () => ({}),
|
|
2604
|
+
},
|
|
2605
|
+
|
|
2575
2606
|
/**
|
|
2576
2607
|
* Whether the current user administers this instance. Shows the
|
|
2577
2608
|
* `adminOnly` header actions. `null` (the default) reads it from
|
|
@@ -2799,6 +2830,19 @@ export default {
|
|
|
2799
2830
|
|
|
2800
2831
|
data() {
|
|
2801
2832
|
return {
|
|
2833
|
+
/**
|
|
2834
|
+
* The records `visibilityContext` names, keyed `@context.<name>`: the row,
|
|
2835
|
+
* `null` when it could not be read, absent while it is on its way.
|
|
2836
|
+
*/
|
|
2837
|
+
visibilityRows: {},
|
|
2838
|
+
/** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */
|
|
2839
|
+
visibilityRowsFor: '',
|
|
2840
|
+
/** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */
|
|
2841
|
+
visibilityRowsSeq: 0,
|
|
2842
|
+
/** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */
|
|
2843
|
+
widgetConditionAnswers: {},
|
|
2844
|
+
/** Sequence of the last remote condition round. */
|
|
2845
|
+
widgetConditionSeq: 0,
|
|
2802
2846
|
/** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
|
|
2803
2847
|
pillCounts: {},
|
|
2804
2848
|
/** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
|
|
@@ -3058,11 +3102,17 @@ export default {
|
|
|
3058
3102
|
* because a keyboard user who cannot see the page needs to know what
|
|
3059
3103
|
* they are being offered before they take it.
|
|
3060
3104
|
*
|
|
3105
|
+
* One library string with a placeholder, not "Skip to" from the host's
|
|
3106
|
+
* catalogue glued to the label: no app carried that fragment, so every
|
|
3107
|
+
* Dutch reader got "Skip to In behandeling nemen" (Zuiddrecht live
|
|
3108
|
+
* round 3), and a fragment cannot be put in Dutch word order anyway.
|
|
3109
|
+
*
|
|
3061
3110
|
* @return {string} The link text.
|
|
3062
3111
|
* @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
|
|
3112
|
+
* @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
|
|
3063
3113
|
*/
|
|
3064
3114
|
skipLinkLabel() {
|
|
3065
|
-
return
|
|
3115
|
+
return t('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })
|
|
3066
3116
|
},
|
|
3067
3117
|
|
|
3068
3118
|
// ── The action model (detail-action-model-and-case-surfaces) ────
|
|
@@ -3904,6 +3954,68 @@ export default {
|
|
|
3904
3954
|
return store.objects?.[this.objectType]?.[this.objectId] ?? null
|
|
3905
3955
|
},
|
|
3906
3956
|
|
|
3957
|
+
/**
|
|
3958
|
+
* The records `visibilityContext` names for the loaded record.
|
|
3959
|
+
*
|
|
3960
|
+
* @return {Array<{key: string, register: string, schema: string, id: string}>}
|
|
3961
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
3962
|
+
*/
|
|
3963
|
+
visibilityRefs() {
|
|
3964
|
+
return visibilityContextRefs(this.visibilityContext, this.resolvedObject)
|
|
3965
|
+
},
|
|
3966
|
+
|
|
3967
|
+
/**
|
|
3968
|
+
* Whether a record `visibilityContext` names is still on its way.
|
|
3969
|
+
*
|
|
3970
|
+
* @return {boolean}
|
|
3971
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
3972
|
+
*/
|
|
3973
|
+
visibilityRowsPending() {
|
|
3974
|
+
return this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey
|
|
3975
|
+
},
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* The named references as one key, so another record (or another case
|
|
3979
|
+
* type on the same record) reads them again.
|
|
3980
|
+
*
|
|
3981
|
+
* @return {string}
|
|
3982
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
3983
|
+
*/
|
|
3984
|
+
visibilityRefsKey() {
|
|
3985
|
+
return JSON.stringify(this.visibilityRefs)
|
|
3986
|
+
},
|
|
3987
|
+
|
|
3988
|
+
/**
|
|
3989
|
+
* What a grid widget's `visibleWhen` reads: the record, with the
|
|
3990
|
+
* `visibilityContext` records under `@context.<name>`. Null until the record loads.
|
|
3991
|
+
*
|
|
3992
|
+
* @return {object|null}
|
|
3993
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
3994
|
+
*/
|
|
3995
|
+
widgetVisibilityData() {
|
|
3996
|
+
return widgetConditionData(this.resolvedObject, this.visibilityRows)
|
|
3997
|
+
},
|
|
3998
|
+
|
|
3999
|
+
/**
|
|
4000
|
+
* The grid widgets whose `visibleWhen` needs a request (endpoint or
|
|
4001
|
+
* source), with what they ask, as one key: a change re-asks them.
|
|
4002
|
+
*
|
|
4003
|
+
* @return {string}
|
|
4004
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
4005
|
+
*/
|
|
4006
|
+
remoteWidgetConditionsKey() {
|
|
4007
|
+
if (!this.widgetVisibilityData || this.visibilityRowsPending) {
|
|
4008
|
+
return ''
|
|
4009
|
+
}
|
|
4010
|
+
const defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || [])]
|
|
4011
|
+
const remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'
|
|
4012
|
+
&& !isLocallyDecidableVisibleWhen(def.visibleWhen))
|
|
4013
|
+
if (remote.length === 0) {
|
|
4014
|
+
return ''
|
|
4015
|
+
}
|
|
4016
|
+
return JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])
|
|
4017
|
+
},
|
|
4018
|
+
|
|
3907
4019
|
/**
|
|
3908
4020
|
* The `@self` block of the resolved object (interaction markers live here).
|
|
3909
4021
|
*
|
|
@@ -4436,6 +4548,22 @@ export default {
|
|
|
4436
4548
|
},
|
|
4437
4549
|
|
|
4438
4550
|
watch: {
|
|
4551
|
+
// The records a condition names are read again for another record.
|
|
4552
|
+
visibilityRefsKey: {
|
|
4553
|
+
immediate: true,
|
|
4554
|
+
handler() {
|
|
4555
|
+
this.refreshVisibilityRows()
|
|
4556
|
+
},
|
|
4557
|
+
},
|
|
4558
|
+
|
|
4559
|
+
// Endpoint and source conditions on grid widgets are asked once per record.
|
|
4560
|
+
remoteWidgetConditionsKey: {
|
|
4561
|
+
immediate: true,
|
|
4562
|
+
handler() {
|
|
4563
|
+
this.refreshWidgetConditions()
|
|
4564
|
+
},
|
|
4565
|
+
},
|
|
4566
|
+
|
|
4439
4567
|
// The header's count pills count again for another record or other lists.
|
|
4440
4568
|
pillCountKey: {
|
|
4441
4569
|
immediate: true,
|
|
@@ -5674,6 +5802,62 @@ export default {
|
|
|
5674
5802
|
|| undefined
|
|
5675
5803
|
},
|
|
5676
5804
|
|
|
5805
|
+
/**
|
|
5806
|
+
* Read the records `visibilityContext` names for the loaded record. A
|
|
5807
|
+
* late answer for a record the page has left is dropped.
|
|
5808
|
+
*
|
|
5809
|
+
* @return {Promise<void>}
|
|
5810
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
5811
|
+
*/
|
|
5812
|
+
async refreshVisibilityRows() {
|
|
5813
|
+
const key = this.visibilityRefsKey
|
|
5814
|
+
const refs = this.visibilityRefs
|
|
5815
|
+
const seq = ++this.visibilityRowsSeq
|
|
5816
|
+
if (refs.length === 0) {
|
|
5817
|
+
// Write only on a change, so a page without `visibilityContext`
|
|
5818
|
+
// never touches the state its layout could read.
|
|
5819
|
+
if (Object.keys(this.visibilityRows).length > 0) {
|
|
5820
|
+
this.visibilityRows = {}
|
|
5821
|
+
}
|
|
5822
|
+
if (this.visibilityRowsFor !== key) {
|
|
5823
|
+
this.visibilityRowsFor = key
|
|
5824
|
+
}
|
|
5825
|
+
return
|
|
5826
|
+
}
|
|
5827
|
+
if (!this._visibilityRowCache) {
|
|
5828
|
+
this._visibilityRowCache = new Map()
|
|
5829
|
+
}
|
|
5830
|
+
const rows = await fetchVisibilityRows(refs, this._visibilityRowCache)
|
|
5831
|
+
if (seq !== this.visibilityRowsSeq) {
|
|
5832
|
+
return
|
|
5833
|
+
}
|
|
5834
|
+
this.visibilityRows = rows
|
|
5835
|
+
this.visibilityRowsFor = key
|
|
5836
|
+
},
|
|
5837
|
+
|
|
5838
|
+
/**
|
|
5839
|
+
* Ask the endpoint and source `visibleWhen` conditions of the grid
|
|
5840
|
+
* widgets, once per record. Each is fail-safe: an error hides the widget.
|
|
5841
|
+
*
|
|
5842
|
+
* @return {Promise<void>}
|
|
5843
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
5844
|
+
*/
|
|
5845
|
+
async refreshWidgetConditions() {
|
|
5846
|
+
const seq = ++this.widgetConditionSeq
|
|
5847
|
+
if (this.remoteWidgetConditionsKey === '') {
|
|
5848
|
+
return
|
|
5849
|
+
}
|
|
5850
|
+
const [, entries] = JSON.parse(this.remoteWidgetConditionsKey)
|
|
5851
|
+
const ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined }
|
|
5852
|
+
const answers = {}
|
|
5853
|
+
await Promise.all(entries.map(async ([id, cond]) => {
|
|
5854
|
+
answers[id] = await evaluateVisibleWhen(cond, ctx)
|
|
5855
|
+
}))
|
|
5856
|
+
if (seq === this.widgetConditionSeq) {
|
|
5857
|
+
this.widgetConditionAnswers = answers
|
|
5858
|
+
}
|
|
5859
|
+
},
|
|
5860
|
+
|
|
5677
5861
|
/**
|
|
5678
5862
|
* Drop the items that draw nothing, then close the rows they freed —
|
|
5679
5863
|
* `float: true` leaves the item below pinned under a gap nobody owns.
|
|
@@ -5696,7 +5880,10 @@ export default {
|
|
|
5696
5880
|
* Whether a layout item has anything to draw, so a grid cell is worth
|
|
5697
5881
|
* spending on it.
|
|
5698
5882
|
*
|
|
5699
|
-
*
|
|
5883
|
+
* A widget whose definition carries `visibleWhen` draws only while the
|
|
5884
|
+
* condition holds for the record (see `detailWidgetVisible`).
|
|
5885
|
+
*
|
|
5886
|
+
* Beyond that, TWO widget types are judged, and no others.
|
|
5700
5887
|
*
|
|
5701
5888
|
* `integration`, in the case where nothing can ever appear: a leaf from
|
|
5702
5889
|
* another app that is not registered, with no `requiredApp` to fall back
|
|
@@ -5743,6 +5930,18 @@ export default {
|
|
|
5743
5930
|
if (!widget) {
|
|
5744
5931
|
return true
|
|
5745
5932
|
}
|
|
5933
|
+
// The widget's own `visibleWhen`, read against the record and the
|
|
5934
|
+
// records `visibilityContext` names (a case's case type). Without
|
|
5935
|
+
// one the widget shows, as it always did, and the layout takes no
|
|
5936
|
+
// dependency on the record: a page without conditions re-lays out
|
|
5937
|
+
// exactly as often as before.
|
|
5938
|
+
if (widget.visibleWhen !== undefined && widget.visibleWhen !== null
|
|
5939
|
+
&& !detailWidgetVisible(widget, this.widgetVisibilityData, {
|
|
5940
|
+
pending: this.visibilityRowsPending,
|
|
5941
|
+
answers: this.widgetConditionAnswers,
|
|
5942
|
+
})) {
|
|
5943
|
+
return false
|
|
5944
|
+
}
|
|
5746
5945
|
// The board look shows the activity as the History tab. When a tabs
|
|
5747
5946
|
// widget already holds it, it is not also a body section.
|
|
5748
5947
|
if (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {
|
|
@@ -111,6 +111,14 @@
|
|
|
111
111
|
role="note"
|
|
112
112
|
:data-testid="`cn-field-list-band-${row.key}`">
|
|
113
113
|
<span class="cn-field-list-widget__band-text">{{ row.detail }}</span>
|
|
114
|
+
<NcButton
|
|
115
|
+
v-for="copy in row.copies"
|
|
116
|
+
:key="`copy-${copy.path}`"
|
|
117
|
+
variant="tertiary"
|
|
118
|
+
:data-testid="`cn-field-list-copy-${copy.path}-${row.key}`"
|
|
119
|
+
@click="copyValue(copy)">
|
|
120
|
+
{{ copy.label }}
|
|
121
|
+
</NcButton>
|
|
114
122
|
<NcButton
|
|
115
123
|
v-for="action in row.bandActions"
|
|
116
124
|
:key="action.id"
|
|
@@ -162,6 +170,16 @@
|
|
|
162
170
|
<p v-if="row.detail" class="cn-field-list-widget__detail">
|
|
163
171
|
{{ row.detail }}
|
|
164
172
|
</p>
|
|
173
|
+
<div v-if="row.detail && row.copies.length > 0" class="cn-field-list-widget__copies">
|
|
174
|
+
<NcButton
|
|
175
|
+
v-for="copy in row.copies"
|
|
176
|
+
:key="`copy-${copy.path}`"
|
|
177
|
+
variant="tertiary"
|
|
178
|
+
:data-testid="`cn-field-list-copy-${copy.path}-${row.key}`"
|
|
179
|
+
@click="copyValue(copy)">
|
|
180
|
+
{{ copy.label }}
|
|
181
|
+
</NcButton>
|
|
182
|
+
</div>
|
|
165
183
|
</li>
|
|
166
184
|
</ul>
|
|
167
185
|
|
|
@@ -211,6 +229,7 @@
|
|
|
211
229
|
</NcButton>
|
|
212
230
|
</form>
|
|
213
231
|
|
|
232
|
+
<span class="cn-field-list-widget__sr" aria-live="polite" data-testid="cn-field-list-copied">{{ copied }}</span>
|
|
214
233
|
<NcNoteCard v-if="notice" :type="noticeType" data-testid="cn-field-list-notice">
|
|
215
234
|
{{ notice }}
|
|
216
235
|
</NcNoteCard>
|
|
@@ -260,7 +279,7 @@ export default {
|
|
|
260
279
|
*
|
|
261
280
|
* - `field` (required) — the object field holding the array.
|
|
262
281
|
* - `key` — the item field that identifies an entry (default: the item itself for strings).
|
|
263
|
-
* - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when
|
|
282
|
+
* - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when?, copy?: [{ path, label }] } }`. `detail.copy` puts a copy button beside the detail line for each named value of the row (an empty value gets none), such as a DNS record's name and value.
|
|
264
283
|
* - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.
|
|
265
284
|
* - `remove` — `{ label? }`; omitted = no remove.
|
|
266
285
|
* - `rowActions` — `[{ id, label, when?, placement?: 'band', request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.
|
|
@@ -316,6 +335,8 @@ export default {
|
|
|
316
335
|
busy: '',
|
|
317
336
|
notice: '',
|
|
318
337
|
noticeType: 'error',
|
|
338
|
+
// What the last copy button put on the clipboard, said to a screen reader.
|
|
339
|
+
copied: '',
|
|
319
340
|
}
|
|
320
341
|
},
|
|
321
342
|
|
|
@@ -358,6 +379,7 @@ export default {
|
|
|
358
379
|
pill: this.pillOf(entry, spec.pill),
|
|
359
380
|
aside: spec.aside ? this.asideOf(entry, spec.aside) : '',
|
|
360
381
|
detail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',
|
|
382
|
+
copies: spec.detail && this.matches(entry, spec.detail.when) ? this.copiesOf(entry, spec.detail.copy) : [],
|
|
361
383
|
actions: this.actionsFor(entry),
|
|
362
384
|
cells: this.isTable ? this.cellsOf(entry) : [],
|
|
363
385
|
}
|
|
@@ -818,6 +840,40 @@ export default {
|
|
|
818
840
|
* @return {Promise<void>}
|
|
819
841
|
* @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-row-can-run-a-configured-request
|
|
820
842
|
*/
|
|
843
|
+
/**
|
|
844
|
+
* The copy buttons of a row's detail line: one per named value that is not empty.
|
|
845
|
+
*
|
|
846
|
+
* @param {object} entry The entry.
|
|
847
|
+
* @param {Array<{path: string, label?: string}>|undefined} declared The `detail.copy` list.
|
|
848
|
+
* @return {Array<{path: string, label: string, value: string}>}
|
|
849
|
+
*/
|
|
850
|
+
copiesOf(entry, declared) {
|
|
851
|
+
return (Array.isArray(declared) ? declared : [])
|
|
852
|
+
.filter((copy) => copy && typeof copy.path === 'string' && copy.path !== '')
|
|
853
|
+
.map((copy) => ({
|
|
854
|
+
path: copy.path,
|
|
855
|
+
label: this.tr(copy.label || 'Copy'),
|
|
856
|
+
value: String(readPath(entry, copy.path) ?? ''),
|
|
857
|
+
}))
|
|
858
|
+
.filter((copy) => copy.value !== '')
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Put one value on the clipboard and say so; a browser that refuses says that instead.
|
|
863
|
+
*
|
|
864
|
+
* @param {{label: string, value: string}} copy The copy button.
|
|
865
|
+
* @return {Promise<void>}
|
|
866
|
+
*/
|
|
867
|
+
async copyValue(copy) {
|
|
868
|
+
this.copied = ''
|
|
869
|
+
try {
|
|
870
|
+
await navigator.clipboard.writeText(copy.value)
|
|
871
|
+
this.copied = `${this.tr('Copied')}: ${copy.label}`
|
|
872
|
+
} catch {
|
|
873
|
+
this.notify(this.tr('Copying did not work. Select the text and copy it yourself.'))
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
|
|
821
877
|
async runAction(action, row) {
|
|
822
878
|
this.busy = row.key
|
|
823
879
|
this.notice = ''
|
|
@@ -912,6 +968,12 @@ export default {
|
|
|
912
968
|
|
|
913
969
|
.cn-field-list-widget__meta,
|
|
914
970
|
.cn-field-list-widget__aside,
|
|
971
|
+
.cn-field-list-widget__copies {
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-wrap: wrap;
|
|
974
|
+
gap: 4px;
|
|
975
|
+
}
|
|
976
|
+
|
|
915
977
|
.cn-field-list-widget__detail {
|
|
916
978
|
font-size: 13px;
|
|
917
979
|
}
|
|
@@ -280,7 +280,7 @@ export default {
|
|
|
280
280
|
* @return {string} The formatted string.
|
|
281
281
|
*/
|
|
282
282
|
formatNumber(value) {
|
|
283
|
-
return formatMetricValue(value, this.resolvedFormat, this.configCtx)
|
|
283
|
+
return formatMetricValue(value, this.resolvedFormat, this.configCtx, null, this.effectiveTranslate)
|
|
284
284
|
},
|
|
285
285
|
|
|
286
286
|
/**
|
|
@@ -47,8 +47,19 @@
|
|
|
47
47
|
actions when it names none): a header action, a built-in by
|
|
48
48
|
name, or a link (an entry with `href`, the documentation). -->
|
|
49
49
|
<template v-for="entry in headerMenuEntries(button)" :key="entry.key">
|
|
50
|
+
<!-- A group heading and a separator (`{ heading }` and
|
|
51
|
+
`separator` in items), as the board's "Ga naar" and
|
|
52
|
+
"Selectie" groups. -->
|
|
53
|
+
<NcActionCaption
|
|
54
|
+
v-if="entry.kind === 'heading'"
|
|
55
|
+
class="cn-index-header-menu__heading"
|
|
56
|
+
:name="entry.label"
|
|
57
|
+
:data-testid="`cn-index-header-menu-${entry.key}`" />
|
|
58
|
+
<NcActionSeparator
|
|
59
|
+
v-else-if="entry.kind === 'separator'"
|
|
60
|
+
:data-testid="`cn-index-header-menu-${entry.key}`" />
|
|
50
61
|
<NcActionLink
|
|
51
|
-
v-if="entry.href"
|
|
62
|
+
v-else-if="entry.href"
|
|
52
63
|
:href="entry.href"
|
|
53
64
|
target="_blank"
|
|
54
65
|
rel="noopener noreferrer"
|
|
@@ -58,6 +69,17 @@
|
|
|
58
69
|
</template>
|
|
59
70
|
{{ entry.label }}
|
|
60
71
|
</NcActionLink>
|
|
72
|
+
<NcActionLink
|
|
73
|
+
v-else-if="entry.link"
|
|
74
|
+
:href="entry.link.href"
|
|
75
|
+
:closeAfterClick="true"
|
|
76
|
+
:data-testid="`cn-index-header-menu-${entry.key}`"
|
|
77
|
+
@click="onHeaderMenuLinkClick(entry, $event)">
|
|
78
|
+
<template v-if="entry.icon" #icon>
|
|
79
|
+
<CnIcon :name="entry.icon" :size="20" />
|
|
80
|
+
</template>
|
|
81
|
+
{{ entry.label }}
|
|
82
|
+
</NcActionLink>
|
|
61
83
|
<NcActionButton
|
|
62
84
|
v-else
|
|
63
85
|
:disabled="entry.disabled"
|
|
@@ -65,8 +87,12 @@
|
|
|
65
87
|
:closeAfterClick="true"
|
|
66
88
|
:data-testid="`cn-index-header-menu-${entry.key}`"
|
|
67
89
|
@click="entry.run()">
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
<!-- A built-in ships its own icon (`iconComponent`), so it
|
|
91
|
+
never falls back to the "?" when the app has not
|
|
92
|
+
registered that icon name. -->
|
|
93
|
+
<template v-if="entry.iconComponent || entry.icon" #icon>
|
|
94
|
+
<component :is="entry.iconComponent" v-if="entry.iconComponent" :size="20" />
|
|
95
|
+
<CnIcon v-else :name="entry.icon" :size="20" />
|
|
70
96
|
</template>
|
|
71
97
|
{{ entry.label }}
|
|
72
98
|
</NcActionButton>
|
|
@@ -1028,7 +1054,7 @@
|
|
|
1028
1054
|
<script>
|
|
1029
1055
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
1030
1056
|
import { translate as t } from '@nextcloud/l10n'
|
|
1031
|
-
import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
|
|
1057
|
+
import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActionLink, NcActions, NcActionSeparator, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
|
|
1032
1058
|
import { getCurrentInstance, inject, markRaw, ref } from 'vue'
|
|
1033
1059
|
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
|
|
1034
1060
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
@@ -1038,6 +1064,8 @@ import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
|
|
1038
1064
|
import Export from 'vue-material-design-icons/Export.vue'
|
|
1039
1065
|
import Eye from 'vue-material-design-icons/Eye.vue'
|
|
1040
1066
|
import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
|
|
1067
|
+
import ImportIcon from 'vue-material-design-icons/Import.vue'
|
|
1068
|
+
import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
|
|
1041
1069
|
import Tune from 'vue-material-design-icons/Tune.vue'
|
|
1042
1070
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
1043
1071
|
import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
@@ -1053,13 +1081,13 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
|
|
|
1053
1081
|
import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'
|
|
1054
1082
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
1055
1083
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
1056
|
-
import { routeHref } from '../../utils/actionLink.js'
|
|
1084
|
+
import { followItemActionLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
|
|
1057
1085
|
import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
1058
1086
|
import { reportBindingProblems } from '../../utils/diagnostics.js'
|
|
1059
1087
|
import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
|
|
1060
1088
|
import { buildExportUrl } from '../../utils/indexExportHelpers.js'
|
|
1061
1089
|
import { lensUnavailableText } from '../../utils/lensAvailability.js'
|
|
1062
|
-
import { openRowTarget } from '../../utils/linkNavigation.js'
|
|
1090
|
+
import { isModifiedClick, openRowTarget } from '../../utils/linkNavigation.js'
|
|
1063
1091
|
import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
|
|
1064
1092
|
import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
|
|
1065
1093
|
import { multiKeySort } from '../../utils/multiKeySort.js'
|
|
@@ -1352,6 +1380,7 @@ export default {
|
|
|
1352
1380
|
NcActionLink,
|
|
1353
1381
|
NcActionButton,
|
|
1354
1382
|
NcActionCaption,
|
|
1383
|
+
NcActionSeparator,
|
|
1355
1384
|
NcActionCheckbox,
|
|
1356
1385
|
NcButton,
|
|
1357
1386
|
AlertCircleOutline,
|
|
@@ -1631,9 +1660,11 @@ export default {
|
|
|
1631
1660
|
* actions, or what its `items` name (`header-actions`, a header action
|
|
1632
1661
|
* id, `import`, `export`, `refresh`, `copy-selected`, `delete-selected`,
|
|
1633
1662
|
* `documentation`), so the entries the bar's menu loses have a place.
|
|
1663
|
+
* `items` can group them: `{ heading }` draws a group heading (an i18n
|
|
1664
|
+
* key) and `separator` a rule, each only when an entry follows.
|
|
1634
1665
|
*
|
|
1635
1666
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
1636
|
-
* @type {Array<{label?: string, action: string, variant?: ('primary'|'secondary'), icon?: string, format?: ('csv'|'excel'), id?: string, items?: string
|
|
1667
|
+
* @type {Array<{label?: string, action: string, variant?: ('primary'|'secondary'), icon?: string, format?: ('csv'|'excel'), id?: string, items?: Array<(string|{heading: string})>}>}
|
|
1637
1668
|
*/
|
|
1638
1669
|
headerButtons: {
|
|
1639
1670
|
type: Array,
|
|
@@ -5560,7 +5591,10 @@ export default {
|
|
|
5560
5591
|
variant: button.variant === 'primary' ? 'primary' : 'secondary',
|
|
5561
5592
|
icon,
|
|
5562
5593
|
format: button.format === 'excel' ? 'excel' : 'csv',
|
|
5563
|
-
items: Array.isArray(button.items)
|
|
5594
|
+
items: Array.isArray(button.items)
|
|
5595
|
+
? button.items.filter((item) => (typeof item === 'string' && item !== '')
|
|
5596
|
+
|| (item && typeof item === 'object' && typeof item.heading === 'string' && item.heading !== ''))
|
|
5597
|
+
: null,
|
|
5564
5598
|
}
|
|
5565
5599
|
})
|
|
5566
5600
|
.filter((button) => button.label !== '')
|
|
@@ -6855,11 +6889,14 @@ export default {
|
|
|
6855
6889
|
* `import`, `export`, `refresh`, `copy-selected`, `delete-selected`
|
|
6856
6890
|
* (disabled until a row is selected) and `documentation` (a link to
|
|
6857
6891
|
* `documentationUrl`, left out when the page has none). An unknown name
|
|
6858
|
-
* is left out.
|
|
6892
|
+
* is left out. `{ heading }` and `separator` group the entries: an
|
|
6893
|
+
* entry of `kind` `heading` or `separator`, drawn only when an entry
|
|
6894
|
+
* follows it.
|
|
6859
6895
|
*
|
|
6860
6896
|
* @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
|
|
6861
|
-
* @
|
|
6862
|
-
* @
|
|
6897
|
+
* @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
|
|
6898
|
+
* @param {{items?: Array<(string|{heading: string})>|null, format?: string}} button The resolved header button.
|
|
6899
|
+
* @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, disabled?: boolean, title?: string, iconComponent?: object, run?: Function}>}
|
|
6863
6900
|
*/
|
|
6864
6901
|
headerMenuEntries(button) {
|
|
6865
6902
|
const fromAction = (entry) => ({
|
|
@@ -6867,14 +6904,18 @@ export default {
|
|
|
6867
6904
|
label: entry.label ? this.cnTranslate(entry.label) : '',
|
|
6868
6905
|
icon: entry.icon && typeof entry.icon === 'string' && !entry.icon.startsWith('icon-') ? entry.icon : '',
|
|
6869
6906
|
href: typeof entry.href === 'string' && entry.href !== '' ? entry.href : '',
|
|
6907
|
+
// A `handler: "navigate"` entry whose route resolves carries `to`
|
|
6908
|
+
// (resolveHeaderHandler drops its handler): the menu draws it as
|
|
6909
|
+
// an in-app link, as the actions bar does, so it navigates.
|
|
6910
|
+
link: !entry.href && entry.to && !entry.disabled ? resolveItemActionLink({ to: entry.to }, null, this.$router) : null,
|
|
6870
6911
|
disabled: Boolean(entry.disabled),
|
|
6871
6912
|
run: () => this.onHeaderAction({ action: entry.id, id: entry.id }),
|
|
6872
6913
|
})
|
|
6873
6914
|
const names = button && Array.isArray(button.items) ? button.items : ['header-actions']
|
|
6874
6915
|
const noSelection = this.internalSelectedIds.length < 1
|
|
6875
6916
|
const builtIns = {
|
|
6876
|
-
import: () => ({ key: 'import', label: t('nextcloud-vue', 'Import'), icon: 'Import', run: () => { this.showImportDialog = true } }),
|
|
6877
|
-
export: () => ({ key: 'export', label: t('nextcloud-vue', 'Export'), icon: 'Export', run: () => this.onHeaderButton({ action: 'export', format: button.format }) }),
|
|
6917
|
+
import: () => ({ key: 'import', label: t('nextcloud-vue', 'Import'), icon: 'Import', iconComponent: ImportIcon, run: () => { this.showImportDialog = true } }),
|
|
6918
|
+
export: () => ({ key: 'export', label: t('nextcloud-vue', 'Export'), icon: 'Export', iconComponent: Export, run: () => this.onHeaderButton({ action: 'export', format: button.format }) }),
|
|
6878
6919
|
refresh: () => ({ key: 'refresh', label: t('nextcloud-vue', 'Refresh'), icon: 'Refresh', run: () => this.onRefreshEvent() }),
|
|
6879
6920
|
'copy-selected': () => ({
|
|
6880
6921
|
key: 'copy-selected',
|
|
@@ -6889,6 +6930,7 @@ export default {
|
|
|
6889
6930
|
key: 'delete-selected',
|
|
6890
6931
|
label: t('nextcloud-vue', 'Delete selected'),
|
|
6891
6932
|
icon: 'TrashCanOutline',
|
|
6933
|
+
iconComponent: TrashCanOutline,
|
|
6892
6934
|
disabled: noSelection,
|
|
6893
6935
|
title: noSelection ? t('nextcloud-vue', 'Select 1 or more items to delete') : '',
|
|
6894
6936
|
run: () => { this.showMassDeleteDialog = true },
|
|
@@ -6906,8 +6948,17 @@ export default {
|
|
|
6906
6948
|
entries.push(entry)
|
|
6907
6949
|
}
|
|
6908
6950
|
}
|
|
6951
|
+
// A group heading (`{ heading }`) or a `separator` is kept as a mark
|
|
6952
|
+
// and only drawn when an entry follows it (see groupMenuEntries).
|
|
6953
|
+
let mark = 0
|
|
6909
6954
|
for (const name of names) {
|
|
6910
|
-
if (name === '
|
|
6955
|
+
if (name && typeof name === 'object') {
|
|
6956
|
+
mark += 1
|
|
6957
|
+
entries.push({ key: `__heading-${mark}`, kind: 'heading', label: this.cnTranslate(name.heading) })
|
|
6958
|
+
} else if (name === 'separator') {
|
|
6959
|
+
mark += 1
|
|
6960
|
+
entries.push({ key: `__separator-${mark}`, kind: 'separator', label: '' })
|
|
6961
|
+
} else if (name === 'header-actions') {
|
|
6911
6962
|
this.mergedHeaderActions.forEach((entry) => push(fromAction(entry)))
|
|
6912
6963
|
} else if (Object.hasOwn(builtIns, name)) {
|
|
6913
6964
|
push(builtIns[name]())
|
|
@@ -6918,7 +6969,66 @@ export default {
|
|
|
6918
6969
|
}
|
|
6919
6970
|
}
|
|
6920
6971
|
}
|
|
6921
|
-
return entries
|
|
6972
|
+
return this.groupMenuEntries(entries)
|
|
6973
|
+
},
|
|
6974
|
+
|
|
6975
|
+
/**
|
|
6976
|
+
* Tidy the marks of the header's Actions menu: a heading or separator
|
|
6977
|
+
* with no entry after it (before the next mark or the end) is dropped,
|
|
6978
|
+
* a separator never opens the menu, two separators in a row are one,
|
|
6979
|
+
* and a heading followed by a separator before any entry is dropped. So a group whose entries are all left out (no documentation
|
|
6980
|
+
* URL, an unknown id) leaves no empty heading or double rule behind,
|
|
6981
|
+
* and a menu of marks only has no entries and is not drawn.
|
|
6982
|
+
*
|
|
6983
|
+
* @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
|
|
6984
|
+
* @param {Array<{key: string, kind?: string}>} entries The entries with their marks, in order.
|
|
6985
|
+
* @return {Array<{key: string, kind?: string}>} The entries to draw.
|
|
6986
|
+
*/
|
|
6987
|
+
groupMenuEntries(entries) {
|
|
6988
|
+
const out = []
|
|
6989
|
+
let separator = null
|
|
6990
|
+
let heading = null
|
|
6991
|
+
for (const entry of entries) {
|
|
6992
|
+
if (entry.kind === 'separator') {
|
|
6993
|
+
// A rule ends the group before it: a heading not yet followed
|
|
6994
|
+
// by an entry is dropped with it.
|
|
6995
|
+
separator = entry
|
|
6996
|
+
heading = null
|
|
6997
|
+
continue
|
|
6998
|
+
}
|
|
6999
|
+
if (entry.kind === 'heading') {
|
|
7000
|
+
heading = entry
|
|
7001
|
+
continue
|
|
7002
|
+
}
|
|
7003
|
+
if (separator && out.length > 0) {
|
|
7004
|
+
out.push(separator)
|
|
7005
|
+
}
|
|
7006
|
+
if (heading) {
|
|
7007
|
+
out.push(heading)
|
|
7008
|
+
}
|
|
7009
|
+
separator = null
|
|
7010
|
+
heading = null
|
|
7011
|
+
out.push(entry)
|
|
7012
|
+
}
|
|
7013
|
+
return out
|
|
7014
|
+
},
|
|
7015
|
+
|
|
7016
|
+
/**
|
|
7017
|
+
* An in-app link of the header's Actions menu was clicked: a plain click
|
|
7018
|
+
* goes through the router (a modified click is left to the browser),
|
|
7019
|
+
* and the entry is reported as a header action, as the actions bar does.
|
|
7020
|
+
*
|
|
7021
|
+
* @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
|
|
7022
|
+
* @param {{key: string, link: object}} entry The menu entry.
|
|
7023
|
+
* @param {MouseEvent} event The click.
|
|
7024
|
+
* @return {void}
|
|
7025
|
+
*/
|
|
7026
|
+
onHeaderMenuLinkClick(entry, event) {
|
|
7027
|
+
if (isModifiedClick(event)) {
|
|
7028
|
+
return
|
|
7029
|
+
}
|
|
7030
|
+
followItemActionLink(event, entry.link, this.$router)
|
|
7031
|
+
this.$emit('header-action', { action: entry.key, id: entry.key })
|
|
6922
7032
|
},
|
|
6923
7033
|
|
|
6924
7034
|
/**
|