@antelopejs/dms 0.4.4 → 0.4.6
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 +43 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks/invite-extensions.js +2 -2
- package/dist/hooks/invite-extensions.js.map +1 -1
- package/dist/implementations/dms/layout-banners.d.ts +18 -0
- package/dist/implementations/dms/layout-banners.js +60 -0
- package/dist/implementations/dms/layout-banners.js.map +1 -0
- package/dist/implementations/dms/page.d.ts +3 -0
- package/dist/implementations/dms/page.js +90 -28
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms/quick-actions.d.ts +5 -0
- package/dist/implementations/dms/quick-actions.js.map +1 -1
- package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
- package/dist/implementations/dms/resolver-timeout.js +13 -0
- package/dist/implementations/dms/resolver-timeout.js.map +1 -0
- package/dist/implementations/dms/saas-mode.d.ts +8 -0
- package/dist/implementations/dms/saas-mode.js +20 -0
- package/dist/implementations/dms/saas-mode.js.map +1 -0
- package/dist/implementations/dms/stale-session.d.ts +2 -0
- package/dist/implementations/dms/stale-session.js +16 -0
- package/dist/implementations/dms/stale-session.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +3 -1
- package/dist/implementations/dms-auth/index.js +38 -8
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/implementations/dms-html-render/service-jwt.js +2 -1
- package/dist/implementations/dms-html-render/service-jwt.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/settings/users/index.d.ts +1 -0
- package/dist/pages/settings/users/index.js +1 -0
- package/dist/pages/settings/users/index.js.map +1 -1
- package/dist/pages/settings/users/invite-extension-routes.d.ts +4 -0
- package/dist/pages/settings/users/invite-extension-routes.js +57 -0
- package/dist/pages/settings/users/invite-extension-routes.js.map +1 -0
- package/dist/pages/settings/users/invites.d.ts +2 -1
- package/dist/pages/settings/users/invites.js +17 -5
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.d.ts +4 -1
- package/dist/pages/settings/users/members.js +10 -6
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -1
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/pages/settings/users/quick-actions.d.ts +1 -0
- package/dist/pages/settings/users/quick-actions.js +22 -0
- package/dist/pages/settings/users/quick-actions.js.map +1 -0
- package/dist/routes/auth/client-ip.d.ts +7 -0
- package/dist/routes/auth/client-ip.js +42 -0
- package/dist/routes/auth/client-ip.js.map +1 -0
- package/dist/routes/auth/index.d.ts +1 -0
- package/dist/routes/auth/index.js +1 -0
- package/dist/routes/auth/index.js.map +1 -1
- package/dist/routes/auth/signup.d.ts +2 -0
- package/dist/routes/auth/signup.js +7 -2
- package/dist/routes/auth/signup.js.map +1 -1
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.js +15 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/test/integration/auth/client-ip.test.d.ts +1 -0
- package/dist/test/integration/auth/client-ip.test.js +14 -0
- package/dist/test/integration/auth/client-ip.test.js.map +1 -0
- package/dist/test/integration/invite-extension-edit.test.d.ts +1 -0
- package/dist/test/integration/invite-extension-edit.test.js +122 -0
- package/dist/test/integration/invite-extension-edit.test.js.map +1 -0
- package/dist/test/integration/page-session.test.d.ts +1 -0
- package/dist/test/integration/page-session.test.js +52 -0
- package/dist/test/integration/page-session.test.js.map +1 -0
- package/dist/test/integration/saas-mode-ownership.test.d.ts +1 -0
- package/dist/test/integration/saas-mode-ownership.test.js +81 -0
- package/dist/test/integration/saas-mode-ownership.test.js.map +1 -0
- package/dist/test/integration/tenant-lifecycle.test.js +38 -0
- package/dist/test/integration/tenant-lifecycle.test.js.map +1 -1
- package/dist/test/unit/client-ip.test.d.ts +1 -0
- package/dist/test/unit/client-ip.test.js +77 -0
- package/dist/test/unit/client-ip.test.js.map +1 -0
- package/dist/test/unit/implementations/dms/layout-banners.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/layout-banners.test.js +217 -0
- package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -0
- package/dist/test/unit/implementations/dms/quick-actions-button.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/quick-actions-button.test.js +220 -0
- package/dist/test/unit/implementations/dms/quick-actions-button.test.js.map +1 -0
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +1 -1
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/service-jwt.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js +37 -0
- package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/invite-extension.test.js +138 -12
- package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/saas-mode.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/saas-mode.test.js +42 -0
- package/dist/test/unit/interfaces/dms/saas-mode.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/tenant-invite-email.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js +53 -0
- package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js +80 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js +96 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js.map +1 -0
- package/dist/test/unit/pages/settings/invite-member-quick-action.test.d.ts +1 -0
- package/dist/test/unit/pages/settings/invite-member-quick-action.test.js +104 -0
- package/dist/test/unit/pages/settings/invite-member-quick-action.test.js.map +1 -0
- package/dist/test/unit/pages/settings/notifications-access-gate.test.d.ts +1 -0
- package/dist/test/unit/pages/settings/notifications-access-gate.test.js +78 -0
- package/dist/test/unit/pages/settings/notifications-access-gate.test.js.map +1 -0
- package/dist/test/unit/signup-account-guard.test.js +20 -2
- package/dist/test/unit/signup-account-guard.test.js.map +1 -1
- package/dist/test/unit/utils/admin-invite-email.test.d.ts +1 -0
- package/dist/test/unit/utils/admin-invite-email.test.js +69 -0
- package/dist/test/unit/utils/admin-invite-email.test.js.map +1 -0
- package/dist/utils/admin-invite-email.d.ts +14 -0
- package/dist/utils/admin-invite-email.js +51 -0
- package/dist/utils/admin-invite-email.js.map +1 -0
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/validation/auth.schema.d.ts +3 -3
- package/dist/validation/auth.schema.js +1 -1
- package/dist/validation/auth.schema.js.map +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +37 -8
- package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -2
- package/frontend-vue/layers/dms-auth/i18n/locales/auth-en-GB.json +3 -1
- package/frontend-vue/layers/dms-auth/i18n/locales/auth-fr-FR.json +3 -1
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +27 -2
- package/frontend-vue/layers/dms-core/app/utils/menu.ts +26 -10
- package/frontend-vue/layers/dms-core/app/utils/routePath.ts +23 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +3 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +3 -0
- package/frontend-vue/layers/dms-core/shared/types/runtime-config.d.ts +4 -1
- package/frontend-vue/layers/dms-layout/app/app.config.ts +9 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +50 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +16 -13
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +2 -2
- package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +11 -5
- package/frontend-vue/layers/dms-layout/app/build/components/notification/widgetRequest.ts +20 -0
- package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -5
- package/frontend-vue/layers/dms-layout/app/components/profile/ProfileLanguage.vue +3 -23
- package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +59 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useQuickActions.ts +15 -2
- package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +33 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +54 -9
- package/frontend-vue/layers/dms-layout/app/error.vue +17 -2
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +2 -1
- package/frontend-vue/layers/dms-layout/app/types/page.ts +22 -0
- package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +48 -0
- package/frontend-vue/layers/dms-layout/app/utils/favorites.ts +94 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +21 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +21 -2
- package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/index.ts +1 -1
- package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/{useOnboardingMiddleware.ts → useOnboarding.ts} +0 -12
- package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +4 -1
- package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +21 -4
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +31 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationBadges.ts +36 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +2 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/columnVisibility.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/build/types/tree.ts +5 -10
- package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +6 -16
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/chart/internal/TrendBadge.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/copy/CopyButton.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +3 -3
- package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/status/StatusSummary.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +33 -10
- package/frontend-vue/layers/dms-ui/app/composables/chart/donutRecords.ts +26 -0
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +4 -2
- package/frontend-vue/layers/dms-ui/app/composables/tab/useTab.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/column.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/types/quick-actions.ts +38 -3
- package/frontend-vue/tests/admin-invite-email.test.ts +86 -0
- package/frontend-vue/tests/api-error-title.test.ts +24 -0
- package/frontend-vue/tests/auth-middleware-error.test.ts +58 -0
- package/frontend-vue/tests/breadcrumb-target.test.ts +50 -0
- package/frontend-vue/tests/button-disabled-theme.test.ts +21 -0
- package/frontend-vue/tests/chart-nested-donut.test.ts +97 -0
- package/frontend-vue/tests/error-page-message.test.ts +83 -0
- package/frontend-vue/tests/favorite-page.test.ts +104 -0
- package/frontend-vue/tests/icon-aria-hidden.test.ts +29 -0
- package/frontend-vue/tests/layout-banners.test.ts +224 -0
- package/frontend-vue/tests/menu-navigation.test.ts +39 -10
- package/frontend-vue/tests/notification-popover-errors.test.ts +147 -0
- package/frontend-vue/tests/onboarding-middleware.test.ts +103 -0
- package/frontend-vue/tests/password-strength-meter.test.ts +33 -0
- package/frontend-vue/tests/quick-action-intent.test.ts +74 -0
- package/frontend-vue/tests/quick-actions.test.ts +23 -0
- package/frontend-vue/tests/relation-badges.test.ts +32 -0
- package/frontend-vue/tests/signup-invitation-link.test.ts +126 -0
- package/frontend-vue/tests/skeleton-paragraph.test.ts +30 -0
- package/frontend-vue/tests/tab-without-component-id.test.ts +51 -0
- package/frontend-vue/tests/table-column-visibility.test.ts +20 -0
- package/frontend-vue/tests/user-language.test.ts +58 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/REFERENCE.md +1 -1
package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { formatRelativeTime } from "#dms-core/app/utils/formatter";
|
|
3
3
|
import NotificationCard from "./NotificationCard.vue";
|
|
4
|
+
import { settleWidgetRequest } from "./widgetRequest";
|
|
4
5
|
|
|
5
6
|
const MAX_DISPLAYED_COUNT = 99;
|
|
6
7
|
|
|
@@ -25,9 +26,14 @@ const { isLoadingMore, setupObserver, disconnectObserver } = useInfiniteScroll(
|
|
|
25
26
|
hasMore,
|
|
26
27
|
);
|
|
27
28
|
|
|
29
|
+
const refreshUnreadCount = () =>
|
|
30
|
+
settleWidgetRequest(fetchUnreadCount, () => {
|
|
31
|
+
unreadCount.value = 0;
|
|
32
|
+
});
|
|
33
|
+
|
|
28
34
|
const handleNotificationEvent = async () => {
|
|
29
35
|
if (!isOpen.value) {
|
|
30
|
-
await
|
|
36
|
+
await refreshUnreadCount();
|
|
31
37
|
}
|
|
32
38
|
};
|
|
33
39
|
|
|
@@ -37,14 +43,14 @@ const handleFormSubmitSuccess = async (event: Event) => {
|
|
|
37
43
|
|
|
38
44
|
if (submitUrl && submitUrl.includes("/api/notification/")) {
|
|
39
45
|
if (!isOpen.value) {
|
|
40
|
-
await
|
|
46
|
+
await refreshUnreadCount();
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
const markNotificationsAsRead = async () => {
|
|
46
52
|
if (hasBeenOpened.value) {
|
|
47
|
-
await markAllAsRead
|
|
53
|
+
await settleWidgetRequest(markAllAsRead);
|
|
48
54
|
hasBeenOpened.value = false;
|
|
49
55
|
}
|
|
50
56
|
};
|
|
@@ -57,7 +63,7 @@ const handleBeforeUnload = () => {
|
|
|
57
63
|
|
|
58
64
|
onMounted(async () => {
|
|
59
65
|
if (!loggedIn.value) return;
|
|
60
|
-
await
|
|
66
|
+
await refreshUnreadCount();
|
|
61
67
|
|
|
62
68
|
window.addEventListener(
|
|
63
69
|
NotificationEvents.NOTIFICATION_RECEIVED,
|
|
@@ -87,7 +93,7 @@ onUnmounted(() => {
|
|
|
87
93
|
watch(isOpen, async (isNowOpen) => {
|
|
88
94
|
if (isNowOpen) {
|
|
89
95
|
hasBeenOpened.value = true;
|
|
90
|
-
await fetchNotifications(true);
|
|
96
|
+
await settleWidgetRequest(() => fetchNotifications(true));
|
|
91
97
|
await nextTick();
|
|
92
98
|
setupObserver();
|
|
93
99
|
} else {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const WIDGET_REQUEST_FAILED_MESSAGE = "[dms] Header widget request failed";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs a request a layout widget issues on its own (on mount, on a realtime
|
|
5
|
+
* event, on open). A failure -- a tenant access gate answering 403, a network
|
|
6
|
+
* error -- is logged and degrades the widget through `onFailure` instead of
|
|
7
|
+
* reaching the app error handler, which would replace the whole page with an
|
|
8
|
+
* error screen.
|
|
9
|
+
*/
|
|
10
|
+
export async function settleWidgetRequest(
|
|
11
|
+
request: () => Promise<unknown>,
|
|
12
|
+
onFailure?: () => void,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
await request();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
onFailure?.();
|
|
18
|
+
console.warn(WIDGET_REQUEST_FAILED_MESSAGE, error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -82,11 +82,6 @@ function onLinkHover(): void {
|
|
|
82
82
|
const { to } = props.item;
|
|
83
83
|
if (typeof to === "string") {
|
|
84
84
|
prefetchPageLayout(stripQueryAndHash(to));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const path = (to as MenuItemTarget | undefined)?.path;
|
|
88
|
-
if (path) {
|
|
89
|
-
prefetchPageLayout(path);
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
87
|
</script>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const { locale
|
|
2
|
+
const { locale } = useI18n();
|
|
3
3
|
const { uniqueLocales } = useUniqueLocales();
|
|
4
|
-
const {
|
|
5
|
-
const { user, fetch: refreshUser } = useUserSession();
|
|
4
|
+
const { changeLanguage } = useUserLanguage();
|
|
6
5
|
|
|
7
6
|
const languageOptions = computed(() =>
|
|
8
7
|
uniqueLocales.value.map((lang) => ({
|
|
@@ -10,25 +9,6 @@ const languageOptions = computed(() =>
|
|
|
10
9
|
value: lang.code,
|
|
11
10
|
})),
|
|
12
11
|
);
|
|
13
|
-
|
|
14
|
-
const handleLanguageChange = async (newLanguage: "en" | "fr") => {
|
|
15
|
-
setLocale(newLanguage);
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
await $authFetch("/settings/user/profile", {
|
|
19
|
-
method: "POST",
|
|
20
|
-
body: {
|
|
21
|
-
name: user.value?.name,
|
|
22
|
-
email: user.value?.email,
|
|
23
|
-
language: newLanguage,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
await refreshUser();
|
|
28
|
-
} catch {
|
|
29
|
-
/* ignore — silently skip the update on network / backend errors */
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
12
|
</script>
|
|
33
13
|
|
|
34
14
|
<template>
|
|
@@ -62,7 +42,7 @@ const handleLanguageChange = async (newLanguage: "en" | "fr") => {
|
|
|
62
42
|
class="w-full"
|
|
63
43
|
:items="languageOptions"
|
|
64
44
|
:model-value="locale"
|
|
65
|
-
@update:model-value="
|
|
45
|
+
@update:model-value="changeLanguage"
|
|
66
46
|
/>
|
|
67
47
|
</div>
|
|
68
48
|
</section>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const DISMISSED_BANNERS_COOKIE = "dms-dismissed-banners";
|
|
2
|
+
|
|
3
|
+
type LayoutBannerRole = "status" | "alert";
|
|
4
|
+
|
|
5
|
+
interface LayoutBannerPresentation {
|
|
6
|
+
role: LayoutBannerRole;
|
|
7
|
+
color: LayoutBannerVariant;
|
|
8
|
+
icon: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// An error interrupts the screen reader; info and warning wait their turn.
|
|
12
|
+
const PRESENTATIONS: Record<LayoutBannerVariant, LayoutBannerPresentation> = {
|
|
13
|
+
info: { role: "status", color: "info", icon: "i-ph-info-light" },
|
|
14
|
+
warning: { role: "status", color: "warning", icon: "i-ph-warning-light" },
|
|
15
|
+
error: { role: "alert", color: "error", icon: "i-ph-warning-octagon-light" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Role, color and icon a banner renders with. An unknown variant — from a
|
|
20
|
+
* newer backend, or an untyped registration — renders as `info` rather than
|
|
21
|
+
* as an unstyled strip.
|
|
22
|
+
*/
|
|
23
|
+
export const resolveLayoutBannerPresentation = (
|
|
24
|
+
banner: LayoutBanner,
|
|
25
|
+
): LayoutBannerPresentation => {
|
|
26
|
+
const presentation = PRESENTATIONS[banner.variant] ?? PRESENTATIONS.info;
|
|
27
|
+
return { ...presentation, icon: banner.icon ?? presentation.icon };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The global banners rendered under the dashboard header, as resolved by the
|
|
32
|
+
* server for this request (see `RegisterLayoutBanner` in
|
|
33
|
+
* `@antelopejs/interface-dms/layout-banners`), minus the ones the user
|
|
34
|
+
* dismissed.
|
|
35
|
+
*
|
|
36
|
+
* Dismissals are kept in a session cookie rather than in local storage so the
|
|
37
|
+
* server-rendered page already leaves a dismissed banner out, instead of
|
|
38
|
+
* painting it and removing it on hydration.
|
|
39
|
+
*/
|
|
40
|
+
export const useLayoutBanners = () => {
|
|
41
|
+
const { siteLayout } = useSiteLayout();
|
|
42
|
+
const dismissed = useDmsCookie<string[]>(DISMISSED_BANNERS_COOKIE, {
|
|
43
|
+
default: () => [],
|
|
44
|
+
sameSite: "lax",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const banners = computed<LayoutBanner[]>(() =>
|
|
48
|
+
(siteLayout.value?.banners ?? []).filter(
|
|
49
|
+
(banner) => !banner.dismissible || !dismissed.value.includes(banner.key),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function dismiss(key: string): void {
|
|
54
|
+
if (dismissed.value.includes(key)) return;
|
|
55
|
+
dismissed.value = [...dismissed.value, key];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { banners, dismiss };
|
|
59
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
QUICK_ACTION_ADD,
|
|
3
|
+
QUICK_ACTION_BUTTON,
|
|
4
|
+
QUICK_ACTION_BUTTON_KEY,
|
|
3
5
|
QUICK_ACTION_COMPONENT_KEY,
|
|
4
6
|
QUICK_ACTION_QUERY_KEY,
|
|
5
7
|
} from "#dms-ui/app/types/quick-actions";
|
|
@@ -32,6 +34,17 @@ const targetHandlers: Record<
|
|
|
32
34
|
},
|
|
33
35
|
});
|
|
34
36
|
},
|
|
37
|
+
button: (target) => {
|
|
38
|
+
if (target.type !== "button") return;
|
|
39
|
+
return navigateDms({
|
|
40
|
+
path: target.to,
|
|
41
|
+
query: {
|
|
42
|
+
[QUICK_ACTION_QUERY_KEY]: QUICK_ACTION_BUTTON,
|
|
43
|
+
[QUICK_ACTION_COMPONENT_KEY]: target.component,
|
|
44
|
+
[QUICK_ACTION_BUTTON_KEY]: target.button,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
},
|
|
35
48
|
event: (target) => {
|
|
36
49
|
if (target.type !== "event") return;
|
|
37
50
|
if (typeof window === "undefined") return;
|
|
@@ -43,8 +56,8 @@ const targetHandlers: Record<
|
|
|
43
56
|
|
|
44
57
|
/**
|
|
45
58
|
* Runs the client-side behavior a quick action declares through its
|
|
46
|
-
* discriminated `target`: `navigate` and `
|
|
47
|
-
* page, `event` dispatches a window `CustomEvent` in place for whoever
|
|
59
|
+
* discriminated `target`: `navigate`, `openForm` and `button` route to the
|
|
60
|
+
* action's page, `event` dispatches a window `CustomEvent` in place for whoever
|
|
48
61
|
* listens.
|
|
49
62
|
*/
|
|
50
63
|
export function dispatchQuickActionTarget(target: QuickActionTarget): unknown {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const PROFILE_ENDPOINT = "/settings/user/profile";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Switches the interface language and stores it on the user's profile, so the
|
|
5
|
+
* `language-sync` plugin re-applies it on the next load instead of reverting
|
|
6
|
+
* to the previously stored language. Every language switcher goes through it.
|
|
7
|
+
*/
|
|
8
|
+
export function useUserLanguage() {
|
|
9
|
+
const dmsApp = useDmsApp();
|
|
10
|
+
const { $authFetch } = useAuthFetch();
|
|
11
|
+
const { user, fetch: refreshUser } = useUserSession();
|
|
12
|
+
|
|
13
|
+
async function changeLanguage(language: string): Promise<void> {
|
|
14
|
+
await dmsApp.$i18n.setLocale(language);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await $authFetch(PROFILE_ENDPOINT, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: {
|
|
20
|
+
name: user.value?.name,
|
|
21
|
+
email: user.value?.email,
|
|
22
|
+
language,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await refreshUser();
|
|
27
|
+
} catch {
|
|
28
|
+
/* ignore — silently skip the update on network / backend errors */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { changeLanguage };
|
|
33
|
+
}
|
|
@@ -3,6 +3,7 @@ import Container from "../build/components/layout/Container.vue";
|
|
|
3
3
|
import PageHeader from "../build/components/layout/PageHeader.vue";
|
|
4
4
|
import DashboardSidebar from "../build/components/layout/DashboardSidebar.vue";
|
|
5
5
|
import DashboardHeader from "../build/components/layout/DashboardHeader.vue";
|
|
6
|
+
import DashboardBanners from "../build/components/layout/DashboardBanners.vue";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
fullWidth?: boolean;
|
|
@@ -28,6 +29,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
28
29
|
<UDashboardPanel>
|
|
29
30
|
<template #header>
|
|
30
31
|
<DashboardHeader />
|
|
32
|
+
<DashboardBanners />
|
|
31
33
|
</template>
|
|
32
34
|
|
|
33
35
|
<template #body>
|
|
@@ -1,31 +1,76 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
2
4
|
import EmailLayout from "./components/EmailLayout.vue";
|
|
3
5
|
import EmailButton from "./components/EmailButton.vue";
|
|
4
6
|
import { emailStyles } from "./components/index";
|
|
5
7
|
|
|
6
|
-
defineProps<{
|
|
8
|
+
const props = defineProps<{
|
|
7
9
|
email: string;
|
|
8
10
|
signupLink: string;
|
|
9
|
-
|
|
11
|
+
expiresInDays: number;
|
|
12
|
+
workspaceName?: string;
|
|
13
|
+
inviterName?: string;
|
|
14
|
+
platformName?: string;
|
|
10
15
|
}>();
|
|
16
|
+
|
|
17
|
+
const { t, locale } = useI18n();
|
|
18
|
+
|
|
19
|
+
const joinTarget = computed(() => {
|
|
20
|
+
const { workspaceName, platformName } = props;
|
|
21
|
+
if (workspaceName && platformName) {
|
|
22
|
+
return t("emails.admin_invite.workspace_on_platform", {
|
|
23
|
+
workspace: workspaceName,
|
|
24
|
+
platform: platformName,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return workspaceName || platformName;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const title = computed(() =>
|
|
31
|
+
props.workspaceName
|
|
32
|
+
? t("emails.admin_invite.title_workspace", {
|
|
33
|
+
workspace: props.workspaceName,
|
|
34
|
+
})
|
|
35
|
+
: t("emails.admin_invite.title"),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const invitation = computed(() => {
|
|
39
|
+
if (!joinTarget.value) {
|
|
40
|
+
return t("emails.admin_invite.invitation");
|
|
41
|
+
}
|
|
42
|
+
return props.inviterName
|
|
43
|
+
? t("emails.admin_invite.invitation_from_inviter", {
|
|
44
|
+
inviter: props.inviterName,
|
|
45
|
+
target: joinTarget.value,
|
|
46
|
+
})
|
|
47
|
+
: t("emails.admin_invite.invitation_to_target", {
|
|
48
|
+
target: joinTarget.value,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
11
51
|
</script>
|
|
12
52
|
|
|
13
53
|
<template>
|
|
14
|
-
<EmailLayout title="
|
|
15
|
-
<EText :style="emailStyles.text">
|
|
54
|
+
<EmailLayout :title="title" :lang="locale">
|
|
55
|
+
<EText :style="emailStyles.text">
|
|
56
|
+
{{ t("emails.admin_invite.greeting") }}
|
|
57
|
+
</EText>
|
|
16
58
|
|
|
17
59
|
<EText :style="emailStyles.text">
|
|
18
|
-
|
|
19
|
-
your account:
|
|
60
|
+
{{ invitation }} {{ t("emails.admin_invite.call_to_action") }}
|
|
20
61
|
</EText>
|
|
21
62
|
|
|
22
|
-
<EmailButton
|
|
23
|
-
|
|
63
|
+
<EmailButton
|
|
64
|
+
:href="signupLink"
|
|
65
|
+
:expires-in="t('emails.admin_invite.expires_in', { days: expiresInDays })"
|
|
66
|
+
:expires-label="t('emails.admin_invite.expires_label')"
|
|
67
|
+
>
|
|
68
|
+
{{ t("emails.admin_invite.button") }}
|
|
24
69
|
</EmailButton>
|
|
25
70
|
|
|
26
71
|
<template #footer>
|
|
27
72
|
<EText :style="emailStyles.footer">
|
|
28
|
-
|
|
73
|
+
{{ t("emails.admin_invite.footer") }}
|
|
29
74
|
</EText>
|
|
30
75
|
</template>
|
|
31
76
|
</EmailLayout>
|
|
@@ -34,6 +34,13 @@ const DEFAULT_ERROR_CONFIG = {
|
|
|
34
34
|
|
|
35
35
|
const DEFAULT_STATUS_CODE = 500;
|
|
36
36
|
|
|
37
|
+
// Their cards already say what happened; the message behind them is the
|
|
38
|
+
// technical cause ("DMS backend request failed"), which the server has logged.
|
|
39
|
+
const STATUSES_WITHOUT_ERROR_MESSAGE = new Set([
|
|
40
|
+
HTTP_NOT_FOUND,
|
|
41
|
+
HTTP_FORBIDDEN,
|
|
42
|
+
]);
|
|
43
|
+
|
|
37
44
|
const props = defineProps<{
|
|
38
45
|
error: DmsErrorData;
|
|
39
46
|
}>();
|
|
@@ -45,6 +52,14 @@ const { loggedIn, reconcileSession, redirectToAuth } = useSessionRecovery();
|
|
|
45
52
|
|
|
46
53
|
const isRecovering = ref(false);
|
|
47
54
|
|
|
55
|
+
const visibleErrorMessage = computed(() =>
|
|
56
|
+
STATUSES_WITHOUT_ERROR_MESSAGE.has(
|
|
57
|
+
props.error.statusCode || DEFAULT_STATUS_CODE,
|
|
58
|
+
)
|
|
59
|
+
? undefined
|
|
60
|
+
: props.error.message,
|
|
61
|
+
);
|
|
62
|
+
|
|
48
63
|
const errorConfig = computed(() => {
|
|
49
64
|
const statusCode = props.error.statusCode || DEFAULT_STATUS_CODE;
|
|
50
65
|
const config = ERROR_CONFIGS[statusCode] || DEFAULT_ERROR_CONFIG;
|
|
@@ -121,10 +136,10 @@ onMounted(() => {
|
|
|
121
136
|
</p>
|
|
122
137
|
|
|
123
138
|
<p
|
|
124
|
-
v-if="!isRecovering &&
|
|
139
|
+
v-if="!isRecovering && visibleErrorMessage"
|
|
125
140
|
class="text-dimmed mb-6 text-sm"
|
|
126
141
|
>
|
|
127
|
-
{{
|
|
142
|
+
{{ visibleErrorMessage }}
|
|
128
143
|
</p>
|
|
129
144
|
|
|
130
145
|
<div v-if="!isRecovering" class="flex gap-3">
|
|
@@ -34,6 +34,7 @@ export default defineDmsPlugin(() => {
|
|
|
34
34
|
const { i18n, translate } = getPluginI18n();
|
|
35
35
|
const toast = useToast();
|
|
36
36
|
const { logout } = useLogout();
|
|
37
|
+
const { changeLanguage } = useUserLanguage();
|
|
37
38
|
const { copy } = useClipboard({ legacy: true });
|
|
38
39
|
|
|
39
40
|
const localeItems = (): CommandPaletteItem[] =>
|
|
@@ -41,7 +42,7 @@ export default defineDmsPlugin(() => {
|
|
|
41
42
|
label: locale.name ?? locale.code,
|
|
42
43
|
active: i18n.locale.value === locale.code,
|
|
43
44
|
onSelect: () => {
|
|
44
|
-
void
|
|
45
|
+
void changeLanguage(locale.code);
|
|
45
46
|
},
|
|
46
47
|
}));
|
|
47
48
|
|
|
@@ -65,9 +65,30 @@ export type SiteLayoutTree = Omit<PageInfo, "layoutUrl"> & {
|
|
|
65
65
|
layoutUrl?: string;
|
|
66
66
|
hasAccess?: boolean;
|
|
67
67
|
};
|
|
68
|
+
// Mirror of LayoutBannerVariant from @antelopejs/interface-dms/layout-banners.
|
|
69
|
+
export type LayoutBannerVariant = "info" | "warning" | "error";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Mirror of the backend's serialized layout banner. The payload only carries
|
|
73
|
+
* the banners whose visibility resolver passed for this request, so there is
|
|
74
|
+
* nothing left to decide here but the user's own dismissals.
|
|
75
|
+
*/
|
|
76
|
+
export interface LayoutBanner {
|
|
77
|
+
key: string;
|
|
78
|
+
variant: LayoutBannerVariant;
|
|
79
|
+
order: number;
|
|
80
|
+
dismissible: boolean;
|
|
81
|
+
icon?: string;
|
|
82
|
+
text?: string;
|
|
83
|
+
component?: string;
|
|
84
|
+
props?: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export type SiteLayout = {
|
|
69
88
|
pages: Record<string, PageInfo>;
|
|
70
89
|
categories: Record<string, CategoryInfo>;
|
|
90
|
+
/** Absent when the backend predates layout banners. */
|
|
91
|
+
banners?: LayoutBanner[];
|
|
71
92
|
};
|
|
72
93
|
|
|
73
94
|
export interface QuickActionCategoryInfo {
|
|
@@ -80,6 +101,7 @@ export interface QuickActionCategoryInfo {
|
|
|
80
101
|
export type QuickActionTarget =
|
|
81
102
|
| { type: "navigate"; to: string; query?: Record<string, string> }
|
|
82
103
|
| { type: "openForm"; to: string; component: string }
|
|
104
|
+
| { type: "button"; to: string; component: string; button: string }
|
|
83
105
|
| { type: "event"; name: string; payload?: unknown };
|
|
84
106
|
|
|
85
107
|
/**
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PageValidation } from "../types/page";
|
|
2
|
+
|
|
3
|
+
export interface BreadcrumbTargetSource {
|
|
4
|
+
layoutUrl?: string;
|
|
5
|
+
validation?: PageValidation;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The URL that opens the page at `path`. A page that declares
|
|
10
|
+
* `validation.requiredQueryParams` 404s without them, so the URL carries the
|
|
11
|
+
* values `currentQuery` holds for them — and only those — and there is no URL
|
|
12
|
+
* when any is missing, empty or repeated.
|
|
13
|
+
*/
|
|
14
|
+
export function buildPageTarget(
|
|
15
|
+
path: string,
|
|
16
|
+
validation: PageValidation | undefined,
|
|
17
|
+
currentQuery: Record<string, unknown>,
|
|
18
|
+
): string | undefined {
|
|
19
|
+
const requiredParams = validation?.requiredQueryParams ?? [];
|
|
20
|
+
const carriedQuery = new URLSearchParams();
|
|
21
|
+
|
|
22
|
+
for (const param of requiredParams) {
|
|
23
|
+
const value = currentQuery[param];
|
|
24
|
+
if (typeof value !== "string" || value === "") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
carriedQuery.set(param, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const search = carriedQuery.toString();
|
|
31
|
+
return search ? `${path}?${search}` : path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The link of a breadcrumb crumb: the page's {@link buildPageTarget}.
|
|
36
|
+
* Categories without a page behind them get no link.
|
|
37
|
+
*/
|
|
38
|
+
export function buildBreadcrumbTarget(
|
|
39
|
+
path: string,
|
|
40
|
+
metadata: BreadcrumbTargetSource,
|
|
41
|
+
currentQuery: Record<string, unknown>,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
if (!metadata.layoutUrl) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return buildPageTarget(path, metadata.validation, currentQuery);
|
|
48
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getQuery, parseURL } from "ufo";
|
|
2
|
+
import type { FavoritePage } from "../composables/favorites/useFavoritePages";
|
|
3
|
+
import type { PageValidation } from "../types/page";
|
|
4
|
+
import { buildPageTarget } from "./breadcrumb";
|
|
5
|
+
|
|
6
|
+
const FAVORITE_TITLE_SEPARATOR = " · ";
|
|
7
|
+
|
|
8
|
+
export interface FavoritePageMetadata {
|
|
9
|
+
id?: string;
|
|
10
|
+
displayName?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
validation?: PageValidation;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FavoritePageSource {
|
|
16
|
+
path: string;
|
|
17
|
+
metadata: FavoritePageMetadata;
|
|
18
|
+
query: Record<string, unknown>;
|
|
19
|
+
translate: (text: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type FavoriteRouteResolver = (
|
|
23
|
+
path: string,
|
|
24
|
+
) => { metadata: FavoritePageMetadata } | null;
|
|
25
|
+
|
|
26
|
+
function requiredQueryValues(
|
|
27
|
+
validation: PageValidation | undefined,
|
|
28
|
+
query: Record<string, unknown>,
|
|
29
|
+
): string[] {
|
|
30
|
+
return (validation?.requiredQueryParams ?? []).map((param) =>
|
|
31
|
+
String(query[param]),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A page narrowed by its query shows one entity out of many: its generic
|
|
36
|
+
// display name would label every favorite of it the same way.
|
|
37
|
+
function entityTitle(pageTitle: string, queryValues: string[]): string {
|
|
38
|
+
return [pageTitle, ...queryValues].join(FAVORITE_TITLE_SEPARATOR);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The favorite that reopens the page at `path` as it is showing now. A page
|
|
43
|
+
* requiring query parameters keeps them in its path and gets a title telling
|
|
44
|
+
* its entities apart; there is no favorite when a required value is missing.
|
|
45
|
+
*/
|
|
46
|
+
export function buildFavoritePage(
|
|
47
|
+
source: FavoritePageSource,
|
|
48
|
+
): FavoritePage | null {
|
|
49
|
+
const { path, metadata, query } = source;
|
|
50
|
+
const target = buildPageTarget(path, metadata.validation, query);
|
|
51
|
+
if (!target) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const displayName = metadata.displayName || path;
|
|
56
|
+
const queryValues = requiredQueryValues(metadata.validation, query);
|
|
57
|
+
if (queryValues.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
id: metadata.id || path,
|
|
60
|
+
path,
|
|
61
|
+
title: displayName,
|
|
62
|
+
icon: metadata.icon,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id: target,
|
|
68
|
+
path: target,
|
|
69
|
+
title: entityTitle(source.translate(displayName), queryValues),
|
|
70
|
+
icon: metadata.icon,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether a stored favorite still opens a page: its path matches one, and its
|
|
76
|
+
* query holds every value that page requires.
|
|
77
|
+
*/
|
|
78
|
+
export function isFavoritePathValid(
|
|
79
|
+
favoritePath: string,
|
|
80
|
+
resolveRoute: FavoriteRouteResolver,
|
|
81
|
+
): boolean {
|
|
82
|
+
const { pathname } = parseURL(favoritePath);
|
|
83
|
+
const route = resolveRoute(pathname);
|
|
84
|
+
if (!route) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
buildPageTarget(
|
|
89
|
+
pathname,
|
|
90
|
+
route.metadata.validation,
|
|
91
|
+
getQuery(favoritePath),
|
|
92
|
+
) !== undefined
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"soon": "Coming soon"
|
|
41
41
|
},
|
|
42
42
|
"quickActions": {
|
|
43
|
-
"title": "Quick actions"
|
|
43
|
+
"title": "Quick actions",
|
|
44
|
+
"category_members": "Members",
|
|
45
|
+
"invite_member": "Invite a member"
|
|
44
46
|
},
|
|
45
47
|
"commandPalette": {
|
|
46
48
|
"button": "Search…",
|
|
@@ -218,7 +220,8 @@
|
|
|
218
220
|
"cancel_confirm_description": "Are you sure you want to cancel this invitation? The invite link will no longer work."
|
|
219
221
|
},
|
|
220
222
|
"error": {
|
|
221
|
-
"not_found": "Invitation not found"
|
|
223
|
+
"not_found": "Invitation not found",
|
|
224
|
+
"resolved": "This invitation has already been accepted or cancelled"
|
|
222
225
|
}
|
|
223
226
|
},
|
|
224
227
|
"roles": {
|
|
@@ -269,5 +272,21 @@
|
|
|
269
272
|
"intro": "Browse the modules available on this platform.",
|
|
270
273
|
"empty": "No modules are accessible to you yet.",
|
|
271
274
|
"open": "Open module"
|
|
275
|
+
},
|
|
276
|
+
"emails": {
|
|
277
|
+
"admin_invite": {
|
|
278
|
+
"title": "You're Invited!",
|
|
279
|
+
"title_workspace": "You're invited to {workspace}",
|
|
280
|
+
"greeting": "Hello,",
|
|
281
|
+
"invitation": "You've been invited to join our platform.",
|
|
282
|
+
"invitation_to_target": "You've been invited to join {target}.",
|
|
283
|
+
"invitation_from_inviter": "{inviter} invited you to join {target}.",
|
|
284
|
+
"workspace_on_platform": "{workspace} on {platform}",
|
|
285
|
+
"call_to_action": "Click the button below to create your account:",
|
|
286
|
+
"button": "Create Account",
|
|
287
|
+
"expires_label": "This invitation expires in",
|
|
288
|
+
"expires_in": "{days} days",
|
|
289
|
+
"footer": "If you didn't expect this invitation, you can safely ignore this email."
|
|
290
|
+
}
|
|
272
291
|
}
|
|
273
292
|
}
|