@conduction/nextcloud-vue 2.56.0-beta.31 → 2.56.0-beta.32
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/CnBannerWidget/CnBannerWidget.vue2.js +55 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +51 -15
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +20 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +7 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +77 -3
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +0 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
- 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/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
- package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +4 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +1 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +1 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +843 -4
- package/dist/esm/utils/contentHeaderLink.js +51 -0
- package/dist/esm/utils/contentHeaderLink.js.map +1 -0
- package/dist/esm/utils/fetchFilterCounts.js +40 -5
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/initials.js +32 -0
- package/dist/esm/utils/initials.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +7 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectWrite.js +59 -0
- package/dist/esm/utils/objectWrite.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +10510 -6830
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +843 -4
- package/l10n/en.json +1 -0
- package/l10n/nl.json +1 -0
- package/package.json +1 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +93 -13
- package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +43 -3
- package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
- package/src/components/CnChipsFieldWidget/index.js +25 -0
- package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
- package/src/components/CnChoicePickerWidget/index.js +26 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +38 -2
- package/src/components/CnDataTable/CnDataTable.vue +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +45 -2
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -3
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +265 -4
- package/src/components/CnIndexPage/CnIndexPage.vue +0 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
- package/src/components/CnRadioCardsWidget/index.js +27 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +43 -1
- package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
- package/src/components/CnSavedViewsWidget/index.js +29 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
- package/src/components/CnSettingsFormWidget/index.js +26 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
- package/src/components/CnSwitchRowsWidget/index.js +25 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
- package/src/components/CnToggleTableWidget/index.js +26 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +28 -1
- package/src/components/index.js +7 -0
- package/src/css/look-board-detail.css +36 -2
- package/src/css/look-board-index.css +23 -0
- package/src/css/look-board.css +72 -0
- package/src/index.js +7 -0
- package/src/utils/contentHeaderLink.js +48 -0
- package/src/utils/fetchFilterCounts.js +39 -4
- package/src/utils/initials.js +29 -0
- package/src/utils/libraryWidgetKeys.js +7 -0
- package/src/utils/objectWrite.js +55 -0
- package/src/utils/widgetDispatch.js +1 -1
|
@@ -163,6 +163,7 @@ import widgetLink from '../../mixins/widgetLink.js'
|
|
|
163
163
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
164
164
|
import { resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
165
165
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
166
|
+
import { fetchListTotal, needsListEndpoint } from '../../utils/fetchFilterCounts.js'
|
|
166
167
|
import { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
167
168
|
import { dropOptionalUnresolved, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
168
169
|
import { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'
|
|
@@ -1746,8 +1747,7 @@ export default {
|
|
|
1746
1747
|
// drop any optional `@workspace.<key>?` / `@config.<key>?` that stayed
|
|
1747
1748
|
// unresolved so an unset value omits the filter (show all) instead of
|
|
1748
1749
|
// sending a literal token.
|
|
1749
|
-
|
|
1750
|
-
filter = dropOptionalUnresolved(resolveFilterTokens(filter, ctx))
|
|
1750
|
+
filter = this.resolveSourceFilter(filter)
|
|
1751
1751
|
for (const [k, v] of Object.entries(filter)) {
|
|
1752
1752
|
if (v && typeof v === 'object') {
|
|
1753
1753
|
for (const [op, ov] of Object.entries(v)) {
|
|
@@ -1759,6 +1759,21 @@ export default {
|
|
|
1759
1759
|
}
|
|
1760
1760
|
},
|
|
1761
1761
|
|
|
1762
|
+
/**
|
|
1763
|
+
* A source filter with its @-tokens resolved against the object, page
|
|
1764
|
+
* and config contexts, and unresolved optional clauses dropped.
|
|
1765
|
+
*
|
|
1766
|
+
* @param {object} filter The raw filter map.
|
|
1767
|
+
* @return {object} The resolved filter.
|
|
1768
|
+
*/
|
|
1769
|
+
resolveSourceFilter(filter) {
|
|
1770
|
+
if (!filter || typeof filter !== 'object') {
|
|
1771
|
+
return {}
|
|
1772
|
+
}
|
|
1773
|
+
const ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx, config: this.configCtx }
|
|
1774
|
+
return dropOptionalUnresolved(resolveFilterTokens(filter, ctx))
|
|
1775
|
+
},
|
|
1776
|
+
|
|
1762
1777
|
/**
|
|
1763
1778
|
* Fetch one scalar from the OpenRegister `/value` aggregation endpoint.
|
|
1764
1779
|
*
|
|
@@ -1768,9 +1783,19 @@ export default {
|
|
|
1768
1783
|
* @param {string} metric The aggregation metric.
|
|
1769
1784
|
* @param {?string} field The numeric field (non-count metrics).
|
|
1770
1785
|
* @param {object} filter The filter map.
|
|
1786
|
+
* @param {string} [via] `"list"` counts through the list endpoint's total.
|
|
1771
1787
|
* @return {Promise<number|null>} The aggregated value.
|
|
1772
1788
|
*/
|
|
1773
|
-
async fetchAggregate(axios, generateUrl, s, metric, field, filter) {
|
|
1789
|
+
async fetchAggregate(axios, generateUrl, s, metric, field, filter, via) {
|
|
1790
|
+
// A count the aggregation endpoint cannot express (a filter holding a
|
|
1791
|
+
// list of values, a test for no value (`null` or `{ isnull: true }`),
|
|
1792
|
+
// or `via: "list"`) reads the list endpoint's total instead, because
|
|
1793
|
+
// the aggregation endpoint has no null operator.
|
|
1794
|
+
const resolved = this.resolveSourceFilter(filter)
|
|
1795
|
+
const listed = Object.values(resolved).some((v) => Array.isArray(v) || (v && typeof v === 'object' && Object.values(v).some(Array.isArray)))
|
|
1796
|
+
if ((!metric || metric === 'count') && (via === 'list' || listed || needsListEndpoint(resolved))) {
|
|
1797
|
+
return fetchListTotal(s.register, s.schema, resolved, {})
|
|
1798
|
+
}
|
|
1774
1799
|
const url = generateUrl(
|
|
1775
1800
|
'/apps/openregister/api/objects/aggregations/{register}/{schema}/value',
|
|
1776
1801
|
{ register: s.register, schema: s.schema },
|
|
@@ -1827,9 +1852,15 @@ export default {
|
|
|
1827
1852
|
if (s.kind === 'endpoint') {
|
|
1828
1853
|
this.value = await this.fetchEndpoint(s, refresh)
|
|
1829
1854
|
} else if (s.kind === 'ratio') {
|
|
1830
|
-
|
|
1831
|
-
|
|
1855
|
+
// Each part may name its own metric, field and `via`, so "answered
|
|
1856
|
+
// in time" over "answered" is two counts, and a share of a sum is
|
|
1857
|
+
// two sums.
|
|
1858
|
+
const part = (p) => this.fetchAggregate(axios, generateUrl, s, (p && p.metric) || s.metric, (p && p.field) || s.field, (p && p.filter) || {}, (p && p.via) || s.via)
|
|
1859
|
+
const num = await part(s.numerator)
|
|
1860
|
+
const den = await part(s.denominator)
|
|
1832
1861
|
this.value = (den && Number(den) !== 0) ? (Number(num) / Number(den)) * 100 : null
|
|
1862
|
+
} else if (s.kind === 'elapsed') {
|
|
1863
|
+
this.value = await this.fetchElapsed(axios, generateUrl, s)
|
|
1833
1864
|
} else if (s.kind === 'computed') {
|
|
1834
1865
|
// Fetch each named part, then evaluate the formula over them.
|
|
1835
1866
|
const { evalFormula } = await import('../../utils/evalFormula.js')
|
|
@@ -1842,7 +1873,7 @@ export default {
|
|
|
1842
1873
|
} else if (s.kind === 'weighted') {
|
|
1843
1874
|
this.value = await this.fetchWeighted(axios, generateUrl, s)
|
|
1844
1875
|
} else {
|
|
1845
|
-
this.value = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, s.filter || {})
|
|
1876
|
+
this.value = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, s.filter || {}, s.via)
|
|
1846
1877
|
}
|
|
1847
1878
|
} catch (e) {
|
|
1848
1879
|
this.error = (e && e.message) || 'error'
|
|
@@ -1852,6 +1883,56 @@ export default {
|
|
|
1852
1883
|
}
|
|
1853
1884
|
},
|
|
1854
1885
|
|
|
1886
|
+
/**
|
|
1887
|
+
* The average time between two date fields of the matching objects
|
|
1888
|
+
* (`{ from, to, unit?, filter?, limit? }`): an average answer time is
|
|
1889
|
+
* `occurredAt` to `resolvedAt`. Computed client-side over the fetched
|
|
1890
|
+
* objects (capped at `limit`, default 1000), because the aggregation
|
|
1891
|
+
* endpoint averages one numeric field, not a difference. Objects missing
|
|
1892
|
+
* either date, or with `to` before `from`, are left out; null when none
|
|
1893
|
+
* is left. `unit` is `days` (default), `hours` or `minutes`.
|
|
1894
|
+
*
|
|
1895
|
+
* @param {object} axios The axios instance.
|
|
1896
|
+
* @param {(url: string, params?: object) => string} generateUrl The router helper.
|
|
1897
|
+
* @param {object} s The elapsed source.
|
|
1898
|
+
* @return {Promise<number|null>} The average, in `unit`.
|
|
1899
|
+
* @spec openspec/changes/screens-ratio-stat/specs/ratio-stat/spec.md#requirement-a-stat-can-average-the-time-between-two-dates
|
|
1900
|
+
*/
|
|
1901
|
+
async fetchElapsed(axios, generateUrl, s) {
|
|
1902
|
+
if (!s.from || !s.to) {
|
|
1903
|
+
return null
|
|
1904
|
+
}
|
|
1905
|
+
const url = generateUrl(
|
|
1906
|
+
'/apps/openregister/api/objects/{register}/{schema}',
|
|
1907
|
+
{ register: s.register, schema: s.schema },
|
|
1908
|
+
)
|
|
1909
|
+
const params = { _limit: s.limit || 1000 }
|
|
1910
|
+
const filter = this.resolveSourceFilter(s.filter || {})
|
|
1911
|
+
for (const [k, v] of Object.entries(filter)) {
|
|
1912
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
1913
|
+
for (const [op, operand] of Object.entries(v)) {
|
|
1914
|
+
params[`${k}[${op}]`] = operand
|
|
1915
|
+
}
|
|
1916
|
+
} else if (v !== '' && v !== null && v !== undefined) {
|
|
1917
|
+
params[k] = v
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
const res = await axios.get(url, { params })
|
|
1921
|
+
const rows = (res && res.data && res.data.results) || []
|
|
1922
|
+
const per = { minutes: 60000, hours: 3600000, days: 86400000 }[s.unit] || 86400000
|
|
1923
|
+
let sum = 0
|
|
1924
|
+
let n = 0
|
|
1925
|
+
for (const r of rows) {
|
|
1926
|
+
const from = Date.parse(r && r[s.from])
|
|
1927
|
+
const to = Date.parse(r && r[s.to])
|
|
1928
|
+
if (Number.isFinite(from) && Number.isFinite(to) && to >= from) {
|
|
1929
|
+
sum += (to - from) / per
|
|
1930
|
+
n += 1
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return n > 0 ? sum / n : null
|
|
1934
|
+
},
|
|
1935
|
+
|
|
1855
1936
|
/**
|
|
1856
1937
|
* Compute a weighted sum `Σ (field × weightField) ÷ divisor` client-side
|
|
1857
1938
|
* (no OpenRegister expression-aggregation primitive yet). Pulls the
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-switch-rows" data-testid="cn-switch-rows">
|
|
8
|
+
<ul class="cn-switch-rows__list">
|
|
9
|
+
<li
|
|
10
|
+
v-for="row in rows"
|
|
11
|
+
:key="row.key"
|
|
12
|
+
class="cn-switch-rows__row"
|
|
13
|
+
:data-testid="`cn-switch-rows-row-${row.key}`">
|
|
14
|
+
<NcCheckboxRadioSwitch
|
|
15
|
+
:modelValue="values[row.key] === true"
|
|
16
|
+
type="switch"
|
|
17
|
+
:disabled="readOnly || busy === row.key"
|
|
18
|
+
@update:modelValue="toggle(row, $event)">
|
|
19
|
+
<span class="cn-switch-rows__label">{{ row.label }}</span>
|
|
20
|
+
<span v-if="row.hint" class="cn-switch-rows__hint">{{ row.hint }}</span>
|
|
21
|
+
</NcCheckboxRadioSwitch>
|
|
22
|
+
</li>
|
|
23
|
+
</ul>
|
|
24
|
+
<NcNoteCard v-if="notice" type="error" data-testid="cn-switch-rows-notice">
|
|
25
|
+
{{ notice }}
|
|
26
|
+
</NcNoteCard>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import { NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
|
|
32
|
+
import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
|
|
33
|
+
import { readPath } from '../../utils/readPath.js'
|
|
34
|
+
import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* CnSwitchRowsWidget — the `switch-rows` widget: a card of switches, each with
|
|
38
|
+
* a label and a hint, bound to boolean fields of the CURRENT object. A toggle
|
|
39
|
+
* saves at once with ONE PATCH of that field; a refused PATCH puts the switch
|
|
40
|
+
* back and shows the server's words.
|
|
41
|
+
*
|
|
42
|
+
* @spec openspec/changes/abstract-switch-rows/specs/switch-rows/spec.md
|
|
43
|
+
*/
|
|
44
|
+
export default {
|
|
45
|
+
name: 'CnSwitchRowsWidget',
|
|
46
|
+
|
|
47
|
+
components: { NcCheckboxRadioSwitch, NcNoteCard },
|
|
48
|
+
|
|
49
|
+
inject: {
|
|
50
|
+
cnTranslate: { default: () => (key) => key },
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
props: {
|
|
54
|
+
/**
|
|
55
|
+
* The widget configuration.
|
|
56
|
+
*
|
|
57
|
+
* - `rows` (required) — `[{ key, label, hint?, invert? }]`; `key` is a top-level boolean field. `invert` shows the switch on when the field is false (a stored `disabled` read as "enabled").
|
|
58
|
+
* - `readOnly`.
|
|
59
|
+
*
|
|
60
|
+
* @type {object}
|
|
61
|
+
*/
|
|
62
|
+
content: {
|
|
63
|
+
type: Object,
|
|
64
|
+
default: () => ({}),
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** The current detail-page object. */
|
|
68
|
+
objectData: {
|
|
69
|
+
type: Object,
|
|
70
|
+
default: null,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** The current object id. */
|
|
74
|
+
objectId: {
|
|
75
|
+
type: [String, Number],
|
|
76
|
+
default: '',
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** OpenRegister register slug of the surface. */
|
|
80
|
+
register: {
|
|
81
|
+
type: [String, Object],
|
|
82
|
+
default: '',
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/** OpenRegister schema slug of the surface. */
|
|
86
|
+
schema: {
|
|
87
|
+
type: [String, Object],
|
|
88
|
+
default: '',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
emits: ['saved'],
|
|
93
|
+
|
|
94
|
+
data() {
|
|
95
|
+
return {
|
|
96
|
+
/** The switch states as last saved (by key, as the switch shows them). */
|
|
97
|
+
saved: {},
|
|
98
|
+
values: {},
|
|
99
|
+
busy: '',
|
|
100
|
+
notice: '',
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
computed: {
|
|
105
|
+
rows() {
|
|
106
|
+
const list = Array.isArray(this.content.rows) ? this.content.rows : []
|
|
107
|
+
return list
|
|
108
|
+
.filter((row) => row && row.key)
|
|
109
|
+
.map((row) => ({ ...row, label: this.tr(row.label || row.key), hint: this.tr(row.hint || '') }))
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
readOnly() {
|
|
113
|
+
return this.content.readOnly === true
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
objectUrl() {
|
|
117
|
+
return objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
watch: {
|
|
122
|
+
objectData() {
|
|
123
|
+
this.adopt()
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
created() {
|
|
128
|
+
this.adopt()
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
methods: {
|
|
132
|
+
tr(text) {
|
|
133
|
+
if (typeof text !== 'string' || text === '') {
|
|
134
|
+
return ''
|
|
135
|
+
}
|
|
136
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/** Read every switch off the object. */
|
|
140
|
+
adopt() {
|
|
141
|
+
const values = {}
|
|
142
|
+
for (const row of this.rows) {
|
|
143
|
+
const stored = readPath(this.objectData || {}, row.key) === true
|
|
144
|
+
values[row.key] = row.invert ? !stored : stored
|
|
145
|
+
}
|
|
146
|
+
this.saved = values
|
|
147
|
+
this.values = { ...values }
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Flip one switch and save that one field.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} row The row.
|
|
154
|
+
* @param {boolean} on The new switch state.
|
|
155
|
+
* @return {Promise<void>}
|
|
156
|
+
* @spec openspec/changes/abstract-switch-rows/specs/switch-rows/spec.md#requirement-a-toggle-saves-that-one-field
|
|
157
|
+
*/
|
|
158
|
+
async toggle(row, on) {
|
|
159
|
+
if (!this.objectUrl) {
|
|
160
|
+
this.notice = this.tr('This switch cannot be saved here.')
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
this.notice = ''
|
|
164
|
+
this.busy = row.key
|
|
165
|
+
this.values = { ...this.values, [row.key]: on }
|
|
166
|
+
const stored = row.invert ? !on : on
|
|
167
|
+
try {
|
|
168
|
+
const data = await patchObject(this.objectUrl, { [row.key]: stored })
|
|
169
|
+
const written = data[row.key] !== undefined ? data[row.key] === true : stored
|
|
170
|
+
const shown = row.invert ? !written : written
|
|
171
|
+
this.saved = { ...this.saved, [row.key]: shown }
|
|
172
|
+
this.values = { ...this.values, [row.key]: shown }
|
|
173
|
+
/**
|
|
174
|
+
* @event saved A switch was written.
|
|
175
|
+
* @type {{field: string, value: boolean}}
|
|
176
|
+
*/
|
|
177
|
+
this.$emit('saved', { field: row.key, value: written })
|
|
178
|
+
} catch (e) {
|
|
179
|
+
this.values = { ...this.values, [row.key]: this.saved[row.key] }
|
|
180
|
+
this.notice = serverErrorMessage(e)
|
|
181
|
+
} finally {
|
|
182
|
+
this.busy = ''
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<style scoped>
|
|
190
|
+
.cn-switch-rows {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
gap: 8px;
|
|
194
|
+
min-width: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cn-switch-rows__list {
|
|
198
|
+
list-style: none;
|
|
199
|
+
margin: 0;
|
|
200
|
+
padding: 0;
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cn-switch-rows__row {
|
|
206
|
+
padding: 10px 0;
|
|
207
|
+
border-top: 1px solid var(--cn-board-hairline, var(--color-border));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.cn-switch-rows__row:first-child {
|
|
211
|
+
border-top: none;
|
|
212
|
+
padding-top: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.cn-switch-rows__label {
|
|
216
|
+
display: block;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.cn-switch-rows__hint {
|
|
221
|
+
display: block;
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
font-weight: 400;
|
|
224
|
+
color: var(--color-text-maxcontrast);
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnSwitchRowsWidget — self-registers the `switch-rows` widget type (a card of
|
|
6
|
+
* switches, each with a label and a hint, bound to boolean fields of the
|
|
7
|
+
* current object and saved on toggle) into the shared dashboardWidgetRegistry.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import CnSwitchRowsWidget from './CnSwitchRowsWidget.vue'
|
|
11
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
12
|
+
|
|
13
|
+
registerDashboardWidget('switch-rows', {
|
|
14
|
+
renderer: CnSwitchRowsWidget,
|
|
15
|
+
form: null,
|
|
16
|
+
defaultContent: {
|
|
17
|
+
rows: [],
|
|
18
|
+
},
|
|
19
|
+
displayName: 'Switch rows',
|
|
20
|
+
icon: 'ToggleSwitch',
|
|
21
|
+
surfaces: ['detail-page'],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export { CnSwitchRowsWidget }
|
|
25
|
+
export default CnSwitchRowsWidget
|
|
@@ -85,8 +85,9 @@
|
|
|
85
85
|
<div
|
|
86
86
|
v-if="entry.widget && entry.stack.length"
|
|
87
87
|
class="cn-tabs-widget__stack"
|
|
88
|
+
:class="{ 'cn-tabs-widget__stack--grid': !!entry.grid }"
|
|
88
89
|
data-testid="cn-tabs-widget-stack">
|
|
89
|
-
<div class="cn-tabs-widget__stack-first">
|
|
90
|
+
<div class="cn-tabs-widget__stack-first" :style="spanStyle(entry.grid, 0)">
|
|
90
91
|
<CnDetailWidgetHost
|
|
91
92
|
:widget="entry.widget"
|
|
92
93
|
chrome="card"
|
|
@@ -108,9 +109,10 @@
|
|
|
108
109
|
@selectObject="onSelectObject" />
|
|
109
110
|
</div>
|
|
110
111
|
<div
|
|
111
|
-
v-for="child in entry.stack"
|
|
112
|
+
v-for="(child, childIndex) in entry.stack"
|
|
112
113
|
:key="`${entry.key}-${child.id}`"
|
|
113
114
|
class="cn-tabs-widget__stack-item"
|
|
115
|
+
:style="spanStyle(entry.grid, childIndex + 1)"
|
|
114
116
|
:data-testid="`cn-tabs-widget-stack-${child.id}`">
|
|
115
117
|
<CnDetailWidgetHost
|
|
116
118
|
:widget="child"
|
|
@@ -330,12 +332,17 @@ export default {
|
|
|
330
332
|
* names the list; `count` and `countField` win over it. `overflow: true`
|
|
331
333
|
* lists the tab under "More".
|
|
332
334
|
*
|
|
335
|
+
* A tab with `widgetIds` can lay them out in a grid instead of one under
|
|
336
|
+
* the other: `columns: 4` gives each a quarter of the row, and
|
|
337
|
+
* `layout: [{ widgetId, gridWidth }]` sets a width (of 12) per widget, as
|
|
338
|
+
* the page grid does. Widgets wrap onto the next row.
|
|
339
|
+
*
|
|
333
340
|
* `maxVisibleTabs` caps the strip: later tabs go under "More".
|
|
334
341
|
* `hideEmpty: true` moves a tab whose count is 0 there too.
|
|
335
342
|
* `moreLabel` names that menu. `variant: "segmented"` draws the strip
|
|
336
343
|
* as a pill switch (CnTabs' segmented variant); `line` is the default.
|
|
337
344
|
*
|
|
338
|
-
* @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}), overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
|
|
345
|
+
* @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}), overflow?: boolean, columns?: number, layout?: Array<{widgetId: string, gridWidth: number}>}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
|
|
339
346
|
*/
|
|
340
347
|
content: {
|
|
341
348
|
type: Object,
|
|
@@ -552,6 +559,7 @@ export default {
|
|
|
552
559
|
icon: (tab && tab.icon) || widget?.icon || '',
|
|
553
560
|
widget,
|
|
554
561
|
stack: widgets.slice(1),
|
|
562
|
+
grid: this.stackGrid(tab, widgets),
|
|
555
563
|
count,
|
|
556
564
|
overflow,
|
|
557
565
|
pending: visibility === 'pending',
|
|
@@ -751,6 +759,46 @@ export default {
|
|
|
751
759
|
},
|
|
752
760
|
|
|
753
761
|
methods: {
|
|
762
|
+
/**
|
|
763
|
+
* The grid a stack lays its widgets out in, as one column span (of 12)
|
|
764
|
+
* per widget in order, or null for the plain vertical stack. A tab says
|
|
765
|
+
* `columns: 4` (every widget spans a quarter of the row) and/or
|
|
766
|
+
* `layout: [{ widgetId, gridWidth }]`, the page grid's width key per
|
|
767
|
+
* widget (of 12; a widget it does not name takes the `columns` share, or
|
|
768
|
+
* the whole row). Widgets wrap onto the next row. Heights follow the
|
|
769
|
+
* content.
|
|
770
|
+
*
|
|
771
|
+
* @param {object|string} tab The `content.tabs[]` entry.
|
|
772
|
+
* @param {Array<object>} widgets The widgets of the stack, in order.
|
|
773
|
+
* @return {number[]|null} The column span of each widget, or null.
|
|
774
|
+
*/
|
|
775
|
+
stackGrid(tab, widgets) {
|
|
776
|
+
if (!tab || typeof tab !== 'object' || widgets.length < 2) {
|
|
777
|
+
return null
|
|
778
|
+
}
|
|
779
|
+
const clamp = (value) => Math.min(12, Math.max(1, Math.floor(Number(value))))
|
|
780
|
+
const columns = Number(tab.columns)
|
|
781
|
+
const share = Number.isFinite(columns) && columns >= 1 ? clamp(12 / Math.min(12, Math.floor(columns))) : 12
|
|
782
|
+
const placed = new Map((Array.isArray(tab.layout) ? tab.layout : [])
|
|
783
|
+
.filter((entry) => entry && typeof entry.widgetId === 'string' && Number.isFinite(Number(entry.gridWidth)))
|
|
784
|
+
.map((entry) => [entry.widgetId, clamp(entry.gridWidth)]))
|
|
785
|
+
if (placed.size === 0 && share === 12) {
|
|
786
|
+
return null
|
|
787
|
+
}
|
|
788
|
+
return widgets.map((widget) => placed.get(widget.id) ?? share)
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* The inline column span of one widget of a gridded stack.
|
|
793
|
+
*
|
|
794
|
+
* @param {number[]|null} grid The spans of the stack, or null.
|
|
795
|
+
* @param {number} index The widget's position in the stack.
|
|
796
|
+
* @return {object|undefined} The style, or undefined without a grid.
|
|
797
|
+
*/
|
|
798
|
+
spanStyle(grid, index) {
|
|
799
|
+
return grid ? { gridColumn: `span ${grid[index] || 12}` } : undefined
|
|
800
|
+
},
|
|
801
|
+
|
|
754
802
|
/**
|
|
755
803
|
* The widget ids a tab names: `widgetIds` (a stack) when it holds at
|
|
756
804
|
* least one id, else `widgetId`, or the bare string.
|
|
@@ -999,6 +1047,29 @@ export default {
|
|
|
999
1047
|
gap: 20px;
|
|
1000
1048
|
}
|
|
1001
1049
|
|
|
1050
|
+
/* A gridded stack (`columns` / `layout` on the tab): 12 columns, widgets wrap
|
|
1051
|
+
onto the next row, a row's cards as tall as the tallest. Narrow, it is the
|
|
1052
|
+
plain stack again. */
|
|
1053
|
+
.cn-tabs-widget__stack--grid {
|
|
1054
|
+
display: grid;
|
|
1055
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
1056
|
+
align-items: stretch;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.cn-tabs-widget__stack--grid > * {
|
|
1060
|
+
min-width: 0;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
@media (max-width: 700px) {
|
|
1064
|
+
.cn-tabs-widget__stack--grid {
|
|
1065
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.cn-tabs-widget__stack--grid > * {
|
|
1069
|
+
grid-column: auto !important;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1002
1073
|
/* The panel is the card; the strip is not inside it. There is no title bar
|
|
1003
1074
|
above the tabs either: the open tab names the panel, so a title row would
|
|
1004
1075
|
say the same thing twice and cost a row of height on a card that is mostly
|
|
@@ -1119,6 +1190,14 @@ export default {
|
|
|
1119
1190
|
height: 100%;
|
|
1120
1191
|
}
|
|
1121
1192
|
|
|
1193
|
+
/* A list widget's footer Add bleeds through its card's 16px content padding
|
|
1194
|
+
(margin 0 -16px -16px). The panel of a tab pads 12px, so on a tab that
|
|
1195
|
+
holds the list directly the footer stuck out 4px on the right and at the
|
|
1196
|
+
bottom and made the panel scroll. It bleeds exactly the panel's padding. */
|
|
1197
|
+
.cn-tabs-widget__tabs :deep(.cn-tabs__content > .cn-tab > .cn-detail-widget-host--bare .cn-object-list-widget__add) {
|
|
1198
|
+
margin: auto -12px -12px;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1122
1201
|
.cn-tabs-widget__title {
|
|
1123
1202
|
align-items: center;
|
|
1124
1203
|
display: inline-flex;
|