@antelopejs/dms 0.7.20 → 0.8.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 +62 -0
- package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
- package/dist/implementations/dms-auth/account-emails.js +62 -0
- package/dist/implementations/dms-auth/account-emails.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +1 -3
- package/dist/implementations/dms-auth/index.js +15 -61
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/pages/settings/users/email-change.js +3 -4
- package/dist/pages/settings/users/email-change.js.map +1 -1
- package/dist/pages/settings/users/invites.js +1 -1
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.js +1 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -0
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js +27 -0
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +18 -4
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
- package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
- package/dist/test/unit/utils/email-subjects.test.js +27 -0
- package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
- package/dist/utils/admin-invite-email.js +2 -4
- package/dist/utils/admin-invite-email.js.map +1 -1
- package/dist/utils/email-change-mail.d.ts +1 -1
- package/dist/utils/email-change-mail.js +11 -6
- package/dist/utils/email-change-mail.js.map +1 -1
- package/dist/utils/email-subjects.d.ts +3 -0
- package/dist/utils/email-subjects.js +32 -0
- package/dist/utils/email-subjects.js.map +1 -0
- package/dist/utils/export-jobs.js +15 -10
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
- package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
- package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +14 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +90 -43
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableUrlTab.ts +42 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +10 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
- package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +14 -6
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
- package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
- package/frontend-vue/tests/builtin-emails.test.ts +141 -0
- package/frontend-vue/tests/cascader.test.ts +27 -0
- package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
- package/frontend-vue/tests/chart-colors.test.ts +8 -0
- package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
- package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
- package/frontend-vue/tests/check-list-marker.test.ts +58 -0
- package/frontend-vue/tests/confirm-from.test.ts +1 -1
- package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
- package/frontend-vue/tests/context-links.test.ts +2 -2
- package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
- package/frontend-vue/tests/download-action-target.test.ts +2 -2
- package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
- package/frontend-vue/tests/footer-config.test.ts +135 -0
- package/frontend-vue/tests/form-actions.test.ts +7 -0
- package/frontend-vue/tests/form-entry-order.test.ts +45 -0
- package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
- package/frontend-vue/tests/form-load-error.test.ts +38 -1
- package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
- package/frontend-vue/tests/header-actions.test.ts +55 -9
- package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
- package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
- package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
- package/frontend-vue/tests/row-side-panel.test.ts +93 -2
- package/frontend-vue/tests/save-bar.test.ts +15 -3
- package/frontend-vue/tests/save-status-error.test.ts +2 -2
- package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
- package/frontend-vue/tests/table-row-hover.test.ts +2 -2
- package/frontend-vue/tests/table-view-bulk-drawer.test.ts +114 -0
- package/frontend-vue/tests/table-view-cards-reread.test.ts +77 -0
- package/frontend-vue/tests/table-view-sources.test.ts +1 -1
- package/frontend-vue/tests/table-view-url-tab.test.ts +84 -2
- package/frontend-vue/tests/template-tokens.test.ts +235 -0
- package/frontend-vue/tests/url-interpolation.test.ts +6 -6
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-pages/REFERENCE.md +2 -2
- package/skills/dms-project/SKILL.md +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import type { SelectMenuItem } from "@nuxt/ui";
|
|
3
4
|
import { refDebounced } from "@vueuse/core";
|
|
4
5
|
import {
|
|
@@ -225,7 +226,9 @@ interface CountryOptions {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
async function fetchCountryOptions(): Promise<CountryOptions> {
|
|
228
|
-
const
|
|
229
|
+
const url = pageRequestUrl(props.searchUrl);
|
|
230
|
+
if (!url) return { results: [], total: 0 };
|
|
231
|
+
const data = await $authFetch<CountryApiResponse>(url, {
|
|
229
232
|
params: {
|
|
230
233
|
q: countrySearchTermDebounced.value,
|
|
231
234
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { getCurrentInstance, h, render } from "vue";
|
|
3
4
|
import { useFormField } from "@nuxt/ui/composables/useFormField";
|
|
4
5
|
import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
@@ -84,10 +85,11 @@ function checkSyntax(text: string): void {
|
|
|
84
85
|
|
|
85
86
|
async function loadCompletions(): Promise<CodeEditorCompletion[]> {
|
|
86
87
|
const fixed = props.completions ?? [];
|
|
87
|
-
|
|
88
|
+
const url = pageRequestUrl(props.completionsUrl);
|
|
89
|
+
if (!url) return fixed;
|
|
88
90
|
const response = await $authFetch<
|
|
89
91
|
CodeEditorCompletion[] | CompletionsResponse
|
|
90
|
-
>(
|
|
92
|
+
>(url).catch(() => []);
|
|
91
93
|
const fetched = Array.isArray(response) ? response : (response.items ?? []);
|
|
92
94
|
return [...fixed, ...fetched];
|
|
93
95
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { useConfirm } from "../../../composables/confirm/useConfirm";
|
|
3
4
|
import DmsCopyButton from "../../copy/CopyButton.vue";
|
|
4
5
|
|
|
@@ -43,7 +44,7 @@ const masked = computed(() => {
|
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
async function rotate(): Promise<void> {
|
|
46
|
-
const url = props.rotateUrl;
|
|
47
|
+
const url = pageRequestUrl(props.rotateUrl);
|
|
47
48
|
if (!url) return;
|
|
48
49
|
const confirmed = await confirm({
|
|
49
50
|
title: t("dms.form.secret.rotate_title"),
|
|
@@ -95,7 +96,7 @@ async function rotate(): Promise<void> {
|
|
|
95
96
|
</UInput>
|
|
96
97
|
<DmsCopyButton v-if="props.copyable && value" :value="value" />
|
|
97
98
|
<UButton
|
|
98
|
-
v-if="props.rotateUrl"
|
|
99
|
+
v-if="pageRequestUrl(props.rotateUrl)"
|
|
99
100
|
icon="i-ph-arrows-clockwise"
|
|
100
101
|
:label="t('dms.form.secret.rotate')"
|
|
101
102
|
color="neutral"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
lang="ts"
|
|
4
4
|
generic="T extends ArrayOrNested<SelectMenuItem>, M extends boolean = false"
|
|
5
5
|
>
|
|
6
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
6
7
|
import { useForwardPropsEmits } from "reka-ui";
|
|
7
8
|
import type {
|
|
8
9
|
SelectMenuProps,
|
|
@@ -193,7 +194,9 @@ interface RelationItems {
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
async function fetchRelationItems(): Promise<RelationItems> {
|
|
196
|
-
const
|
|
197
|
+
const url = pageRequestUrl(props.searchUrl);
|
|
198
|
+
if (!url) return { results: [], total: 0 };
|
|
199
|
+
const data = await $authFetch<RelationResponse>(url, {
|
|
197
200
|
params: {
|
|
198
201
|
search: searchTermDebounced.value || undefined,
|
|
199
202
|
},
|
|
@@ -5,6 +5,18 @@ const SINGLE_COLUMN = 1;
|
|
|
5
5
|
/** Track floor below which a column stops being usable, as a CSS length. */
|
|
6
6
|
export const GRID_DEFAULT_MIN_COLUMN_WIDTH = "240px";
|
|
7
7
|
|
|
8
|
+
const UNITLESS_NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A length usable inside `calc()`. A bare `0` is a valid `gap` but not a
|
|
12
|
+
* length in a calculation, where it would void the whole template and leave
|
|
13
|
+
* the grid on a single column; a unitless number reads as pixels.
|
|
14
|
+
*/
|
|
15
|
+
function calcLength(value: string): string {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return UNITLESS_NUMBER.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* The column track template a `Grid` and every `GridRow` inside it share.
|
|
10
22
|
*
|
|
@@ -26,8 +38,8 @@ export function gridColumnsTemplate(
|
|
|
26
38
|
minColumnWidth: string,
|
|
27
39
|
): string {
|
|
28
40
|
const columns = Math.max(maxColumns, SINGLE_COLUMN);
|
|
29
|
-
const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${gap}) / ${columns}`;
|
|
30
|
-
return `repeat(auto-fill, minmax(min(100%, max(${minColumnWidth}, ${columnShare})), 1fr))`;
|
|
41
|
+
const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${calcLength(gap)}) / ${columns}`;
|
|
42
|
+
return `repeat(auto-fill, minmax(min(100%, max(${calcLength(minColumnWidth)}, ${columnShare})), 1fr))`;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
/** Custom property a `Grid` / `GridRow` hands its cells: the tracks left. */
|
|
@@ -43,7 +55,7 @@ function trackBreakpoint(
|
|
|
43
55
|
gap: string,
|
|
44
56
|
minColumnWidth: string,
|
|
45
57
|
): string {
|
|
46
|
-
return `calc(${columns} * ${minColumnWidth} + ${columns - SINGLE_COLUMN} * ${gap})`;
|
|
58
|
+
return `calc(${columns} * ${calcLength(minColumnWidth)} + ${columns - SINGLE_COLUMN} * ${calcLength(gap)})`;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/** A container name / attribute value built from any id. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { computed } from "vue";
|
|
3
4
|
import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
|
|
4
5
|
import DmsStatusPill from "../status-pill/StatusPill.vue";
|
|
@@ -216,7 +217,7 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
216
217
|
<template v-else>
|
|
217
218
|
<component
|
|
218
219
|
:is="item.to ? DmsAutoLink : 'span'"
|
|
219
|
-
:to="item.to"
|
|
220
|
+
:to="pageLink(item.to)"
|
|
220
221
|
class="min-w-0"
|
|
221
222
|
:class="item.to && item.type !== 'status' && 'hover:underline'"
|
|
222
223
|
>
|
|
@@ -125,6 +125,7 @@ const theme = tv({
|
|
|
125
125
|
dot: "size-2 rounded-full bg-(--dms-accent-fill) shadow-[0_0_0_3px_var(--dms-accent-tint)]",
|
|
126
126
|
body: "min-w-0 flex-1",
|
|
127
127
|
title: "flex items-center gap-2",
|
|
128
|
+
titleText: "",
|
|
128
129
|
description: "text-muted mt-0.5 text-[12.5px]",
|
|
129
130
|
// The "·" closes each entry followed by another (rather than opening it),
|
|
130
131
|
// so a wrapped line never starts with a separator.
|
|
@@ -188,7 +189,11 @@ const theme = tv({
|
|
|
188
189
|
// end, so the title and meta wrap there instead.
|
|
189
190
|
truncate: {
|
|
190
191
|
true: {
|
|
191
|
-
title:
|
|
192
|
+
title:
|
|
193
|
+
"truncate flex-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
|
|
194
|
+
// The text box, not the flex title, carries the ellipsis: a flex
|
|
195
|
+
// container only clips its overflowing children.
|
|
196
|
+
titleText: "truncate max-sm:whitespace-normal",
|
|
192
197
|
meta: "flex-nowrap overflow-hidden whitespace-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
|
|
193
198
|
detail: "truncate max-sm:whitespace-normal",
|
|
194
199
|
},
|
|
@@ -281,7 +286,9 @@ const unreadText = computed(
|
|
|
281
286
|
:class="ui.title()"
|
|
282
287
|
:title="props.truncate ? props.title : undefined"
|
|
283
288
|
>
|
|
284
|
-
<slot>
|
|
289
|
+
<slot>
|
|
290
|
+
<span :class="ui.titleText()">{{ props.title }}</span>
|
|
291
|
+
</slot>
|
|
285
292
|
</div>
|
|
286
293
|
<p v-if="hasDescription" :class="ui.description()">
|
|
287
294
|
<slot name="description">{{ props.description }}</slot>
|
|
@@ -3,10 +3,11 @@ import type { SaveStatusState } from "./SaveStatus.vue";
|
|
|
3
3
|
|
|
4
4
|
interface InstantSaveBadgeProps {
|
|
5
5
|
/**
|
|
6
|
-
* Save activity of the page, for a subtle "just saved" flash: the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Save activity of the page, for a subtle "just saved" flash: the pill
|
|
7
|
+
* reads "Saving" with a spinning icon while a change is being written,
|
|
8
|
+
* then "Saved instantly" with a tick once it is, and turns red with "Not
|
|
9
|
+
* saved" while a save failed (the control says which). Defaults to `idle`
|
|
10
|
+
* (a lightning bolt and "Saved instantly").
|
|
10
11
|
*/
|
|
11
12
|
state?: SaveStatusState;
|
|
12
13
|
}
|
|
@@ -23,6 +24,13 @@ const ICONS: Record<SaveStatusState, string> = {
|
|
|
23
24
|
error: "i-ph-warning-circle",
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
const LABELS: Record<SaveStatusState, string> = {
|
|
28
|
+
idle: "dms.save_bar.instant",
|
|
29
|
+
saving: "dms.save_bar.saving",
|
|
30
|
+
saved: "dms.save_bar.instant",
|
|
31
|
+
error: "dms.save_bar.not_saved",
|
|
32
|
+
};
|
|
33
|
+
|
|
26
34
|
const TONE_CLASSES = {
|
|
27
35
|
ok: "border-success/40 bg-success/10 text-success",
|
|
28
36
|
error: "border-error/40 bg-error/10 text-error",
|
|
@@ -49,6 +57,6 @@ const TONE_CLASSES = {
|
|
|
49
57
|
class="size-3.5 shrink-0"
|
|
50
58
|
:class="{ 'animate-spin': props.state === 'saving' }"
|
|
51
59
|
/>
|
|
52
|
-
{{ t(
|
|
60
|
+
{{ t(LABELS[props.state]) }}
|
|
53
61
|
</span>
|
|
54
62
|
</template>
|
|
@@ -40,7 +40,7 @@ interface SaveBarProps {
|
|
|
40
40
|
kind?: FormKind;
|
|
41
41
|
/**
|
|
42
42
|
* Whether anything in the form can be changed: an `action` form nothing in
|
|
43
|
-
* which can be changed
|
|
43
|
+
* which can be changed never offers Reset.
|
|
44
44
|
*/
|
|
45
45
|
resettable?: boolean;
|
|
46
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { computed } from "vue";
|
|
3
4
|
import type { IconWellTone } from "../icon-well/IconWell.vue";
|
|
4
5
|
import DmsStatCell from "../../build/components/stat/StatCell.vue";
|
|
@@ -146,7 +147,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
|
|
|
146
147
|
:icon="item.icon"
|
|
147
148
|
:detail="item.detail"
|
|
148
149
|
:detail-tone="item.detailTone"
|
|
149
|
-
:to="item.to"
|
|
150
|
+
:to="pageLink(item.to)"
|
|
150
151
|
size="sm"
|
|
151
152
|
:tone="wellTone(item)"
|
|
152
153
|
:loading="props.loading"
|
|
@@ -175,7 +176,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
|
|
|
175
176
|
:icon="item.icon"
|
|
176
177
|
:detail="item.detail"
|
|
177
178
|
:detail-tone="item.detailTone"
|
|
178
|
-
:to="item.to"
|
|
179
|
+
:to="pageLink(item.to)"
|
|
179
180
|
:tone="wellTone(item)"
|
|
180
181
|
:loading="props.loading"
|
|
181
182
|
stack
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
-
buildCascaderPathLabels,
|
|
4
3
|
CASCADER_FETCH_LIMIT,
|
|
5
4
|
CASCADER_PATH_SEPARATOR,
|
|
5
|
+
cascaderValuePath,
|
|
6
6
|
resolveCascaderKeyMapping,
|
|
7
7
|
type CascaderKeyMapping,
|
|
8
8
|
} from "../../utils/cascader";
|
|
9
|
+
import { MAX_RELATION_BADGES } from "../../build/composables/data-types/relationBadges";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_VALUE_KEY = "_id";
|
|
11
12
|
|
|
12
13
|
interface CascaderPathProps {
|
|
14
|
+
/** One picked value, or the list a multiple cascader holds. */
|
|
13
15
|
value: unknown;
|
|
14
16
|
searchUrl?: string;
|
|
15
17
|
keyMapping?: Partial<CascaderKeyMapping>;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** A picked value of a multiple cascader: its name, its path on hover. */
|
|
21
|
+
interface CascaderBadge {
|
|
22
|
+
label: string;
|
|
23
|
+
title: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
const props = defineProps<CascaderPathProps>();
|
|
19
27
|
|
|
20
28
|
const { $authFetch } = useAuthFetch();
|
|
@@ -31,22 +39,6 @@ const keyMapping = computed(() =>
|
|
|
31
39
|
}),
|
|
32
40
|
);
|
|
33
41
|
|
|
34
|
-
const valueObject = computed(() =>
|
|
35
|
-
isObject(props.value) ? (props.value as Record<string, unknown>) : undefined,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const valueId = computed(() => {
|
|
39
|
-
const raw = valueObject.value
|
|
40
|
-
? valueObject.value[keyMapping.value.value]
|
|
41
|
-
: props.value;
|
|
42
|
-
return raw === undefined || raw === null ? undefined : String(raw);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const ownLabel = computed(() => {
|
|
46
|
-
const label = valueObject.value?.[keyMapping.value.label];
|
|
47
|
-
return label === undefined || label === null ? undefined : String(label);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
42
|
const { data } = await useDmsAsyncData(
|
|
51
43
|
`cascader-path-${props.searchUrl}`,
|
|
52
44
|
() =>
|
|
@@ -58,21 +50,51 @@ const { data } = await useDmsAsyncData(
|
|
|
58
50
|
{ lazy: true },
|
|
59
51
|
);
|
|
60
52
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
53
|
+
const pathOf = (item: unknown): string[] =>
|
|
54
|
+
cascaderValuePath(item, keyMapping.value, data.value?.results);
|
|
55
|
+
|
|
56
|
+
const pathLabel = computed(() =>
|
|
57
|
+
pathOf(props.value).join(CASCADER_PATH_SEPARATOR),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const badges = computed<CascaderBadge[]>(() =>
|
|
61
|
+
(Array.isArray(props.value) ? props.value : [])
|
|
62
|
+
.map(pathOf)
|
|
63
|
+
.filter((path) => path.length > 0)
|
|
64
|
+
.map((path) => ({
|
|
65
|
+
label: path.at(-1) ?? "",
|
|
66
|
+
title: path.join(CASCADER_PATH_SEPARATOR),
|
|
67
|
+
})),
|
|
68
|
+
);
|
|
69
|
+
const visibleBadges = computed(() =>
|
|
70
|
+
badges.value.slice(0, MAX_RELATION_BADGES),
|
|
71
|
+
);
|
|
72
|
+
const hiddenBadges = computed(() => badges.value.slice(MAX_RELATION_BADGES));
|
|
74
73
|
</script>
|
|
75
74
|
|
|
76
75
|
<template>
|
|
77
|
-
<span
|
|
76
|
+
<span
|
|
77
|
+
v-if="Array.isArray(props.value)"
|
|
78
|
+
class="inline-flex flex-wrap items-center gap-1"
|
|
79
|
+
>
|
|
80
|
+
<UBadge
|
|
81
|
+
v-for="badge in visibleBadges"
|
|
82
|
+
:key="badge.title"
|
|
83
|
+
:label="badge.label"
|
|
84
|
+
:title="badge.title"
|
|
85
|
+
color="neutral"
|
|
86
|
+
variant="subtle"
|
|
87
|
+
size="sm"
|
|
88
|
+
class="max-w-40"
|
|
89
|
+
/>
|
|
90
|
+
<UBadge
|
|
91
|
+
v-if="hiddenBadges.length"
|
|
92
|
+
:label="`+${hiddenBadges.length}`"
|
|
93
|
+
:title="hiddenBadges.map((badge) => badge.title).join(', ')"
|
|
94
|
+
color="neutral"
|
|
95
|
+
variant="subtle"
|
|
96
|
+
size="sm"
|
|
97
|
+
/>
|
|
98
|
+
</span>
|
|
99
|
+
<span v-else class="truncate" :title="pathLabel">{{ pathLabel }}</span>
|
|
78
100
|
</template>
|
|
@@ -267,6 +267,11 @@ const kanbanGroupByOptions = kanbanEligibleColumns.map((column) => ({
|
|
|
267
267
|
const { displays: registeredDisplays, getById } = useTableViewDisplays();
|
|
268
268
|
|
|
269
269
|
const offeredDisplayIds = new Set(resolvedDisplays.map((d) => d.id));
|
|
270
|
+
// Where a stale or unavailable choice lands: the grid, unless the table view
|
|
271
|
+
// drops it, then its default display.
|
|
272
|
+
const fallbackDisplayId = offeredDisplayIds.has(TABLE_DISPLAY_ID)
|
|
273
|
+
? TABLE_DISPLAY_ID
|
|
274
|
+
: defaultDisplay;
|
|
270
275
|
const resolvedDisplay = (id: string): TableViewDisplayConfig | undefined =>
|
|
271
276
|
resolvedDisplays.find((d) => d.id === id);
|
|
272
277
|
const optionsForDisplay = (id: string): Record<string, unknown> | undefined =>
|
|
@@ -293,7 +298,7 @@ const activeDisplayId = ref<string>(
|
|
|
293
298
|
? defaultDisplay
|
|
294
299
|
: offeredDisplayIds.has(persistedDisplay)
|
|
295
300
|
? persistedDisplay
|
|
296
|
-
:
|
|
301
|
+
: fallbackDisplayId,
|
|
297
302
|
);
|
|
298
303
|
|
|
299
304
|
/** A display the backend config renders by itself, with no client plugin. */
|
|
@@ -587,7 +592,7 @@ const {
|
|
|
587
592
|
),
|
|
588
593
|
display: offeredDisplayIds.has(defaultDisplay)
|
|
589
594
|
? defaultDisplay
|
|
590
|
-
:
|
|
595
|
+
: fallbackDisplayId,
|
|
591
596
|
density: props.density ?? DEFAULT_DENSITY,
|
|
592
597
|
displays: offeredDisplayIds,
|
|
593
598
|
},
|
|
@@ -1296,6 +1301,7 @@ const {
|
|
|
1296
1301
|
rowNavigation: {
|
|
1297
1302
|
rows: () => (shownResults.value ?? []) as Data[],
|
|
1298
1303
|
rowIdKey: props.rowIdKey ?? ROW_ID_DEFAULT_KEY,
|
|
1304
|
+
getRow: (id) => tableRows.getRow(id) as Promise<Data | undefined>,
|
|
1299
1305
|
},
|
|
1300
1306
|
recordScope: urlScope,
|
|
1301
1307
|
onRecordShown: (id) => recordLink?.shown(id),
|
|
@@ -1854,6 +1860,7 @@ const displayContext = computed(
|
|
|
1854
1860
|
items: shownResults.value ?? EMPTY_ITEMS,
|
|
1855
1861
|
columns: props.columns,
|
|
1856
1862
|
loading: isListLoading.value,
|
|
1863
|
+
firstPageLoading: isFirstPageLoading.value,
|
|
1857
1864
|
selection: { ids: selectedIds.value, ...displaySelection },
|
|
1858
1865
|
pagination: {
|
|
1859
1866
|
pageIndex: paginationState.value.pageIndex,
|
|
@@ -2070,14 +2077,14 @@ onMounted(() => {
|
|
|
2070
2077
|
checkUrlParameter();
|
|
2071
2078
|
void loadRelationQuickFilters();
|
|
2072
2079
|
void refreshLinkTabCounts();
|
|
2073
|
-
// The registry is client-only; once hydrated, degrade to the
|
|
2074
|
-
// the active id is no longer available (e.g. kanban with no eligible column).
|
|
2080
|
+
// The registry is client-only; once hydrated, degrade to the fallback display
|
|
2081
|
+
// if the active id is no longer available (e.g. kanban with no eligible column).
|
|
2075
2082
|
if (
|
|
2076
|
-
activeDisplayId.value !==
|
|
2083
|
+
activeDisplayId.value !== fallbackDisplayId &&
|
|
2077
2084
|
!isConfigRenderedDisplay(activeDisplayId.value) &&
|
|
2078
2085
|
!availableDisplays.value.some((d) => d.id === activeDisplayId.value)
|
|
2079
2086
|
) {
|
|
2080
|
-
activeDisplayId.value =
|
|
2087
|
+
activeDisplayId.value = fallbackDisplayId;
|
|
2081
2088
|
}
|
|
2082
2089
|
});
|
|
2083
2090
|
</script>
|
|
@@ -2123,6 +2130,7 @@ onMounted(() => {
|
|
|
2123
2130
|
:tabs="tabsWithCount"
|
|
2124
2131
|
:initial-column-visibility="initialVisibility"
|
|
2125
2132
|
:loading="isListLoading"
|
|
2133
|
+
:first-page-loading="isFirstPageLoading"
|
|
2126
2134
|
:load-error="listLoadError"
|
|
2127
2135
|
:data="shownResults || []"
|
|
2128
2136
|
:pagination-options="{ manualPagination: true, rowCount: data?.total }"
|
|
@@ -9,7 +9,14 @@ import {
|
|
|
9
9
|
|
|
10
10
|
const DEFAULT_BAR_RADIUS = 4;
|
|
11
11
|
const DEFAULT_BAR_WIDTH_PCT = "60%";
|
|
12
|
-
const
|
|
12
|
+
const HUNDRED_PERCENT = 100;
|
|
13
|
+
/** Ring thickness, in percent of the radius, of a donut without `arcWidth`. */
|
|
14
|
+
const DEFAULT_ARC_WIDTH = 24;
|
|
15
|
+
/**
|
|
16
|
+
* The thinnest ring, in percent of the radius, a slice label ("33.0%") fits
|
|
17
|
+
* in: on a thinner one its white text runs past the ring onto the card.
|
|
18
|
+
*/
|
|
19
|
+
const MIN_LABELLED_ARC_WIDTH = 35;
|
|
13
20
|
const DEFAULT_RADIAL_HOLE = "60%";
|
|
14
21
|
const CENTER_NAME_FONT_SIZE = "10.5px";
|
|
15
22
|
const CENTER_NAME_FONT_WEIGHT = 600;
|
|
@@ -17,7 +24,6 @@ const CENTER_VALUE_FONT_SIZE = "20px";
|
|
|
17
24
|
const CENTER_VALUE_FONT_WEIGHT = 650;
|
|
18
25
|
const HEATMAP_RADIUS = 4;
|
|
19
26
|
const HEATMAP_DEFAULT_INTENSITY = 0.6;
|
|
20
|
-
const HUNDRED_PERCENT = 100;
|
|
21
27
|
|
|
22
28
|
type PlotBuilder = (props: UseApexChartInput) => Record<string, unknown>;
|
|
23
29
|
|
|
@@ -47,8 +53,13 @@ function buildBarPlotOptions(
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
function donutSizeFor(arcWidth: number | undefined): string {
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
return `${HUNDRED_PERCENT - (arcWidth ?? DEFAULT_ARC_WIDTH)}%`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether a slice label fits inside the chart's ring; always true off donuts. */
|
|
60
|
+
export function ringFitsSliceLabels(input: UseApexChartInput): boolean {
|
|
61
|
+
if (input.type !== "donut") return true;
|
|
62
|
+
return (input.arcWidth ?? DEFAULT_ARC_WIDTH) >= MIN_LABELLED_ARC_WIDTH;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/** v2 donut centre: mono eyebrow over a 20px value. */
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { APEX_TYPE_CONFIGS } from "./apexTypeConfigs";
|
|
2
|
+
import type { ChartSeries } from "./types";
|
|
3
|
+
import type { UseApexChartInput } from "./useApexChart.types";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_Y_RANGE_PADDING = 0.08;
|
|
6
|
+
const ZERO_BOUND_MAGNITUDE = 1;
|
|
7
|
+
const STEP_DIVISIONS = 10;
|
|
8
|
+
const NICE_STEP_FACTORS = [1, 2, 5, 10];
|
|
9
|
+
const DECIMAL_BASE = 10;
|
|
10
|
+
/** Enough digits to keep every real value while dropping binary float noise. */
|
|
11
|
+
const SIGNIFICANT_DIGITS = 12;
|
|
12
|
+
|
|
13
|
+
const DOWNWARD = -1;
|
|
14
|
+
const UPWARD = 1;
|
|
15
|
+
|
|
16
|
+
type Rounding = (value: number) => number;
|
|
17
|
+
|
|
18
|
+
/** Bounds Apex resolves from the extent it measured on the drawn series. */
|
|
19
|
+
interface AutoYBounds {
|
|
20
|
+
min?: (nativeMin: number) => number;
|
|
21
|
+
max?: (nativeMax: number) => number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function withoutFloatNoise(value: number): number {
|
|
29
|
+
return Number(value.toPrecision(SIGNIFICANT_DIGITS));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pointValues(point: ChartSeries["data"][number]): unknown[] {
|
|
33
|
+
if (typeof point === "number") return [point];
|
|
34
|
+
if (!point || typeof point !== "object") return [];
|
|
35
|
+
return Array.isArray(point.y) ? point.y : [point.y];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function seriesValues(series: ChartSeries[]): number[] {
|
|
39
|
+
return series.flatMap((entry) =>
|
|
40
|
+
entry.data.flatMap(pointValues).filter(isFiniteNumber),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function annotationValues(input: UseApexChartInput): number[] {
|
|
45
|
+
return (input.annotations ?? [])
|
|
46
|
+
.flatMap((annotation) => [annotation.y, annotation.y2])
|
|
47
|
+
.filter(isFiniteNumber);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolvePadding(input: UseApexChartInput): number {
|
|
51
|
+
const requested = input.autoYRange?.padding ?? DEFAULT_Y_RANGE_PADDING;
|
|
52
|
+
return Number.isFinite(requested) && requested >= 0
|
|
53
|
+
? requested
|
|
54
|
+
: DEFAULT_Y_RANGE_PADDING;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function padOutward(bound: number, padding: number, direction: number): number {
|
|
58
|
+
return bound + direction * (Math.abs(bound) || ZERO_BOUND_MAGNITUDE) * padding;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The smallest 1, 2 or 5 times a power of ten that covers `span`. */
|
|
62
|
+
export function niceStep(span: number): number {
|
|
63
|
+
const magnitude = DECIMAL_BASE ** Math.floor(Math.log10(span));
|
|
64
|
+
const factor =
|
|
65
|
+
NICE_STEP_FACTORS.find((candidate) => candidate * magnitude >= span) ??
|
|
66
|
+
DECIMAL_BASE;
|
|
67
|
+
return withoutFloatNoise(factor * magnitude);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function snapToStep(value: number, step: number, round: Rounding): number {
|
|
71
|
+
return withoutFloatNoise(round(withoutFloatNoise(value / step)) * step);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The tick step for the padded extent of what is known up front: the series
|
|
76
|
+
* and the annotations. Apex only lays round ticks between bounds that are
|
|
77
|
+
* multiples of a round step; padded raw bounds give 603.6, 437.2…
|
|
78
|
+
*/
|
|
79
|
+
function estimateStep(values: number[], padding: number): number | undefined {
|
|
80
|
+
if (values.length === 0) return undefined;
|
|
81
|
+
const lower = padOutward(Math.min(...values), padding, DOWNWARD);
|
|
82
|
+
const upper = padOutward(Math.max(...values), padding, UPWARD);
|
|
83
|
+
const span = upper - lower;
|
|
84
|
+
return span > 0 ? niceStep(span / STEP_DIVISIONS) : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stepForBound(bound: number, knownStep: number | undefined): number {
|
|
88
|
+
if (knownStep !== undefined) return knownStep;
|
|
89
|
+
return niceStep((Math.abs(bound) || ZERO_BOUND_MAGNITUDE) / STEP_DIVISIONS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Y bounds that take in the annotations, padded outward and rounded to a round
|
|
94
|
+
* step so the axis ticks read 100, 200… rather than the raw padded extent.
|
|
95
|
+
*/
|
|
96
|
+
export function buildAutoYRange(input: UseApexChartInput): AutoYBounds {
|
|
97
|
+
if (!input.autoYRange || APEX_TYPE_CONFIGS[input.type].isCircular) return {};
|
|
98
|
+
const padding = resolvePadding(input);
|
|
99
|
+
const annotations = annotationValues(input);
|
|
100
|
+
const knownStep = estimateStep(
|
|
101
|
+
[...seriesValues(input.series), ...annotations],
|
|
102
|
+
padding,
|
|
103
|
+
);
|
|
104
|
+
const bound = (native: number, direction: number, round: Rounding) => {
|
|
105
|
+
const extent = [native, ...annotations];
|
|
106
|
+
const raw = direction === DOWNWARD ? Math.min(...extent) : Math.max(...extent);
|
|
107
|
+
const padded = padOutward(raw, padding, direction);
|
|
108
|
+
return snapToStep(padded, stepForBound(padded, knownStep), round);
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
min: (nativeMin) => bound(nativeMin, DOWNWARD, Math.floor),
|
|
112
|
+
max: (nativeMax) => bound(nativeMax, UPWARD, Math.ceil),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Axis labels without a format of their own. Apex writes them with as many
|
|
118
|
+
* decimals as the most precise data point, so 160 reads "160.00000000000000"
|
|
119
|
+
* once one price carries float noise.
|
|
120
|
+
*/
|
|
121
|
+
export function formatPlainAxisValue(value: number): string {
|
|
122
|
+
return isFiniteNumber(value) ? String(withoutFloatNoise(value)) : String(value);
|
|
123
|
+
}
|