@jskit-ai/users-web 0.1.4

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.
Files changed (97) hide show
  1. package/package.descriptor.mjs +507 -0
  2. package/package.json +31 -0
  3. package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
  4. package/src/client/components/MembersAdminClientElement.vue +404 -0
  5. package/src/client/components/ProfileClientElement.vue +242 -0
  6. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
  7. package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
  8. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
  9. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
  10. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
  11. package/src/client/components/UsersWorkspaceSelector.vue +237 -0
  12. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
  13. package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
  14. package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
  15. package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
  16. package/src/client/components/WorkspacesClientElement.vue +514 -0
  17. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
  18. package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
  19. package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
  20. package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
  21. package/src/client/composables/errorMessageHelpers.js +66 -0
  22. package/src/client/composables/internal/useOperationScope.js +144 -0
  23. package/src/client/composables/modelStateHelpers.js +49 -0
  24. package/src/client/composables/operationUiHelpers.js +121 -0
  25. package/src/client/composables/operationValidationHelpers.js +52 -0
  26. package/src/client/composables/refValueHelpers.js +19 -0
  27. package/src/client/composables/scopeHelpers.js +145 -0
  28. package/src/client/composables/useAccess.js +109 -0
  29. package/src/client/composables/useAccountSettingsRuntime.js +533 -0
  30. package/src/client/composables/useAddEdit.js +135 -0
  31. package/src/client/composables/useAddEditCore.js +137 -0
  32. package/src/client/composables/useBootstrapQuery.js +52 -0
  33. package/src/client/composables/useCommand.js +112 -0
  34. package/src/client/composables/useCommandCore.js +130 -0
  35. package/src/client/composables/useEndpointResource.js +104 -0
  36. package/src/client/composables/useFieldErrorBag.js +61 -0
  37. package/src/client/composables/useList.js +85 -0
  38. package/src/client/composables/useListCore.js +65 -0
  39. package/src/client/composables/usePagedCollection.js +125 -0
  40. package/src/client/composables/usePaths.js +108 -0
  41. package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
  42. package/src/client/composables/useScopeRuntime.js +107 -0
  43. package/src/client/composables/useSurfaceRouteContext.js +31 -0
  44. package/src/client/composables/useUiFeedback.js +96 -0
  45. package/src/client/composables/useView.js +89 -0
  46. package/src/client/composables/useViewCore.js +104 -0
  47. package/src/client/composables/useWorkspaceRouteContext.js +28 -0
  48. package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
  49. package/src/client/index.js +7 -0
  50. package/src/client/lib/bootstrap.js +95 -0
  51. package/src/client/lib/httpClient.js +67 -0
  52. package/src/client/lib/menuIcons.js +192 -0
  53. package/src/client/lib/permissions.js +34 -0
  54. package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
  55. package/src/client/lib/surfaceAccessPolicy.js +350 -0
  56. package/src/client/lib/theme.js +99 -0
  57. package/src/client/lib/workspaceLinkResolver.js +207 -0
  58. package/src/client/lib/workspaceSurfaceContext.js +82 -0
  59. package/src/client/lib/workspaceSurfacePaths.js +163 -0
  60. package/src/client/providers/UsersWebClientProvider.js +85 -0
  61. package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
  62. package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
  63. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
  64. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
  65. package/src/client/support/contractGuards.js +34 -0
  66. package/src/client/support/realtimeWorkspace.js +12 -0
  67. package/src/client/support/runtimeNormalization.js +27 -0
  68. package/src/client/support/workspaceQueryKeys.js +15 -0
  69. package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
  70. package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
  71. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
  72. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
  73. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
  74. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
  75. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
  76. package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
  77. package/templates/src/pages/account/index.vue +17 -0
  78. package/templates/src/pages/admin/members/index.vue +7 -0
  79. package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
  80. package/templates/src/pages/console/settings/index.vue +16 -0
  81. package/templates/src/surfaces/admin/index.vue +29 -0
  82. package/templates/src/surfaces/admin/root.vue +20 -0
  83. package/templates/src/surfaces/app/index.vue +27 -0
  84. package/templates/src/surfaces/app/root.vue +20 -0
  85. package/test/bootstrap.test.js +38 -0
  86. package/test/bootstrapPlacementRuntime.test.js +991 -0
  87. package/test/errorMessageHelpers.test.js +28 -0
  88. package/test/exportsContract.test.js +39 -0
  89. package/test/menuIcons.test.js +33 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/profileSurfaceMenuLinks.test.js +207 -0
  92. package/test/refValueHelpers.test.js +14 -0
  93. package/test/scopeHelpers.test.js +57 -0
  94. package/test/surfaceAccessPolicy.test.js +129 -0
  95. package/test/theme.test.js +95 -0
  96. package/test/workspaceLinkResolver.test.js +61 -0
  97. package/test/workspaceSurfacePaths.test.js +39 -0
@@ -0,0 +1,162 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useRoute } from "vue-router";
4
+ import { useQuery } from "@tanstack/vue-query";
5
+ import { mdiEmailAlertOutline } from "@mdi/js";
6
+ import { appendQueryString } from "@jskit-ai/kernel/shared/support";
7
+ import {
8
+ useWebPlacementContext,
9
+ resolveSurfaceDefinitionFromPlacementContext,
10
+ resolveSurfacePathFromPlacementContext,
11
+ resolveSurfaceNavigationTargetFromPlacementContext
12
+ } from "@jskit-ai/shell-web/client/placement";
13
+
14
+ const { context: placementContext } = useWebPlacementContext();
15
+ const route = useRoute();
16
+
17
+ function normalizePendingInvitesCount(value) {
18
+ const numeric = Number(value);
19
+ if (!Number.isInteger(numeric) || numeric < 1) {
20
+ return 0;
21
+ }
22
+ return numeric;
23
+ }
24
+
25
+ function resolveReturnTo() {
26
+ const fullPath = String(route?.fullPath || "").trim();
27
+ if (fullPath.startsWith("/") && !fullPath.startsWith("//")) {
28
+ return fullPath;
29
+ }
30
+ const path = String(route?.path || "").trim();
31
+ if (path.startsWith("/") && !path.startsWith("//")) {
32
+ return path;
33
+ }
34
+ return "/";
35
+ }
36
+
37
+ function resolveReturnToHref() {
38
+ if (typeof window === "object" && window?.location?.href) {
39
+ return String(window.location.href || "").trim() || resolveReturnTo();
40
+ }
41
+ return resolveReturnTo();
42
+ }
43
+
44
+ function countPendingInvites(entries = []) {
45
+ if (!Array.isArray(entries)) {
46
+ return 0;
47
+ }
48
+
49
+ let total = 0;
50
+ for (const entry of entries) {
51
+ if (!entry || typeof entry !== "object") {
52
+ continue;
53
+ }
54
+ total += 1;
55
+ }
56
+ return total;
57
+ }
58
+
59
+ const authenticated = computed(() => placementContext.value?.auth?.authenticated === true);
60
+
61
+ const bootstrapSummaryQuery = useQuery({
62
+ queryKey: ["local-main", "account", "invites-cue", "bootstrap"],
63
+ enabled: authenticated,
64
+ staleTime: 5_000,
65
+ refetchInterval: 15_000,
66
+ queryFn: async () => {
67
+ const response = await fetch("/api/bootstrap", {
68
+ method: "GET",
69
+ credentials: "include",
70
+ headers: {
71
+ accept: "application/json"
72
+ }
73
+ });
74
+ if (!response.ok) {
75
+ throw new Error(`Bootstrap request failed with status ${response.status}.`);
76
+ }
77
+
78
+ return response.json();
79
+ }
80
+ });
81
+
82
+ const placementPendingInvitesCount = computed(() =>
83
+ normalizePendingInvitesCount(placementContext.value?.pendingInvitesCount)
84
+ );
85
+ const bootstrapPendingInvitesCount = computed(() => {
86
+ const payload = bootstrapSummaryQuery.data.value;
87
+ const invitesEnabled = payload?.app?.features?.workspaceInvites === true;
88
+ if (!invitesEnabled) {
89
+ return 0;
90
+ }
91
+
92
+ return countPendingInvites(payload?.pendingInvites);
93
+ });
94
+ const pendingInvitesCount = computed(() =>
95
+ Math.max(placementPendingInvitesCount.value, bootstrapPendingInvitesCount.value)
96
+ );
97
+
98
+ const placementWorkspaceInvitesEnabled = computed(() => placementContext.value?.workspaceInvitesEnabled === true);
99
+ const bootstrapWorkspaceInvitesEnabled = computed(() => {
100
+ const payload = bootstrapSummaryQuery.data.value;
101
+ return payload?.app?.features?.workspaceInvites === true;
102
+ });
103
+ const workspaceInvitesEnabled = computed(
104
+ () => placementWorkspaceInvitesEnabled.value || bootstrapWorkspaceInvitesEnabled.value
105
+ );
106
+
107
+ const isVisible = computed(() => {
108
+ return (
109
+ authenticated.value &&
110
+ workspaceInvitesEnabled.value &&
111
+ pendingInvitesCount.value > 0
112
+ );
113
+ });
114
+
115
+ const resolvedTo = computed(() => {
116
+ const hasAccountSurface = Boolean(resolveSurfaceDefinitionFromPlacementContext(placementContext.value, "account"));
117
+ const accountSettingsPath = hasAccountSurface
118
+ ? resolveSurfacePathFromPlacementContext(placementContext.value, "account", "/")
119
+ : "/account";
120
+ const accountSettingsNavigation = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
121
+ path: accountSettingsPath,
122
+ surfaceId: "account"
123
+ });
124
+
125
+ const query = new URLSearchParams({
126
+ section: "invites",
127
+ returnTo: accountSettingsNavigation.sameOrigin ? resolveReturnTo() : resolveReturnToHref()
128
+ });
129
+ return appendQueryString(accountSettingsPath, query.toString());
130
+ });
131
+
132
+ const resolvedNavigationTarget = computed(() =>
133
+ resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
134
+ path: resolvedTo.value,
135
+ surfaceId: "account"
136
+ })
137
+ );
138
+ </script>
139
+
140
+ <template>
141
+ <v-badge
142
+ v-if="isVisible"
143
+ color="error"
144
+ :content="pendingInvitesCount"
145
+ :model-value="pendingInvitesCount > 0"
146
+ bordered
147
+ offset-x="6"
148
+ offset-y="8"
149
+ >
150
+ <v-btn
151
+ :to="resolvedNavigationTarget.sameOrigin ? resolvedNavigationTarget.href : undefined"
152
+ :href="resolvedNavigationTarget.sameOrigin ? undefined : resolvedNavigationTarget.href"
153
+ variant="tonal"
154
+ color="warning"
155
+ :prepend-icon="mdiEmailAlertOutline"
156
+ size="small"
157
+ class="text-none"
158
+ >
159
+ Invites
160
+ </v-btn>
161
+ </v-badge>
162
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+
4
+ const props = defineProps({
5
+ surfaceLabel: {
6
+ type: String,
7
+ default: "Workspace"
8
+ },
9
+ message: {
10
+ type: String,
11
+ default: "Workspace is currently unavailable."
12
+ }
13
+ });
14
+
15
+ const normalizedSurfaceLabel = computed(() => String(props.surfaceLabel || "").trim() || "Workspace");
16
+ const normalizedMessage = computed(() => String(props.message || "").trim() || "Workspace is currently unavailable.");
17
+ </script>
18
+
19
+ <template>
20
+ <v-card rounded="lg" elevation="1" border>
21
+ <v-card-item>
22
+ <template #prepend>
23
+ <v-icon icon="mdi-alert-circle-outline" color="error" />
24
+ </template>
25
+ <v-card-title class="text-h5">Unavailable</v-card-title>
26
+ <v-card-subtitle>{{ normalizedSurfaceLabel }} surface.</v-card-subtitle>
27
+ </v-card-item>
28
+ <v-divider />
29
+ <v-card-text class="d-flex flex-column ga-4">
30
+ <p class="text-medium-emphasis mb-0">{{ normalizedMessage }}</p>
31
+ </v-card-text>
32
+ </v-card>
33
+ </template>
@@ -0,0 +1,153 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useRoute, useRouter } from "vue-router";
4
+ import { useAccountSettingsRuntime } from "@jskit-ai/users-web/client/composables/useAccountSettingsRuntime";
5
+ import AccountSettingsProfileSection from "./AccountSettingsProfileSection.vue";
6
+ import AccountSettingsPreferencesSection from "./AccountSettingsPreferencesSection.vue";
7
+ import AccountSettingsNotificationsSection from "./AccountSettingsNotificationsSection.vue";
8
+ import AccountSettingsInvitesSection from "./AccountSettingsInvitesSection.vue";
9
+
10
+ const runtime = useAccountSettingsRuntime();
11
+ const route = useRoute();
12
+ const router = useRouter();
13
+
14
+ const sections = Object.freeze([
15
+ { title: "Profile", value: "profile" },
16
+ { title: "Preferences", value: "preferences" },
17
+ { title: "Notifications", value: "notifications" },
18
+ { title: "Invites", value: "invites" }
19
+ ]);
20
+ const sectionValues = new Set(sections.map((section) => section.value));
21
+
22
+ function normalizeSection(value) {
23
+ const source = Array.isArray(value) ? value[0] : value;
24
+ const normalized = String(source || "").trim().toLowerCase();
25
+ if (!sectionValues.has(normalized)) {
26
+ return "profile";
27
+ }
28
+ return normalized;
29
+ }
30
+
31
+ function readRouteSection() {
32
+ return normalizeSection(route?.query?.section);
33
+ }
34
+
35
+ const activeTab = computed({
36
+ get() {
37
+ return readRouteSection();
38
+ },
39
+ set(nextValue) {
40
+ const normalizedSection = normalizeSection(nextValue);
41
+ const currentSection = readRouteSection();
42
+ if (normalizedSection === currentSection) {
43
+ return;
44
+ }
45
+
46
+ const nextQuery = {
47
+ ...route.query
48
+ };
49
+ if (normalizedSection === "profile") {
50
+ delete nextQuery.section;
51
+ } else {
52
+ nextQuery.section = normalizedSection;
53
+ }
54
+
55
+ void router.replace({
56
+ query: nextQuery
57
+ });
58
+ }
59
+ });
60
+ </script>
61
+
62
+ <template>
63
+ <section class="settings-view py-2 py-md-4">
64
+ <v-card class="panel-card" rounded="lg" elevation="1" border>
65
+ <v-card-item>
66
+ <v-card-title class="panel-title">Account settings</v-card-title>
67
+ <v-card-subtitle>Global profile, preferences, notifications, and invitation controls.</v-card-subtitle>
68
+ <template #append>
69
+ <v-btn
70
+ variant="text"
71
+ color="secondary"
72
+ :to="runtime.backNavigationTarget.value.sameOrigin ? runtime.backNavigationTarget.value.href : undefined"
73
+ :href="runtime.backNavigationTarget.value.sameOrigin ? undefined : runtime.backNavigationTarget.value.href"
74
+ >
75
+ Back
76
+ </v-btn>
77
+ </template>
78
+ </v-card-item>
79
+ <v-divider />
80
+
81
+ <v-card-text class="pt-4">
82
+ <template v-if="runtime.loadingSettings.value">
83
+ <v-skeleton-loader type="text@2, list-item-two-line@4" class="mb-4" />
84
+ <v-skeleton-loader type="text@2, paragraph, button" />
85
+ </template>
86
+ <template v-else>
87
+ <v-progress-linear v-if="runtime.refreshingSettings.value" indeterminate class="mb-4" />
88
+ <v-row class="settings-layout" no-gutters>
89
+ <v-col cols="12" md="3" lg="2" class="pr-md-4 mb-4 mb-md-0">
90
+ <v-list nav density="comfortable" class="settings-section-list rounded-lg">
91
+ <v-list-item
92
+ v-for="section in sections"
93
+ :key="section.value"
94
+ :title="section.title"
95
+ :active="activeTab === section.value"
96
+ rounded="lg"
97
+ @click="activeTab = section.value"
98
+ />
99
+ </v-list>
100
+ </v-col>
101
+
102
+ <v-col cols="12" md="9" lg="10">
103
+ <v-window v-model="activeTab" :touch="false" class="settings-sections-window">
104
+ <v-window-item value="profile">
105
+ <AccountSettingsProfileSection :runtime="runtime" />
106
+ </v-window-item>
107
+
108
+ <v-window-item value="preferences">
109
+ <AccountSettingsPreferencesSection :runtime="runtime" />
110
+ </v-window-item>
111
+
112
+ <v-window-item value="notifications">
113
+ <AccountSettingsNotificationsSection :runtime="runtime" />
114
+ </v-window-item>
115
+
116
+ <v-window-item value="invites">
117
+ <AccountSettingsInvitesSection :runtime="runtime" />
118
+ </v-window-item>
119
+ </v-window>
120
+ </v-col>
121
+ </v-row>
122
+ </template>
123
+ </v-card-text>
124
+ </v-card>
125
+ </section>
126
+ </template>
127
+
128
+ <style scoped>
129
+ .panel-card {
130
+ background-color: rgb(var(--v-theme-surface));
131
+ }
132
+
133
+ .panel-title {
134
+ font-size: 1rem;
135
+ font-weight: 600;
136
+ letter-spacing: 0.01em;
137
+ }
138
+
139
+ .settings-section-list {
140
+ border: 1px solid rgba(var(--v-theme-outline), 0.35);
141
+ }
142
+
143
+ :deep(.settings-section-list .v-list-item--active) {
144
+ background-color: rgba(var(--v-theme-primary), 0.14);
145
+ }
146
+
147
+ :deep(.settings-sections-window .v-window-x-transition-enter-active),
148
+ :deep(.settings-sections-window .v-window-x-transition-leave-active),
149
+ :deep(.settings-sections-window .v-window-x-reverse-transition-enter-active),
150
+ :deep(.settings-sections-window .v-window-x-reverse-transition-leave-active) {
151
+ transition: none !important;
152
+ }
153
+ </style>
@@ -0,0 +1,77 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ runtime: {
4
+ type: Object,
5
+ required: true
6
+ }
7
+ });
8
+
9
+ const invites = props.runtime.invites;
10
+ </script>
11
+
12
+ <template>
13
+ <v-card rounded="lg" elevation="0" border>
14
+ <v-card-item>
15
+ <v-card-title class="text-subtitle-1">Invitations</v-card-title>
16
+ <v-card-subtitle>Accept or refuse workspace invitations.</v-card-subtitle>
17
+ </v-card-item>
18
+ <v-divider />
19
+ <v-card-text>
20
+ <template v-if="invites.isLoading.value">
21
+ <v-skeleton-loader type="text@2, list-item-two-line@3" />
22
+ </template>
23
+
24
+ <template v-else-if="invites.items.value.length < 1">
25
+ <v-progress-linear v-if="invites.isRefetching.value" indeterminate class="mb-4" />
26
+ <p class="text-body-2 text-medium-emphasis mb-0">No pending invitations.</p>
27
+ </template>
28
+
29
+ <template v-else>
30
+ <v-progress-linear v-if="invites.isRefetching.value" indeterminate class="mb-4" />
31
+ <v-list density="comfortable" class="pa-0">
32
+ <v-list-item
33
+ v-for="invite in invites.items.value"
34
+ :key="invite.id"
35
+ :title="invite.workspaceName"
36
+ :subtitle="`/${invite.workspaceSlug} • role: ${invite.roleId}`"
37
+ class="px-0"
38
+ >
39
+ <template #prepend>
40
+ <v-avatar color="warning" size="28">
41
+ <span class="text-caption font-weight-bold">?</span>
42
+ </v-avatar>
43
+ </template>
44
+ <template #append>
45
+ <div class="d-flex ga-2">
46
+ <v-btn
47
+ size="small"
48
+ variant="text"
49
+ color="error"
50
+ :loading="invites.action.value.token === invite.token && invites.action.value.decision === 'refuse'"
51
+ :disabled="
52
+ invites.isRefetching.value || (invites.isResolving.value && invites.action.value.token !== invite.token)
53
+ "
54
+ @click="invites.refuse(invite)"
55
+ >
56
+ Refuse
57
+ </v-btn>
58
+ <v-btn
59
+ size="small"
60
+ variant="tonal"
61
+ color="primary"
62
+ :loading="invites.action.value.token === invite.token && invites.action.value.decision === 'accept'"
63
+ :disabled="
64
+ invites.isRefetching.value || (invites.isResolving.value && invites.action.value.token !== invite.token)
65
+ "
66
+ @click="invites.accept(invite)"
67
+ >
68
+ Join
69
+ </v-btn>
70
+ </div>
71
+ </template>
72
+ </v-list-item>
73
+ </v-list>
74
+ </template>
75
+ </v-card-text>
76
+ </v-card>
77
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ runtime: {
4
+ type: Object,
5
+ required: true
6
+ }
7
+ });
8
+
9
+ const notifications = props.runtime.notifications;
10
+ </script>
11
+
12
+ <template>
13
+ <v-card rounded="lg" elevation="0" border>
14
+ <v-card-item>
15
+ <v-card-title class="text-subtitle-1">Notifications</v-card-title>
16
+ </v-card-item>
17
+ <v-divider />
18
+ <v-card-text>
19
+ <v-form @submit.prevent="notifications.submit" novalidate>
20
+ <v-switch
21
+ v-model="notifications.form.productUpdates"
22
+ label="Product updates"
23
+ color="primary"
24
+ hide-details
25
+ :disabled="notifications.isSaving.value || notifications.isRefreshing.value"
26
+ class="mb-2"
27
+ />
28
+ <v-switch
29
+ v-model="notifications.form.accountActivity"
30
+ label="Account activity alerts"
31
+ color="primary"
32
+ hide-details
33
+ :disabled="notifications.isSaving.value || notifications.isRefreshing.value"
34
+ class="mb-2"
35
+ />
36
+ <v-switch
37
+ v-model="notifications.form.securityAlerts"
38
+ label="Security alerts (required)"
39
+ color="primary"
40
+ hide-details
41
+ disabled
42
+ class="mb-4"
43
+ />
44
+ <v-btn
45
+ type="submit"
46
+ color="primary"
47
+ :loading="notifications.isSaving.value"
48
+ :disabled="notifications.isRefreshing.value"
49
+ >
50
+ Save notification settings
51
+ </v-btn>
52
+ </v-form>
53
+ </v-card-text>
54
+ </v-card>
55
+ </template>
@@ -0,0 +1,125 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ runtime: {
4
+ type: Object,
5
+ required: true
6
+ }
7
+ });
8
+
9
+ const preferences = props.runtime.preferences;
10
+ </script>
11
+
12
+ <template>
13
+ <v-card rounded="lg" elevation="0" border>
14
+ <v-card-item>
15
+ <v-card-title class="text-subtitle-1">Preferences</v-card-title>
16
+ </v-card-item>
17
+ <v-divider />
18
+ <v-card-text>
19
+ <v-form @submit.prevent="preferences.submit" novalidate>
20
+ <v-row>
21
+ <v-col cols="12" md="4">
22
+ <v-select
23
+ v-model="preferences.form.theme"
24
+ label="Theme"
25
+ :items="preferences.options.theme"
26
+ item-title="title"
27
+ item-value="value"
28
+ variant="outlined"
29
+ density="comfortable"
30
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
31
+ :error-messages="preferences.fieldErrors.theme ? [preferences.fieldErrors.theme] : []"
32
+ />
33
+ </v-col>
34
+
35
+ <v-col cols="12" md="4">
36
+ <v-select
37
+ v-model="preferences.form.locale"
38
+ label="Language / locale"
39
+ :items="preferences.options.locale"
40
+ item-title="title"
41
+ item-value="value"
42
+ variant="outlined"
43
+ density="comfortable"
44
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
45
+ :error-messages="preferences.fieldErrors.locale ? [preferences.fieldErrors.locale] : []"
46
+ />
47
+ </v-col>
48
+
49
+ <v-col cols="12" md="4">
50
+ <v-select
51
+ v-model="preferences.form.timeZone"
52
+ label="Time zone"
53
+ :items="preferences.options.timeZone"
54
+ variant="outlined"
55
+ density="comfortable"
56
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
57
+ :error-messages="preferences.fieldErrors.timeZone ? [preferences.fieldErrors.timeZone] : []"
58
+ />
59
+ </v-col>
60
+
61
+ <v-col cols="12" md="4">
62
+ <v-select
63
+ v-model="preferences.form.dateFormat"
64
+ label="Date format"
65
+ :items="preferences.options.dateFormat"
66
+ item-title="title"
67
+ item-value="value"
68
+ variant="outlined"
69
+ density="comfortable"
70
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
71
+ :error-messages="preferences.fieldErrors.dateFormat ? [preferences.fieldErrors.dateFormat] : []"
72
+ />
73
+ </v-col>
74
+
75
+ <v-col cols="12" md="4">
76
+ <v-select
77
+ v-model="preferences.form.numberFormat"
78
+ label="Number format"
79
+ :items="preferences.options.numberFormat"
80
+ item-title="title"
81
+ item-value="value"
82
+ variant="outlined"
83
+ density="comfortable"
84
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
85
+ :error-messages="preferences.fieldErrors.numberFormat ? [preferences.fieldErrors.numberFormat] : []"
86
+ />
87
+ </v-col>
88
+
89
+ <v-col cols="12" md="4">
90
+ <v-select
91
+ v-model="preferences.form.currencyCode"
92
+ label="Currency"
93
+ :items="preferences.options.currency"
94
+ variant="outlined"
95
+ density="comfortable"
96
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
97
+ :error-messages="preferences.fieldErrors.currencyCode ? [preferences.fieldErrors.currencyCode] : []"
98
+ />
99
+ </v-col>
100
+
101
+ <v-col cols="12" md="3">
102
+ <v-select
103
+ v-model.number="preferences.form.avatarSize"
104
+ label="Avatar size"
105
+ :items="preferences.options.avatarSize"
106
+ variant="outlined"
107
+ density="comfortable"
108
+ :disabled="preferences.isSaving.value || preferences.isRefreshing.value"
109
+ :error-messages="preferences.fieldErrors.avatarSize ? [preferences.fieldErrors.avatarSize] : []"
110
+ />
111
+ </v-col>
112
+ </v-row>
113
+
114
+ <v-btn
115
+ type="submit"
116
+ color="primary"
117
+ :loading="preferences.isSaving.value"
118
+ :disabled="preferences.isRefreshing.value"
119
+ >
120
+ Save preferences
121
+ </v-btn>
122
+ </v-form>
123
+ </v-card-text>
124
+ </v-card>
125
+ </template>