@antelopejs/dms 0.14.0 → 0.15.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/CHANGELOG.md +22 -0
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/blockDataState.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexAxes.ts +22 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableViewCounts.ts +30 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableViewTabs.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +17 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +22 -2
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +14 -1
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +17 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +12 -10
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +11 -2
- package/frontend-vue/layers/dms-ui/app/entries/composables/blocks/blockDataState.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.15.0
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.14.0...v0.15.0)
|
|
6
|
+
|
|
7
|
+
### 🚀 Enhancements
|
|
8
|
+
|
|
9
|
+
- **dms-ui:** State the data state of each data block on its root element ([#585](https://github.com/AntelopeJS/dms/pull/585))
|
|
10
|
+
|
|
11
|
+
### 🩹 Fixes
|
|
12
|
+
|
|
13
|
+
- **table-view:** Currency on prices, settled counters, no add without a new route ([#611](https://github.com/AntelopeJS/dms/pull/611))
|
|
14
|
+
- **interface-dms:** Name a bucketed series' periods for a person, 202608 as Aug 2026 ([#617](https://github.com/AntelopeJS/dms/pull/617))
|
|
15
|
+
- **chart:** Thin the category x-axis labels of a long series so they never overlap ([#626](https://github.com/AntelopeJS/dms/pull/626))
|
|
16
|
+
|
|
17
|
+
### 🏡 Chore
|
|
18
|
+
|
|
19
|
+
- **release:** @antelopejs/interface-dms v0.10.3 ([53af1aad](https://github.com/AntelopeJS/dms/commit/53af1aad))
|
|
20
|
+
|
|
21
|
+
### ❤️ Contributors
|
|
22
|
+
|
|
23
|
+
- Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting))
|
|
24
|
+
|
|
3
25
|
## v0.14.0
|
|
4
26
|
|
|
5
27
|
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.13.1...v0.14.0)
|
|
@@ -271,6 +271,20 @@ export function formatNumber(
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
const CURRENCY_CODE = /^[A-Za-z]{3}$/;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The currency an amount is written in: `options.currency` when it is an
|
|
278
|
+
* ISO 4217 code, euros otherwise (a missing or malformed code never turns the
|
|
279
|
+
* amount into a bare number).
|
|
280
|
+
*/
|
|
281
|
+
function priceCurrency(options: Intl.NumberFormatOptions | undefined): string {
|
|
282
|
+
const code = options?.currency;
|
|
283
|
+
return typeof code === "string" && CURRENCY_CODE.test(code)
|
|
284
|
+
? code.toUpperCase()
|
|
285
|
+
: DEFAULT_CURRENCY;
|
|
286
|
+
}
|
|
287
|
+
|
|
274
288
|
/** An amount, in euros unless `options.currency` says otherwise. */
|
|
275
289
|
export function formatPrice(
|
|
276
290
|
value: number,
|
|
@@ -289,10 +303,10 @@ export function formatPrice(
|
|
|
289
303
|
): Parameters<typeof isNumber>[0] {
|
|
290
304
|
return formatNumber(value, locale, {
|
|
291
305
|
style: "currency",
|
|
292
|
-
currency: DEFAULT_CURRENCY,
|
|
293
306
|
minimumFractionDigits: DEFAULT_PRICE_FRACTION_DIGITS,
|
|
294
307
|
maximumFractionDigits: DEFAULT_PRICE_FRACTION_DIGITS,
|
|
295
308
|
...options,
|
|
309
|
+
currency: priceCurrency(options),
|
|
296
310
|
});
|
|
297
311
|
}
|
|
298
312
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
toValue,
|
|
5
|
+
type ComputedRef,
|
|
6
|
+
type InjectionKey,
|
|
7
|
+
type MaybeRefOrGetter,
|
|
8
|
+
type Ref,
|
|
9
|
+
} from "vue";
|
|
10
|
+
import type { ChartSeries } from "../../../composables/chart/types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What a data block shows its reader: still waiting for its first answer
|
|
14
|
+
* (`loading`), content (`ok`), an answer with nothing to draw (`empty`), or a
|
|
15
|
+
* request that failed with nothing to fall back on (`error`).
|
|
16
|
+
*/
|
|
17
|
+
export type BlockDataState = "loading" | "ok" | "empty" | "error";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The attributes a data block carries on its root element. An overlay or a
|
|
21
|
+
* test harness reads them from the DOM, so the names are a contract.
|
|
22
|
+
*/
|
|
23
|
+
export const BLOCK_ATTRIBUTES = {
|
|
24
|
+
/** The block's position in the page layout: its key, then each child id, joined by dots. */
|
|
25
|
+
path: "data-dms-block",
|
|
26
|
+
/** The block kind as the page author names it, for example `ChartCard`. */
|
|
27
|
+
type: "data-dms-block-type",
|
|
28
|
+
/** A {@link BlockDataState}. */
|
|
29
|
+
state: "data-dms-block-state",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/** The layout path of the block being rendered, provided by `RecursiveComponent`. */
|
|
33
|
+
export const BLOCK_PATH_KEY: InjectionKey<Ref<string | undefined>> =
|
|
34
|
+
Symbol("dms-block-path");
|
|
35
|
+
|
|
36
|
+
export interface BlockStateFacts {
|
|
37
|
+
/** An answer for the current inputs is still to come. */
|
|
38
|
+
loading: boolean;
|
|
39
|
+
/** The last request failed. */
|
|
40
|
+
failed: boolean;
|
|
41
|
+
/** The block has something to show its reader. */
|
|
42
|
+
hasContent: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The state of a block from three facts. Content wins over everything: a
|
|
47
|
+
* block refreshing or failing a refresh still shows what it had.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveBlockState(facts: BlockStateFacts): BlockDataState {
|
|
50
|
+
if (facts.hasContent) return "ok";
|
|
51
|
+
if (facts.loading) return "loading";
|
|
52
|
+
if (facts.failed) return "error";
|
|
53
|
+
return "empty";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDrawable(point: ChartSeries["data"][number]): boolean {
|
|
57
|
+
if (typeof point === "number") return Number.isFinite(point);
|
|
58
|
+
return point.y !== null && point.y !== undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether any series holds a point a chart can draw: not absent, not `null`. */
|
|
62
|
+
export function hasDrawablePoints(series: readonly ChartSeries[]): boolean {
|
|
63
|
+
return series.some((entry) => entry.data.some(isDrawable));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type BlockStateAttributes = {
|
|
67
|
+
[BLOCK_ATTRIBUTES.state]: BlockDataState;
|
|
68
|
+
[BLOCK_ATTRIBUTES.type]: string;
|
|
69
|
+
[BLOCK_ATTRIBUTES.path]?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The attributes to bind on a data block's root element (`v-bind`): its
|
|
74
|
+
* kind, its state, and its layout path when a `RecursiveComponent` renders it.
|
|
75
|
+
*/
|
|
76
|
+
export function useBlockStateAttributes(
|
|
77
|
+
type: string,
|
|
78
|
+
state: MaybeRefOrGetter<BlockDataState>,
|
|
79
|
+
): ComputedRef<BlockStateAttributes> {
|
|
80
|
+
const path = inject(BLOCK_PATH_KEY, null);
|
|
81
|
+
return computed(() => ({
|
|
82
|
+
[BLOCK_ATTRIBUTES.state]: toValue(state),
|
|
83
|
+
[BLOCK_ATTRIBUTES.type]: type,
|
|
84
|
+
[BLOCK_ATTRIBUTES.path]: path?.value,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
@@ -15,7 +15,15 @@ import {
|
|
|
15
15
|
const CROSSHAIR_DASH = 3;
|
|
16
16
|
const HIGH_CONTRAST_YAXIS_TYPES: ChartType[] = ["radar"];
|
|
17
17
|
const CATEGORY_YAXIS_TYPES: ChartType[] = ["heatmap"];
|
|
18
|
-
const SELF_THINNED_XAXIS_TYPES: ChartType[] = [
|
|
18
|
+
const SELF_THINNED_XAXIS_TYPES: ChartType[] = [
|
|
19
|
+
"heatmap",
|
|
20
|
+
"line",
|
|
21
|
+
"area",
|
|
22
|
+
"rangeArea",
|
|
23
|
+
"column",
|
|
24
|
+
"bar",
|
|
25
|
+
"mixed",
|
|
26
|
+
];
|
|
19
27
|
|
|
20
28
|
type YAxis = NonNullable<Exclude<ApexOptions["yaxis"], unknown[]>>;
|
|
21
29
|
type XAxis = NonNullable<ApexOptions["xaxis"]>;
|
|
@@ -42,11 +50,22 @@ export function buildYAxis(
|
|
|
42
50
|
return yaxis;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Whether the chart draws a category x-axis whose labels this module thins.
|
|
55
|
+
* A horizontal bar puts its categories on the other axis. `bucketed` marks a
|
|
56
|
+
* datetime series Apex draws as a category axis of bucket labels.
|
|
57
|
+
*/
|
|
58
|
+
export function thinsOwnXLabels(
|
|
59
|
+
input: UseApexChartInput,
|
|
60
|
+
bucketed = false,
|
|
61
|
+
): boolean {
|
|
46
62
|
const settings = input.xAxis;
|
|
63
|
+
const horizontalBar =
|
|
64
|
+
input.type === "bar" && input.orientation !== "vertical";
|
|
47
65
|
return (
|
|
48
66
|
SELF_THINNED_XAXIS_TYPES.includes(input.type) &&
|
|
49
|
-
|
|
67
|
+
!horizontalBar &&
|
|
68
|
+
(bucketed || (input.xaxisType ?? "category") === "category") &&
|
|
50
69
|
settings?.hideOverlappingLabels !== false &&
|
|
51
70
|
settings?.tickAmount === undefined &&
|
|
52
71
|
!settings?.rotate
|
|
@@ -83,15 +83,27 @@ export function useTableViewCountsData<T extends Data>(
|
|
|
83
83
|
const { liveHold } = scope.liveHold;
|
|
84
84
|
const { tableViewItems, canSaveViews } = scope.views;
|
|
85
85
|
const { props } = scope;
|
|
86
|
-
const {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
const {
|
|
87
|
+
data: tabCountsData,
|
|
88
|
+
status: tabCountsStatus,
|
|
89
|
+
refresh: refreshTabCounts,
|
|
90
|
+
} = useTableCountsData<T>({
|
|
91
|
+
componentId,
|
|
92
|
+
pageId,
|
|
93
|
+
rows: tableRows,
|
|
94
|
+
queries: tabCountsQuery,
|
|
95
|
+
archiveQuery,
|
|
96
|
+
heldQuery: liveHold.heldQuery,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The counters are still on their way: no answer yet, and no failure. A
|
|
101
|
+
* request that failed leaves `data` null as a pending one does, so it
|
|
102
|
+
* would draw the counters' skeleton for good.
|
|
103
|
+
*/
|
|
104
|
+
const areCountsPending = computed(
|
|
105
|
+
() => tabCountsData.value == null && tabCountsStatus.value !== "error",
|
|
106
|
+
);
|
|
95
107
|
|
|
96
108
|
const viewItems = computed<TableViewItem[]>(() =>
|
|
97
109
|
tableViewItems.value.map((view) => ({
|
|
@@ -104,13 +116,20 @@ export function useTableViewCountsData<T extends Data>(
|
|
|
104
116
|
count: view.count
|
|
105
117
|
? tabCountsData.value?.[`${VIEW_COUNT_ID_PREFIX}${view.id}`]
|
|
106
118
|
: undefined,
|
|
107
|
-
countPending: !!view.count &&
|
|
119
|
+
countPending: !!view.count && areCountsPending.value,
|
|
108
120
|
})),
|
|
109
121
|
);
|
|
110
122
|
const viewsLayout = props.views?.layout ?? "tabs";
|
|
111
123
|
const hasViews =
|
|
112
124
|
!!props.views && (props.views.items.length > 0 || canSaveViews);
|
|
113
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
refreshTabCounts,
|
|
127
|
+
tabCountsData,
|
|
128
|
+
areCountsPending,
|
|
129
|
+
viewItems,
|
|
130
|
+
viewsLayout,
|
|
131
|
+
hasViews,
|
|
132
|
+
};
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
export function useTableViewLinkCounts<T extends Data>(
|
|
@@ -89,7 +89,7 @@ export function useTableViewTabs<T extends Data>(
|
|
|
89
89
|
export function useTableViewTabPresentation<T extends Data>(
|
|
90
90
|
scope: Pick<TableViewScope<T>, "countsData" | "linkCounts" | "tabs">,
|
|
91
91
|
) {
|
|
92
|
-
const { tabCountsData } = scope.countsData;
|
|
92
|
+
const { tabCountsData, areCountsPending } = scope.countsData;
|
|
93
93
|
const { linkTabCountsLoaded, linkTabCounts } = scope.linkCounts;
|
|
94
94
|
const { resolvedTabs } = scope.tabs;
|
|
95
95
|
const permissionPreview = usePermissionPreview();
|
|
@@ -104,7 +104,7 @@ export function useTableViewTabPresentation<T extends Data>(
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
const isTabCountPending = (tab: ResolvedTab): boolean => {
|
|
107
|
-
if (!tab.to) return
|
|
107
|
+
if (!tab.to) return areCountsPending.value;
|
|
108
108
|
return !!tab.countFrom && !linkTabCountsLoaded.value;
|
|
109
109
|
};
|
|
110
110
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./grid/constants";
|
|
10
10
|
import DmsPermissionVeil from "../build/components/permission/PermissionVeil.vue";
|
|
11
11
|
import { registerBlockAnchor } from "../build/composables/page/blockAnchors";
|
|
12
|
+
import { BLOCK_PATH_KEY } from "../build/composables/blocks/blockDataState";
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
component: ResolvedComponentInfo;
|
|
@@ -40,6 +41,11 @@ function anchorBlock(instance: Element | ComponentPublicInstance | null): void {
|
|
|
40
41
|
anchored = block;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
provide(
|
|
45
|
+
BLOCK_PATH_KEY,
|
|
46
|
+
computed(() => props.layoutPath),
|
|
47
|
+
);
|
|
48
|
+
|
|
43
49
|
const { t } = useI18n();
|
|
44
50
|
const preview = usePermissionPreview();
|
|
45
51
|
const previewBlock = computed(() => preview.blockFor(props.layoutPath));
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { computed, inject, ref, watch } from "vue";
|
|
3
3
|
import DmsEmptyState from "../empty-state/EmptyState.vue";
|
|
4
4
|
import DmsChartSkeleton from "../../build/components/chart/internal/ChartSkeleton.vue";
|
|
5
|
+
import {
|
|
6
|
+
hasDrawablePoints,
|
|
7
|
+
resolveBlockState,
|
|
8
|
+
useBlockStateAttributes,
|
|
9
|
+
} from "../../build/composables/blocks/blockDataState";
|
|
5
10
|
import {
|
|
6
11
|
chartFrameHeight,
|
|
7
12
|
useApexChart,
|
|
@@ -148,7 +153,7 @@ const staticDonut = computed<DonutRecord[]>(() => {
|
|
|
148
153
|
|
|
149
154
|
const isNested = computed(() => !!nestedContext);
|
|
150
155
|
|
|
151
|
-
const { data, isLoading } = useChartFetch<ChartResponse>({
|
|
156
|
+
const { data, isLoading, error } = useChartFetch<ChartResponse>({
|
|
152
157
|
fetchUrl: isNested.value ? undefined : props.fetchUrl,
|
|
153
158
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
154
159
|
routeParams: () => props.routeParams,
|
|
@@ -318,6 +323,16 @@ const isAwaitingData = computed(() =>
|
|
|
318
323
|
|
|
319
324
|
const isRefreshing = computed(() => hasData.value && isAwaitingData.value);
|
|
320
325
|
|
|
326
|
+
const blockAttributes = useBlockStateAttributes("Chart", () =>
|
|
327
|
+
resolveBlockState({
|
|
328
|
+
loading: isAwaitingData.value,
|
|
329
|
+
failed: !nestedContext && Boolean(error.value),
|
|
330
|
+
hasContent: isCircular.value
|
|
331
|
+
? finalDonutData.value.some((record) => record.value !== 0)
|
|
332
|
+
: hasDrawablePoints(finalSeries.value),
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
|
|
321
336
|
const isDrawn = ref(false);
|
|
322
337
|
watch(hasData, (present) => {
|
|
323
338
|
if (!present) isDrawn.value = false;
|
|
@@ -328,6 +343,7 @@ watch(hasData, (present) => {
|
|
|
328
343
|
<!-- Standalone charts get the card frame; charts nested inside a
|
|
329
344
|
DmsChartCard inherit its frame instead. -->
|
|
330
345
|
<div
|
|
346
|
+
v-bind="blockAttributes"
|
|
331
347
|
class="dms-chart"
|
|
332
348
|
:class="nestedContext ? undefined : 'dms-card p-3.5 sm:pr-[18px]'"
|
|
333
349
|
>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, provide, reactive } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
hasDrawablePoints,
|
|
5
|
+
resolveBlockState,
|
|
6
|
+
useBlockStateAttributes,
|
|
7
|
+
} from "../../build/composables/blocks/blockDataState";
|
|
3
8
|
import DmsEyebrow from "../section-header/Eyebrow.vue";
|
|
4
9
|
|
|
5
10
|
import DmsTrendBadge from "./internal/TrendBadge.vue";
|
|
@@ -71,7 +76,7 @@ const { state: watchState } = useWatch(
|
|
|
71
76
|
);
|
|
72
77
|
const watchKey = computed(() => JSON.stringify(watchState.value));
|
|
73
78
|
|
|
74
|
-
const { data, isLoading } = useChartFetch<ChartCardResponse>({
|
|
79
|
+
const { data, isLoading, error } = useChartFetch<ChartCardResponse>({
|
|
75
80
|
fetchUrl: props.fetchUrl,
|
|
76
81
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
77
82
|
routeParams: () => props.routeParams,
|
|
@@ -159,6 +164,17 @@ const comparisonLegendLabel = computed(() =>
|
|
|
159
164
|
t(DEFAULT_COMPARISON_LABEL_KEY, "Comparison")),
|
|
160
165
|
);
|
|
161
166
|
|
|
167
|
+
const blockAttributes = useBlockStateAttributes("ChartCard", () =>
|
|
168
|
+
resolveBlockState({
|
|
169
|
+
loading: isLoading.value,
|
|
170
|
+
failed: Boolean(error.value),
|
|
171
|
+
hasContent:
|
|
172
|
+
value.value !== undefined ||
|
|
173
|
+
hasDrawablePoints(cardSeries.value) ||
|
|
174
|
+
hasDrawablePoints(cardComparisonSeries.value),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
162
178
|
const isFirstLoad = computed(() => isLoading.value && data.value === null);
|
|
163
179
|
|
|
164
180
|
const isRefreshing = computed(() => isLoading.value && data.value !== null);
|
|
@@ -170,7 +186,11 @@ const hasMeta = computed(
|
|
|
170
186
|
</script>
|
|
171
187
|
|
|
172
188
|
<template>
|
|
173
|
-
<DmsCard
|
|
189
|
+
<DmsCard
|
|
190
|
+
v-bind="blockAttributes"
|
|
191
|
+
:padded="false"
|
|
192
|
+
class="flex min-w-0 flex-col"
|
|
193
|
+
>
|
|
174
194
|
<div class="flex flex-wrap items-start gap-3 px-4 pt-4 pb-1.5 sm:px-[18px]">
|
|
175
195
|
<div class="grid min-w-0 justify-items-start">
|
|
176
196
|
<DmsEyebrow
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
resolveBlockState,
|
|
5
|
+
useBlockStateAttributes,
|
|
6
|
+
} from "../../build/composables/blocks/blockDataState";
|
|
3
7
|
import DmsStatCell from "../../build/components/stat/StatCell.vue";
|
|
4
8
|
import DmsEyebrow from "../section-header/Eyebrow.vue";
|
|
5
9
|
|
|
@@ -129,6 +133,14 @@ const previousText = computed(() => {
|
|
|
129
133
|
return t("dms.chart.versus_value", { value: formatted });
|
|
130
134
|
});
|
|
131
135
|
|
|
136
|
+
const blockAttributes = useBlockStateAttributes("KpiCard", () =>
|
|
137
|
+
resolveBlockState({
|
|
138
|
+
loading: isLoading.value,
|
|
139
|
+
failed: Boolean(error.value),
|
|
140
|
+
hasContent: value.value !== undefined,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
132
144
|
const isFirstLoad = computed(() => isLoading.value && data.value === null);
|
|
133
145
|
|
|
134
146
|
const hasError = computed(
|
|
@@ -181,6 +193,7 @@ const isStat = computed(() => props.variant === "stat");
|
|
|
181
193
|
<template>
|
|
182
194
|
<DmsStatCell
|
|
183
195
|
v-if="isStat"
|
|
196
|
+
v-bind="blockAttributes"
|
|
184
197
|
:eyebrow="processI18n(title)"
|
|
185
198
|
:icon="resolvedIcon"
|
|
186
199
|
:loading="isFirstLoad"
|
|
@@ -245,6 +258,7 @@ const isStat = computed(() => props.variant === "stat");
|
|
|
245
258
|
|
|
246
259
|
<DmsCard
|
|
247
260
|
v-else
|
|
261
|
+
v-bind="blockAttributes"
|
|
248
262
|
:padded="false"
|
|
249
263
|
class="flex min-h-[132px] flex-col gap-1.5 px-4 py-4 sm:px-[18px]"
|
|
250
264
|
:aria-busy="isFirstLoad"
|
|
@@ -18,6 +18,10 @@ import ExpandedRowDetail from "../../build/components/table-view/ExpandedRowDeta
|
|
|
18
18
|
import LiveHoldBanner from "../../build/components/table-view/LiveHoldBanner.vue";
|
|
19
19
|
import TableViews from "../../build/components/table/Views.vue";
|
|
20
20
|
import { useTableViewSetup } from "../../build/composables/table-view/tableViewSetup";
|
|
21
|
+
import {
|
|
22
|
+
resolveBlockState,
|
|
23
|
+
useBlockStateAttributes,
|
|
24
|
+
} from "../../build/composables/blocks/blockDataState";
|
|
21
25
|
|
|
22
26
|
interface TableViewProps<T extends Data> extends TableViewConfig<T> {
|
|
23
27
|
/** Expandable rows: caret column + detail band (backend `expandable`). */
|
|
@@ -143,18 +147,27 @@ const {
|
|
|
143
147
|
handleRowEdit,
|
|
144
148
|
contextForSlot,
|
|
145
149
|
} = useTableViewSetup<T>(props);
|
|
150
|
+
|
|
151
|
+
const blockAttributes = useBlockStateAttributes("TableView", () =>
|
|
152
|
+
resolveBlockState({
|
|
153
|
+
loading: isListLoading.value,
|
|
154
|
+
failed: Boolean(listLoadError.value || listNotFound.value),
|
|
155
|
+
hasContent: (shownResults.value?.length ?? 0) > 0,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
146
158
|
</script>
|
|
147
159
|
|
|
148
160
|
<template>
|
|
149
161
|
<DmsBlockStatus
|
|
150
162
|
v-if="listNotFound"
|
|
163
|
+
v-bind="blockAttributes"
|
|
151
164
|
state="not-found"
|
|
152
165
|
:not-found="listNotFound"
|
|
153
166
|
/>
|
|
154
167
|
<UTable
|
|
155
168
|
v-else
|
|
156
169
|
ref="tableRoot"
|
|
157
|
-
v-bind="tableProps"
|
|
170
|
+
v-bind="{ ...tableProps, ...blockAttributes }"
|
|
158
171
|
v-model:pagination="paginationState"
|
|
159
172
|
v-model:global-filter="globalFilter"
|
|
160
173
|
v-model:sorting="sorting"
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
resolveBlockState,
|
|
5
|
+
useBlockStateAttributes,
|
|
6
|
+
} from "../../build/composables/blocks/blockDataState";
|
|
3
7
|
import DmsEmptyState from "../empty-state/EmptyState.vue";
|
|
4
8
|
import DmsSectionHeader from "../section-header/SectionHeader.vue";
|
|
5
9
|
import DmsBlockActions, {
|
|
@@ -219,6 +223,14 @@ const badgeLabel = computed<string | null>(() => {
|
|
|
219
223
|
const comparison = usePeriodComparisonDisplay(props.periodScope);
|
|
220
224
|
const showRowDelta = computed(() => props.showDelta && !comparison.isOff.value);
|
|
221
225
|
|
|
226
|
+
const blockAttributes = useBlockStateAttributes("TopListCard", () =>
|
|
227
|
+
resolveBlockState({
|
|
228
|
+
loading: isLoading.value,
|
|
229
|
+
failed: Boolean(error.value),
|
|
230
|
+
hasContent: items.value.length > 0,
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
222
234
|
const isFirstLoad = computed(() => isLoading.value && data.value === null);
|
|
223
235
|
|
|
224
236
|
const isRefreshing = computed(() => isLoading.value && data.value !== null);
|
|
@@ -268,7 +280,11 @@ const gridStyle = computed(() => ({
|
|
|
268
280
|
</script>
|
|
269
281
|
|
|
270
282
|
<template>
|
|
271
|
-
<DmsCard
|
|
283
|
+
<DmsCard
|
|
284
|
+
v-bind="blockAttributes"
|
|
285
|
+
:padded="false"
|
|
286
|
+
class="flex min-w-0 flex-col overflow-hidden"
|
|
287
|
+
>
|
|
272
288
|
<DmsSectionHeader
|
|
273
289
|
size="card"
|
|
274
290
|
class="border-default border-b py-3.5 pr-4 pl-[18px]"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isNumber } from "../../../../dms-core/app/utils/type-check";
|
|
2
|
-
import type { ChartXAxisFormatter } from "./types";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Width of one character of the axis font (11px, monospace) in pixels, with a
|
|
@@ -38,19 +37,22 @@ export function categoryLabelStep(
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
|
-
* A
|
|
42
|
-
* (`hideOverlappingLabels` only acts on
|
|
43
|
-
* into each other ("
|
|
44
|
-
*
|
|
45
|
-
* whole.
|
|
40
|
+
* A category x-axis gives every point a label and ApexCharts does not thin
|
|
41
|
+
* them (`hideOverlappingLabels` only acts on numeric and datetime axes), so a
|
|
42
|
+
* long series or a narrow plot runs them into each other ("Aug 4, 2026Aug 8,
|
|
43
|
+
* 2026"). This formatter keeps one label every few columns, from the plot's
|
|
44
|
+
* width, and leaves the tooltip's (no column index) whole.
|
|
46
45
|
*/
|
|
47
46
|
export function thinnedCategoryFormatter(
|
|
48
|
-
format:
|
|
47
|
+
format: ApexLabelFormatter | undefined,
|
|
49
48
|
): ApexLabelFormatter {
|
|
50
|
-
const write = (
|
|
51
|
-
|
|
49
|
+
const write = (
|
|
50
|
+
value: string,
|
|
51
|
+
timestamp?: number,
|
|
52
|
+
opts?: ApexLabelFormatterOpts,
|
|
53
|
+
): string => (format ? format(value, timestamp, opts) : String(value ?? ""));
|
|
52
54
|
return (value, timestamp, opts) => {
|
|
53
|
-
const label = write(value, timestamp);
|
|
55
|
+
const label = write(value, timestamp, opts);
|
|
54
56
|
const index = opts?.i;
|
|
55
57
|
const globals = opts?.w?.globals;
|
|
56
58
|
if (!isNumber(index) || !globals) return label;
|
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
import { APEX_TYPE_CONFIGS } from "./apexTypeConfigs";
|
|
17
17
|
import { buildAnnotations } from "./apexAnnotations";
|
|
18
18
|
import { buildPlotOptions, ringFitsSliceLabels } from "./apexPlotOptions";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
buildXAxis,
|
|
21
|
+
buildYAxis,
|
|
22
|
+
thinsOwnXLabels,
|
|
23
|
+
} from "../../build/composables/chart/apexAxes";
|
|
24
|
+
import { thinnedCategoryFormatter } from "./apexCategoryLabels";
|
|
20
25
|
import {
|
|
21
26
|
applyHeatmapThemeMode,
|
|
22
27
|
applyValueBasedColorScale,
|
|
@@ -271,10 +276,14 @@ function applyDatetimeBuckets(
|
|
|
271
276
|
if (!buckets) return;
|
|
272
277
|
const xaxis = options.xaxis;
|
|
273
278
|
const { format: _datetimeFormat, ...labels } = xaxis.labels;
|
|
279
|
+
const bucketLabels = bucketAxisFormatter(buckets, input);
|
|
280
|
+
const bucketFormatter = thinsOwnXLabels(input, true)
|
|
281
|
+
? thinnedCategoryFormatter(bucketLabels)
|
|
282
|
+
: bucketLabels;
|
|
274
283
|
options.xaxis = {
|
|
275
284
|
...xaxis,
|
|
276
285
|
type: "category",
|
|
277
|
-
labels: { ...labels, formatter:
|
|
286
|
+
labels: { ...labels, formatter: bucketFormatter },
|
|
278
287
|
};
|
|
279
288
|
const tooltip = options.tooltip;
|
|
280
289
|
options.tooltip = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../../build/composables/blocks/blockDataState";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "AntelopeJS DMS. Plug in your PostgreSQL, MongoDB or RethinkDB database and your coding agent builds a back office and analytics dashboard as typed TypeScript. Real-time updates on the data the DMS manages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|