@antelopejs/dms 0.14.0 → 0.15.1
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 +50 -0
- package/README.md +1 -1
- package/bin/control/journey.mjs +5 -0
- package/bin/control/session.mjs +1 -0
- package/bin/control/stack.mjs +1 -0
- package/dist/config/internal/state.d.ts +7 -0
- package/dist/config/internal/state.js +9 -0
- package/dist/config/internal/state.js.map +1 -1
- package/dist/config/startup-config.d.ts +1 -0
- package/dist/config/startup-config.js +11 -0
- package/dist/config/startup-config.js.map +1 -1
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/models/index.d.ts +1 -0
- package/dist/db/models/index.js +1 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/setupToken.model.d.ts +29 -0
- package/dist/db/models/setupToken.model.js +62 -0
- package/dist/db/models/setupToken.model.js.map +1 -0
- package/dist/db/setup-token.d.ts +2 -0
- package/dist/db/setup-token.js +28 -0
- package/dist/db/setup-token.js.map +1 -0
- package/dist/db/tables/index.d.ts +1 -0
- package/dist/db/tables/index.js +1 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/setupToken.table.d.ts +7 -0
- package/dist/db/tables/setupToken.table.js +34 -0
- package/dist/db/tables/setupToken.table.js.map +1 -0
- package/dist/implementations/dms/page/payloads.js +6 -10
- package/dist/implementations/dms/page/payloads.js.map +1 -1
- package/dist/implementations/dms/page/public-layout.d.ts +2 -0
- package/dist/implementations/dms/page/public-layout.js +45 -0
- package/dist/implementations/dms/page/public-layout.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/pages/settings/users/current-password.js +1 -1
- package/dist/pages/settings/users/current-password.js.map +1 -1
- package/dist/routes/auth/login.d.ts +1 -1
- package/dist/routes/auth/login.js +7 -2
- package/dist/routes/auth/login.js.map +1 -1
- package/dist/routes/auth/password-hash-upgrade.d.ts +2 -0
- package/dist/routes/auth/password-hash-upgrade.js +22 -0
- package/dist/routes/auth/password-hash-upgrade.js.map +1 -0
- package/dist/routes/authentication.d.ts +2 -0
- package/dist/routes/authentication.js +2 -0
- package/dist/routes/authentication.js.map +1 -1
- package/dist/routes/onboarding.d.ts +3 -1
- package/dist/routes/onboarding.js +20 -3
- package/dist/routes/onboarding.js.map +1 -1
- package/dist/utils/ensure-default-tenant.d.ts +2 -1
- package/dist/utils/ensure-default-tenant.js +126 -8
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/dist/validation/onboarding/register-admin.schema.d.ts +5 -0
- package/dist/validation/onboarding/register-admin.schema.js +3 -0
- package/dist/validation/onboarding/register-admin.schema.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +15 -1
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +1 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +1 -0
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +9 -9
- package/frontend-vue/layers/dms-layout/app/components/layout/AppLogo.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useInstalledModules.ts +78 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +5 -24
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +2 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +2 -1
- package/frontend-vue/layers/dms-onboarding/app/components/onboarding/steps/register.vue +21 -1
- package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +14 -0
- package/frontend-vue/layers/dms-onboarding/i18n/locales/onboarding-en-GB.json +2 -0
- package/frontend-vue/layers/dms-onboarding/i18n/locales/onboarding-fr-FR.json +2 -0
- 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 +3 -3
|
@@ -13,6 +13,7 @@ import { useSidebarState } from "./sidebarState";
|
|
|
13
13
|
import { useKeyboardPlatform } from "#dms-ui/app/composables/global/keyboardPlatform";
|
|
14
14
|
import { useMediaQuery } from "@vueuse/core";
|
|
15
15
|
import DmsSidebarUserMenu from "./SidebarUserMenu.vue";
|
|
16
|
+
import { useInstalledModuleCount } from "../../../composables/page/useInstalledModules";
|
|
16
17
|
|
|
17
18
|
const MODULES_PATH = "/modules";
|
|
18
19
|
|
|
@@ -205,14 +206,7 @@ watch(
|
|
|
205
206
|
{ immediate: true },
|
|
206
207
|
);
|
|
207
208
|
|
|
208
|
-
const moduleCount =
|
|
209
|
-
const modulesNode =
|
|
210
|
-
siteLayout.siteLayoutTree.value?.children[MODULES_TREE_KEY];
|
|
211
|
-
if (!modulesNode) return 0;
|
|
212
|
-
return modulesNode.childrenOrders.filter(
|
|
213
|
-
(childId) => modulesNode.children[childId]?.isModuleRoot,
|
|
214
|
-
).length;
|
|
215
|
-
});
|
|
209
|
+
const moduleCount = useInstalledModuleCount();
|
|
216
210
|
|
|
217
211
|
const footerItems = computed((): NavigationMenuItem[] => {
|
|
218
212
|
const result: NavigationMenuItem[] = [];
|
|
@@ -297,7 +291,13 @@ const footerItems = computed((): NavigationMenuItem[] => {
|
|
|
297
291
|
"
|
|
298
292
|
:class="isRail(sidebarRail) ? 'size-7' : 'h-12 w-auto'"
|
|
299
293
|
class="object-contain"
|
|
300
|
-
:alt="
|
|
294
|
+
:alt="
|
|
295
|
+
appConfig.branding?.logo?.alt
|
|
296
|
+
? t('navigation.goToBrandHomepage', {
|
|
297
|
+
name: appConfig.branding.logo.alt,
|
|
298
|
+
})
|
|
299
|
+
: t('navigation.goToHomepage')
|
|
300
|
+
"
|
|
301
301
|
/>
|
|
302
302
|
</DmsLink>
|
|
303
303
|
</template>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { StoreListing } from "#dms-core/app/types/module-store";
|
|
2
|
+
import type { ModuleCatalogEntry } from "#dms-core/app/types/page";
|
|
3
|
+
import type { Ref } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
sortStoreModules,
|
|
6
|
+
storeOnlyModules,
|
|
7
|
+
toModuleStoreEntry,
|
|
8
|
+
withStoreDetails,
|
|
9
|
+
} from "../../build/utils/module-store-catalog";
|
|
10
|
+
import { ENDPOINT_STORE_LISTING } from "./useModuleStore";
|
|
11
|
+
import { useModulesListing } from "./useModulesListing";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The installed modules, as the Modules page lists them and the sidebar
|
|
15
|
+
* counts them: the modules that register pages, with the store's details,
|
|
16
|
+
* and the installed store modules that register none, such as the Builder.
|
|
17
|
+
* Both read it here, so the badge and the page never count differently.
|
|
18
|
+
*
|
|
19
|
+
* @param modulesListing The registered modules
|
|
20
|
+
* @param moduleStore The module store
|
|
21
|
+
*/
|
|
22
|
+
export const useInstalledModules = (
|
|
23
|
+
modulesListing: ReturnType<typeof useModulesListing>,
|
|
24
|
+
moduleStore: { listing: Readonly<Ref<StoreListing | undefined>> },
|
|
25
|
+
) => {
|
|
26
|
+
const { locale } = useI18n();
|
|
27
|
+
const { processI18n } = useTranslation();
|
|
28
|
+
|
|
29
|
+
const storeModules = computed(() =>
|
|
30
|
+
sortStoreModules(
|
|
31
|
+
(moduleStore.listing.value?.modules ?? []).map((entry) =>
|
|
32
|
+
toModuleStoreEntry(entry, modulesListing.modules.value ?? []),
|
|
33
|
+
),
|
|
34
|
+
processI18n,
|
|
35
|
+
locale.value,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const modules = computed<ModuleCatalogEntry[]>(() => {
|
|
40
|
+
const registered = withStoreDetails(
|
|
41
|
+
(modulesListing.modules.value ?? []).filter((entry) => entry.hasAccess),
|
|
42
|
+
storeModules.value,
|
|
43
|
+
);
|
|
44
|
+
return [...registered, ...storeOnlyModules(storeModules.value, registered)];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { storeModules, modules };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How many modules are installed, for the sidebar's badge. The store listing
|
|
52
|
+
* waits on the npm registry, so a server render must not wait for it: the
|
|
53
|
+
* browser reads it, and the count shows once mounted and the read is over,
|
|
54
|
+
* whether it answered or failed. Until then the badge is absent, on the server
|
|
55
|
+
* and on the browser's first render, which a page that handed the listing
|
|
56
|
+
* over (the Modules page) would otherwise have filled in and mismatched, and
|
|
57
|
+
* while the read runs, so the count never shows without the store's modules.
|
|
58
|
+
*/
|
|
59
|
+
export const useInstalledModuleCount = () => {
|
|
60
|
+
const isOwner = useIsOwner();
|
|
61
|
+
const storeListing = useApiData<StoreListing>(
|
|
62
|
+
() => (isOwner.value ? ENDPOINT_STORE_LISTING : null),
|
|
63
|
+
{ server: false },
|
|
64
|
+
);
|
|
65
|
+
const { modules } = useInstalledModules(useModulesListing(), {
|
|
66
|
+
listing: computed(() => storeListing.data.value ?? undefined),
|
|
67
|
+
});
|
|
68
|
+
const isMounted = ref(false);
|
|
69
|
+
onMounted(() => (isMounted.value = true));
|
|
70
|
+
const isSettled = computed(
|
|
71
|
+
() =>
|
|
72
|
+
storeListing.status.value === "success" ||
|
|
73
|
+
storeListing.status.value === "error",
|
|
74
|
+
);
|
|
75
|
+
return computed(() =>
|
|
76
|
+
isMounted.value && isSettled.value ? modules.value.length : 0,
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
useModulesListing,
|
|
21
21
|
} from "../composables/page/useModulesListing";
|
|
22
22
|
import { useModuleHistory } from "../composables/page/useModuleHistory";
|
|
23
|
-
import type { ModuleCatalogEntry } from "#dms-core/app/types/page";
|
|
24
23
|
import {
|
|
25
24
|
MODULE_CATEGORY_ALL,
|
|
26
25
|
MODULE_STORE_PATH,
|
|
@@ -33,13 +32,8 @@ import {
|
|
|
33
32
|
summarizeModules,
|
|
34
33
|
type ModuleSortMode,
|
|
35
34
|
} from "../build/utils/modules-catalog";
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
storeOnlyModules,
|
|
39
|
-
storeOpenTarget,
|
|
40
|
-
toModuleStoreEntry,
|
|
41
|
-
withStoreDetails,
|
|
42
|
-
} from "../build/utils/module-store-catalog";
|
|
35
|
+
import { storeOpenTarget } from "../build/utils/module-store-catalog";
|
|
36
|
+
import { useInstalledModules } from "../composables/page/useInstalledModules";
|
|
43
37
|
import { useModuleStore } from "../composables/page/useModuleStore";
|
|
44
38
|
import { useModuleStoreActions } from "../composables/page/useModuleStoreActions";
|
|
45
39
|
import DmsSearchInput from "#dms-ui/app/entries/components/form/SearchInput";
|
|
@@ -123,26 +117,13 @@ const isInitiallyLoading = computed(
|
|
|
123
117
|
);
|
|
124
118
|
const isRetrying = ref(false);
|
|
125
119
|
|
|
126
|
-
const storeModules =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
toModuleStoreEntry(entry, modulesListing.modules.value ?? []),
|
|
130
|
-
),
|
|
131
|
-
processI18n,
|
|
132
|
-
locale.value,
|
|
133
|
-
),
|
|
120
|
+
const { storeModules, modules } = useInstalledModules(
|
|
121
|
+
modulesListing,
|
|
122
|
+
moduleStore,
|
|
134
123
|
);
|
|
135
124
|
const storeEntries = computed(
|
|
136
125
|
() => new Map(storeModules.value.map((entry) => [entry.id, entry])),
|
|
137
126
|
);
|
|
138
|
-
|
|
139
|
-
const modules = computed<ModuleCatalogEntry[]>(() => {
|
|
140
|
-
const registered = withStoreDetails(
|
|
141
|
-
(modulesListing.modules.value ?? []).filter((entry) => entry.hasAccess),
|
|
142
|
-
storeModules.value,
|
|
143
|
-
);
|
|
144
|
-
return [...registered, ...storeOnlyModules(storeModules.value, registered)];
|
|
145
|
-
});
|
|
146
127
|
const summary = computed(() => summarizeModules(modules.value));
|
|
147
128
|
|
|
148
129
|
const visitedAt = computed(
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
|
|
24
24
|
interface RegisterStepProps {
|
|
25
25
|
platform: OnboardingPlatform;
|
|
26
|
+
/** The token the address carried; empty when the wizard has to ask for it. */
|
|
27
|
+
setupToken: string;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface RegisterStepEmits {
|
|
@@ -43,6 +45,7 @@ const MAX_NAME_PART_LENGTH = 100;
|
|
|
43
45
|
|
|
44
46
|
const isLoading = ref(false);
|
|
45
47
|
const isRegistered = ref(false);
|
|
48
|
+
const asksForSetupToken = !props.setupToken;
|
|
46
49
|
|
|
47
50
|
const namePart = z.string().trim().min(1).max(MAX_NAME_PART_LENGTH);
|
|
48
51
|
|
|
@@ -52,12 +55,14 @@ const fields = z
|
|
|
52
55
|
lastName: namePart,
|
|
53
56
|
email: z.string().trim().email(),
|
|
54
57
|
password: passwordSchema,
|
|
58
|
+
setupToken: z.string().trim().min(1),
|
|
55
59
|
})
|
|
56
60
|
.transform((data) => ({
|
|
57
61
|
firstName: striptags(data.firstName),
|
|
58
62
|
lastName: striptags(data.lastName),
|
|
59
63
|
email: striptags(data.email),
|
|
60
64
|
password: data.password,
|
|
65
|
+
setupToken: data.setupToken,
|
|
61
66
|
}));
|
|
62
67
|
|
|
63
68
|
type Schema = z.output<typeof fields>;
|
|
@@ -68,6 +73,7 @@ const state = reactive<Partial<Schema>>({
|
|
|
68
73
|
lastName: undefined,
|
|
69
74
|
email: undefined,
|
|
70
75
|
password: undefined,
|
|
76
|
+
setupToken: props.setupToken,
|
|
71
77
|
});
|
|
72
78
|
useLiveFormErrors(form, state);
|
|
73
79
|
|
|
@@ -106,7 +112,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
await showError(error, "page.onboarding.administrator.error_title", {
|
|
109
|
-
fields: ["firstName", "lastName", "email", "password"],
|
|
115
|
+
fields: ["firstName", "lastName", "email", "password", "setupToken"],
|
|
110
116
|
form,
|
|
111
117
|
});
|
|
112
118
|
} finally {
|
|
@@ -139,6 +145,20 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
|
|
|
139
145
|
>
|
|
140
146
|
<AuthFormAlert :error="formError" />
|
|
141
147
|
|
|
148
|
+
<UFormField
|
|
149
|
+
v-if="asksForSetupToken"
|
|
150
|
+
:label="$t('page.onboarding.administrator.setup_token')"
|
|
151
|
+
:help="$t('page.onboarding.administrator.setup_token_help')"
|
|
152
|
+
name="setupToken"
|
|
153
|
+
>
|
|
154
|
+
<UInput
|
|
155
|
+
v-model="state.setupToken"
|
|
156
|
+
autocomplete="off"
|
|
157
|
+
size="lg"
|
|
158
|
+
class="w-full"
|
|
159
|
+
/>
|
|
160
|
+
</UFormField>
|
|
161
|
+
|
|
142
162
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
143
163
|
<UFormField
|
|
144
164
|
:label="$t('page.onboarding.administrator.first_name')"
|
|
@@ -26,6 +26,19 @@ watch(metaTitle, (title) => {
|
|
|
26
26
|
});
|
|
27
27
|
const administrator = ref<OnboardingAdministrator | null>(null);
|
|
28
28
|
|
|
29
|
+
const route = useDmsRoute();
|
|
30
|
+
const router = useDmsRouter();
|
|
31
|
+
const setupToken = ref("");
|
|
32
|
+
|
|
33
|
+
useHead({ meta: [{ name: "referrer", content: "no-referrer" }] });
|
|
34
|
+
|
|
35
|
+
onMounted(() => {
|
|
36
|
+
const { token, ...query } = route.query;
|
|
37
|
+
if (typeof token !== "string" || !token) return;
|
|
38
|
+
setupToken.value = token;
|
|
39
|
+
void router.replace({ query });
|
|
40
|
+
});
|
|
41
|
+
|
|
29
42
|
const stepLabels = computed(() =>
|
|
30
43
|
ONBOARDING_STEPS.map((id) => t(`page.onboarding.steps.${id}`)),
|
|
31
44
|
);
|
|
@@ -53,6 +66,7 @@ function onRegistered(owner: OnboardingAdministrator) {
|
|
|
53
66
|
<Register
|
|
54
67
|
v-else-if="step === 'administrator'"
|
|
55
68
|
:platform="platform"
|
|
69
|
+
:setup-token="setupToken"
|
|
56
70
|
@back="step = 'platform'"
|
|
57
71
|
@registered="onRegistered"
|
|
58
72
|
/>
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"first_name": "First name",
|
|
29
29
|
"last_name": "Last name",
|
|
30
30
|
"name_required": "Required",
|
|
31
|
+
"setup_token": "Setup token",
|
|
32
|
+
"setup_token_help": "Printed in the server log when the instance started, or the value of DMS_SETUP_TOKEN.",
|
|
31
33
|
"email": "Work e-mail",
|
|
32
34
|
"submit": "Create administrator account",
|
|
33
35
|
"back": "Back to platform details",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"first_name": "Prénom",
|
|
29
29
|
"last_name": "Nom",
|
|
30
30
|
"name_required": "Obligatoire",
|
|
31
|
+
"setup_token": "Jeton d'installation",
|
|
32
|
+
"setup_token_help": "Affiché dans les logs du serveur au démarrage de l'instance, ou valeur de DMS_SETUP_TOKEN.",
|
|
31
33
|
"email": "E-mail professionnel",
|
|
32
34
|
"submit": "Créer le compte administrateur",
|
|
33
35
|
"back": "Retour aux informations de la plateforme",
|
|
@@ -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
|