@jskit-ai/workspaces-web 0.1.14 → 0.1.16

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 (48) hide show
  1. package/package.descriptor.mjs +61 -25
  2. package/package.json +13 -3
  3. package/src/client/account-settings/accountSettingsInvitesRuntime.js +88 -0
  4. package/src/client/account-settings/useAccountSettingsInvitesSectionRuntime.js +217 -0
  5. package/src/client/components/AccountSettingsInvitesSection.vue +72 -0
  6. package/src/client/components/MembersAdminClientElement.vue +400 -0
  7. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
  8. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
  9. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +89 -0
  10. package/src/client/components/UsersWorkspaceSelector.vue +242 -0
  11. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
  12. package/src/client/components/UsersWorkspaceToolsWidget.vue +12 -0
  13. package/src/client/components/WorkspaceMembersClientElement.vue +657 -0
  14. package/src/client/components/WorkspaceProfileClientElement.vue +116 -0
  15. package/src/client/components/WorkspaceSettingsClientElement.vue +102 -0
  16. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +265 -0
  17. package/src/client/components/WorkspacesClientElement.vue +540 -0
  18. package/src/client/composables/useBootstrapQuery.js +52 -0
  19. package/src/client/composables/useWorkspaceRouteContext.js +28 -0
  20. package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
  21. package/src/client/index.js +1 -0
  22. package/src/client/lib/bootstrap.js +59 -0
  23. package/src/client/lib/httpClient.js +10 -0
  24. package/src/client/lib/permissions.js +27 -0
  25. package/src/client/lib/profileSurfaceMenuLinks.js +163 -0
  26. package/src/client/lib/surfaceAccessPolicy.js +350 -0
  27. package/src/client/lib/theme.js +332 -0
  28. package/src/client/lib/workspaceLinkResolver.js +207 -0
  29. package/src/client/lib/workspaceSurfaceContext.js +82 -0
  30. package/src/client/lib/workspaceSurfacePaths.js +163 -0
  31. package/src/client/providers/WorkspacesWebClientProvider.js +59 -2
  32. package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
  33. package/src/client/runtime/bootstrapPlacementRuntime.js +463 -0
  34. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +28 -0
  35. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +147 -0
  36. package/src/client/support/realtimeWorkspace.js +21 -0
  37. package/src/client/support/runtimeNormalization.js +23 -0
  38. package/src/client/support/workspaceQueryKeys.js +15 -0
  39. package/src/shared/toolsOutletContracts.js +9 -0
  40. package/templates/src/pages/admin/members/index.vue +1 -1
  41. package/test/bootstrapPlacementRuntime.test.js +1095 -0
  42. package/test/exportsContract.test.js +2 -1
  43. package/test/profileSurfaceMenuLinks.test.js +208 -0
  44. package/test/settingsPlacementContract.test.js +19 -1
  45. package/test/surfaceAccessPolicy.test.js +129 -0
  46. package/test/theme.test.js +101 -0
  47. package/test/workspaceLinkResolver.test.js +61 -0
  48. package/test/workspaceSurfacePaths.test.js +39 -0
@@ -15,7 +15,8 @@ test("workspaces-web exports are explicit and aligned with production usage", ()
15
15
  packageId: "@jskit-ai/workspaces-web",
16
16
  requiredExports: [
17
17
  "./client",
18
- "./client/providers/WorkspacesWebClientProvider"
18
+ "./client/providers/WorkspacesWebClientProvider",
19
+ "./client/composables/useWorkspaceRouteContext"
19
20
  ]
20
21
  });
21
22
 
@@ -0,0 +1,208 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mdiShieldCrownOutline } from "@mdi/js";
4
+ import {
5
+ hasWorkspaceMembership,
6
+ resolvePrimarySurfaceSwitchLink,
7
+ resolveProfileSurfaceMenuLinks
8
+ } from "../src/client/lib/profileSurfaceMenuLinks.js";
9
+
10
+ function createPlacementContext({
11
+ workspace = null,
12
+ workspaces = [],
13
+ authenticated = true,
14
+ opsOwner = false
15
+ } = {}) {
16
+ return {
17
+ auth: {
18
+ authenticated
19
+ },
20
+ workspace,
21
+ workspaces,
22
+ permissions: [],
23
+ surfaceAccess: {
24
+ opsowner: opsOwner
25
+ },
26
+ surfaceAccessPolicies: {
27
+ public: {},
28
+ workspace_member: {
29
+ requireAuth: true,
30
+ requireWorkspaceMembership: true
31
+ },
32
+ ops_owner: {
33
+ requireAuth: true,
34
+ requireFlagsAll: ["ops_owner"]
35
+ }
36
+ },
37
+ surfaceConfig: {
38
+ tenancyMode: "workspaces",
39
+ defaultSurfaceId: "app",
40
+ enabledSurfaceIds: ["home", "app", "admin", "ops"],
41
+ surfacesById: {
42
+ home: {
43
+ id: "home",
44
+ enabled: true,
45
+ pagesRoot: "",
46
+ routeBase: "/",
47
+ requiresAuth: false,
48
+ requiresWorkspace: false,
49
+ accessPolicyId: "public"
50
+ },
51
+ app: {
52
+ id: "app",
53
+ enabled: true,
54
+ pagesRoot: "w/[workspaceSlug]",
55
+ routeBase: "/w/:workspaceSlug",
56
+ requiresAuth: true,
57
+ requiresWorkspace: true,
58
+ accessPolicyId: "workspace_member"
59
+ },
60
+ admin: {
61
+ id: "admin",
62
+ enabled: true,
63
+ pagesRoot: "w/[workspaceSlug]/admin",
64
+ routeBase: "/w/:workspaceSlug/admin",
65
+ requiresAuth: true,
66
+ requiresWorkspace: true,
67
+ accessPolicyId: "workspace_member"
68
+ },
69
+ ops: {
70
+ id: "ops",
71
+ enabled: true,
72
+ pagesRoot: "ops",
73
+ routeBase: "/ops",
74
+ requiresAuth: true,
75
+ requiresWorkspace: false,
76
+ accessPolicyId: "ops_owner",
77
+ icon: "mdi-cog-outline"
78
+ }
79
+ }
80
+ }
81
+ };
82
+ }
83
+
84
+ const originalWindow = globalThis.window;
85
+
86
+ test.afterEach(() => {
87
+ if (typeof originalWindow === "undefined") {
88
+ delete globalThis.window;
89
+ return;
90
+ }
91
+ globalThis.window = originalWindow;
92
+ });
93
+
94
+ test("hasWorkspaceMembership matches context.workspace slug", () => {
95
+ const context = createPlacementContext({
96
+ workspace: {
97
+ id: 1,
98
+ slug: "acme"
99
+ },
100
+ workspaces: []
101
+ });
102
+
103
+ assert.equal(hasWorkspaceMembership(context, "acme"), true);
104
+ });
105
+
106
+ test("hasWorkspaceMembership matches context.workspaces slug", () => {
107
+ const context = createPlacementContext({
108
+ workspace: null,
109
+ workspaces: [
110
+ {
111
+ id: 1,
112
+ slug: "acme"
113
+ }
114
+ ]
115
+ });
116
+
117
+ assert.equal(hasWorkspaceMembership(context, "acme"), true);
118
+ });
119
+
120
+ test("resolvePrimarySurfaceSwitchLink shows Go to admin only for member workspace", () => {
121
+ const context = createPlacementContext({
122
+ workspace: {
123
+ id: 1,
124
+ slug: "acme"
125
+ },
126
+ workspaces: [
127
+ {
128
+ id: 1,
129
+ slug: "acme"
130
+ }
131
+ ]
132
+ });
133
+
134
+ const link = resolvePrimarySurfaceSwitchLink({
135
+ context,
136
+ surface: "app"
137
+ });
138
+
139
+ assert.deepEqual(link, {
140
+ id: "surface-switch.admin",
141
+ label: "Go to admin",
142
+ icon: mdiShieldCrownOutline,
143
+ to: "/w/acme/admin"
144
+ });
145
+ });
146
+
147
+ test("resolvePrimarySurfaceSwitchLink hides workspace switch when slug is only in URL and user is not member", () => {
148
+ globalThis.window = {
149
+ location: {
150
+ pathname: "/w/acme/admin",
151
+ search: "",
152
+ hash: ""
153
+ }
154
+ };
155
+
156
+ const context = createPlacementContext({
157
+ workspace: null,
158
+ workspaces: []
159
+ });
160
+
161
+ const link = resolvePrimarySurfaceSwitchLink({
162
+ context,
163
+ surface: "admin"
164
+ });
165
+
166
+ assert.equal(link, null);
167
+ });
168
+
169
+ test("resolveProfileSurfaceMenuLinks includes gated non-workspace switches only for matching access flags", () => {
170
+ const nonOwnerContext = createPlacementContext({
171
+ workspace: {
172
+ id: 1,
173
+ slug: "acme"
174
+ },
175
+ workspaces: [
176
+ {
177
+ id: 1,
178
+ slug: "acme"
179
+ }
180
+ ],
181
+ opsOwner: false
182
+ });
183
+ const ownerContext = createPlacementContext({
184
+ workspace: {
185
+ id: 1,
186
+ slug: "acme"
187
+ },
188
+ workspaces: [
189
+ {
190
+ id: 1,
191
+ slug: "acme"
192
+ }
193
+ ],
194
+ opsOwner: true
195
+ });
196
+
197
+ const nonOwnerLinks = resolveProfileSurfaceMenuLinks({
198
+ context: nonOwnerContext,
199
+ surface: "app"
200
+ });
201
+ const ownerLinks = resolveProfileSurfaceMenuLinks({
202
+ context: ownerContext,
203
+ surface: "app"
204
+ });
205
+
206
+ assert.equal(nonOwnerLinks.some((entry) => entry.id === "surface-switch.ops"), false);
207
+ assert.equal(ownerLinks.some((entry) => entry.id === "surface-switch.ops"), true);
208
+ });
@@ -62,7 +62,25 @@ test("workspaces-web descriptor metadata advertises admin settings outlets", ()
62
62
  }
63
63
  ]
64
64
  );
65
- assert.equal(findContribution("users.workspace.settings.general"), null);
65
+ assert.equal(findContribution("workspaces.workspace.settings.general"), null);
66
+ assert.deepEqual(findContribution("workspaces.profile.menu.surface-switch"), {
67
+ id: "workspaces.profile.menu.surface-switch",
68
+ target: "auth-profile-menu:primary-menu",
69
+ surfaces: ["*"],
70
+ order: 100,
71
+ componentToken: "workspaces.web.profile.menu.surface-switch-item",
72
+ when: "auth.authenticated === true",
73
+ source: "mutations.text#workspaces-web-profile-surface-switch-placement"
74
+ });
75
+ assert.deepEqual(findContribution("workspaces.workspace.selector"), {
76
+ id: "workspaces.workspace.selector",
77
+ target: "shell-layout:top-left",
78
+ surfaces: ["*"],
79
+ order: 200,
80
+ componentToken: "workspaces.web.workspace.selector",
81
+ when: "auth.authenticated === true",
82
+ source: "mutations.text#workspaces-web-placement-block"
83
+ });
66
84
  assert.deepEqual(findFileMutation("users-web-page-admin-workspace-settings"), {
67
85
  from: "templates/src/pages/admin/workspace/settings/index.vue",
68
86
  toSurface: "admin",
@@ -0,0 +1,129 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { evaluateSurfaceAccess } from "../src/client/lib/surfaceAccessPolicy.js";
4
+
5
+ function createContext(overrides = {}) {
6
+ return {
7
+ auth: {
8
+ authenticated: true
9
+ },
10
+ workspaces: [
11
+ {
12
+ id: 1,
13
+ slug: "acme"
14
+ }
15
+ ],
16
+ permissions: [],
17
+ surfaceAccess: {
18
+ opsowner: false
19
+ },
20
+ surfaceAccessPolicies: {
21
+ public: {},
22
+ workspace_member: {
23
+ requireAuth: true,
24
+ requireWorkspaceMembership: true
25
+ },
26
+ ops_owner: {
27
+ requireAuth: true,
28
+ requireFlagsAll: ["ops_owner"]
29
+ }
30
+ },
31
+ surfaceConfig: {
32
+ defaultSurfaceId: "home",
33
+ enabledSurfaceIds: ["home", "app", "ops"],
34
+ surfacesById: {
35
+ home: {
36
+ id: "home",
37
+ enabled: true,
38
+ routeBase: "/home",
39
+ requiresWorkspace: false,
40
+ accessPolicyId: "public"
41
+ },
42
+ app: {
43
+ id: "app",
44
+ enabled: true,
45
+ routeBase: "/w/:workspaceSlug",
46
+ requiresWorkspace: true,
47
+ accessPolicyId: "workspace_member"
48
+ },
49
+ ops: {
50
+ id: "ops",
51
+ enabled: true,
52
+ routeBase: "/ops",
53
+ requiresWorkspace: false,
54
+ accessPolicyId: "ops_owner"
55
+ }
56
+ }
57
+ },
58
+ ...overrides
59
+ };
60
+ }
61
+
62
+ test("evaluateSurfaceAccess allows workspace member surfaces for accessible workspace", () => {
63
+ const decision = evaluateSurfaceAccess({
64
+ context: createContext(),
65
+ surfaceId: "app",
66
+ workspaceSlug: "acme"
67
+ });
68
+
69
+ assert.equal(decision.allowed, true);
70
+ });
71
+
72
+ test("evaluateSurfaceAccess denies workspace member surfaces for inaccessible workspace", () => {
73
+ const decision = evaluateSurfaceAccess({
74
+ context: createContext(),
75
+ surfaceId: "app",
76
+ workspaceSlug: "missing"
77
+ });
78
+
79
+ assert.equal(decision.allowed, false);
80
+ assert.equal(decision.reason, "surface-access-workspace-membership-required");
81
+ });
82
+
83
+ test("evaluateSurfaceAccess allows unknown workspace membership when allowOnUnknown=true", () => {
84
+ const context = createContext();
85
+ delete context.workspaces;
86
+
87
+ const decision = evaluateSurfaceAccess({
88
+ context,
89
+ surfaceId: "app",
90
+ workspaceSlug: "acme",
91
+ allowOnUnknown: true
92
+ });
93
+
94
+ assert.equal(decision.allowed, true);
95
+ assert.equal(decision.pending, true);
96
+ });
97
+
98
+ test("evaluateSurfaceAccess enforces bootstrap surface access flags", () => {
99
+ const deniedDecision = evaluateSurfaceAccess({
100
+ context: createContext(),
101
+ surfaceId: "ops"
102
+ });
103
+ assert.equal(deniedDecision.allowed, false);
104
+
105
+ const allowedDecision = evaluateSurfaceAccess({
106
+ context: createContext({
107
+ surfaceAccess: {
108
+ opsowner: true
109
+ }
110
+ }),
111
+ surfaceId: "ops"
112
+ });
113
+ assert.equal(allowedDecision.allowed, true);
114
+ });
115
+
116
+ test("evaluateSurfaceAccess denies workspace route with not_found status", () => {
117
+ const decision = evaluateSurfaceAccess({
118
+ context: createContext({
119
+ workspaceBootstrapStatuses: {
120
+ acme: "not_found"
121
+ }
122
+ }),
123
+ surfaceId: "app",
124
+ workspaceSlug: "acme"
125
+ });
126
+
127
+ assert.equal(decision.allowed, false);
128
+ assert.equal(decision.reason, "surface-access-workspace-not-found");
129
+ });
@@ -0,0 +1,101 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { ThemeSymbol } from "vuetify/lib/composables/theme.js";
4
+ import { resolveWorkspaceThemePalette } from "@jskit-ai/workspaces-core/shared/settings";
5
+ import {
6
+ hexColorToRgb,
7
+ setVuetifyPrimaryColorOverride,
8
+ setVuetifyThemeName
9
+ } from "../src/client/lib/theme.js";
10
+
11
+ function createVuetifyThemeController(initialTheme = "light") {
12
+ return {
13
+ global: {
14
+ name: {
15
+ value: initialTheme
16
+ }
17
+ },
18
+ themes: {
19
+ value: {
20
+ light: {
21
+ dark: false,
22
+ colors: {
23
+ primary: "#0f6b54",
24
+ secondary: "#3f5150",
25
+ background: "#eef3ee",
26
+ surface: "#f7fbf6",
27
+ "surface-variant": "#dfe8df"
28
+ }
29
+ },
30
+ dark: {
31
+ dark: true,
32
+ colors: {
33
+ primary: "#6fd0b5",
34
+ secondary: "#9db2af",
35
+ background: "#0f1715",
36
+ surface: "#16211e",
37
+ "surface-variant": "#253430"
38
+ }
39
+ }
40
+ }
41
+ },
42
+ _context: {
43
+ provides: {
44
+ [ThemeSymbol]: null
45
+ }
46
+ }
47
+ };
48
+ }
49
+
50
+ test("hexColorToRgb returns Vuetify rgb tuple and rejects invalid values", () => {
51
+ assert.equal(hexColorToRgb("#0f6b54"), "15,107,84");
52
+ assert.equal(hexColorToRgb("#CC3344"), "204,51,68");
53
+ assert.equal(hexColorToRgb("invalid"), "");
54
+ });
55
+
56
+ test("setVuetifyPrimaryColorOverride mutates workspace themes and restores base theme names", () => {
57
+ const themeController = createVuetifyThemeController("light");
58
+ const themeInput = {
59
+ lightPrimaryColor: "#CC3344",
60
+ lightSecondaryColor: "#884455",
61
+ lightSurfaceColor: "#F4F4F4",
62
+ lightSurfaceVariantColor: "#444444",
63
+ darkPrimaryColor: "#BB2233",
64
+ darkSecondaryColor: "#557799",
65
+ darkSurfaceColor: "#202020",
66
+ darkSurfaceVariantColor: "#A0A0A0"
67
+ };
68
+ const expectedLightPalette = resolveWorkspaceThemePalette(themeInput, {
69
+ mode: "light"
70
+ });
71
+ const expectedDarkPalette = resolveWorkspaceThemePalette(themeInput, {
72
+ mode: "dark"
73
+ });
74
+
75
+ assert.equal(setVuetifyPrimaryColorOverride(themeController, themeInput), true);
76
+ assert.equal(themeController.global.name.value, "workspace-light");
77
+ assert.equal(themeController.themes.value["workspace-light"].colors.primary, expectedLightPalette.color);
78
+ assert.equal(themeController.themes.value["workspace-light"].colors.secondary, expectedLightPalette.secondaryColor);
79
+ assert.equal(themeController.themes.value["workspace-light"].colors.surface, expectedLightPalette.surfaceColor);
80
+ assert.equal(
81
+ themeController.themes.value["workspace-light"].colors["surface-variant"],
82
+ expectedLightPalette.surfaceVariantColor
83
+ );
84
+
85
+ assert.equal(setVuetifyPrimaryColorOverride(themeController, themeInput), false);
86
+
87
+ assert.equal(setVuetifyThemeName(themeController, "dark"), true);
88
+ assert.equal(setVuetifyPrimaryColorOverride(themeController, themeInput), true);
89
+ assert.equal(themeController.global.name.value, "workspace-dark");
90
+ assert.equal(themeController.themes.value["workspace-dark"].colors.primary, expectedDarkPalette.color);
91
+ assert.equal(themeController.themes.value["workspace-dark"].colors.secondary, expectedDarkPalette.secondaryColor);
92
+ assert.equal(themeController.themes.value["workspace-dark"].colors.surface, expectedDarkPalette.surfaceColor);
93
+ assert.equal(
94
+ themeController.themes.value["workspace-dark"].colors["surface-variant"],
95
+ expectedDarkPalette.surfaceVariantColor
96
+ );
97
+
98
+ assert.equal(setVuetifyPrimaryColorOverride(themeController, null), true);
99
+ assert.equal(themeController.global.name.value, "dark");
100
+ assert.equal(setVuetifyPrimaryColorOverride(themeController, null), false);
101
+ });
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { resolveWorkspaceShellLinkPath } from "../src/client/lib/workspaceLinkResolver.js";
4
+
5
+ function createContext({ defaultSurfaceId = "dashboard", enabledSurfaceIds = ["dashboard"], surfacesById = {} } = {}) {
6
+ return {
7
+ surfaceConfig: {
8
+ defaultSurfaceId,
9
+ enabledSurfaceIds,
10
+ surfacesById
11
+ }
12
+ };
13
+ }
14
+
15
+ test("resolveWorkspaceShellLinkPath resolves workspace path for known workspace surfaces", () => {
16
+ const context = {
17
+ surfaceConfig: {
18
+ defaultSurfaceId: "dashboard",
19
+ enabledSurfaceIds: ["dashboard"],
20
+ surfacesById: {
21
+ dashboard: {
22
+ routeBase: "/dashboard",
23
+ requiresWorkspace: true
24
+ }
25
+ }
26
+ }
27
+ };
28
+
29
+ const path = resolveWorkspaceShellLinkPath({
30
+ context,
31
+ surface: "dashboard",
32
+ mode: "workspace",
33
+ workspaceSlug: "acme"
34
+ });
35
+
36
+ assert.equal(path, "/w/acme");
37
+ });
38
+
39
+ test("resolveWorkspaceShellLinkPath returns empty path for unknown non-default workspace surface", () => {
40
+ const context = createContext();
41
+ const path = resolveWorkspaceShellLinkPath({
42
+ context,
43
+ surface: "admin",
44
+ mode: "workspace",
45
+ workspaceSlug: "acme"
46
+ });
47
+
48
+ assert.equal(path, "");
49
+ });
50
+
51
+ test("resolveWorkspaceShellLinkPath returns empty path for unknown non-workspace surface ids", () => {
52
+ const context = createContext();
53
+ const path = resolveWorkspaceShellLinkPath({
54
+ context,
55
+ surface: "ops",
56
+ mode: "workspace",
57
+ workspaceSlug: "acme"
58
+ });
59
+
60
+ assert.equal(path, "");
61
+ });
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ resolveAccountSettingsPathFromPlacementContext,
6
+ resolveSurfaceWorkspacePathFromPlacementContext
7
+ } from "../src/client/lib/workspaceSurfacePaths.js";
8
+
9
+ test("resolveSurfaceWorkspacePathFromPlacementContext returns empty path without workspace slug", () => {
10
+ const context = {
11
+ surfaceConfig: {
12
+ defaultSurfaceId: "home",
13
+ enabledSurfaceIds: ["home", "app", "admin", "ops"],
14
+ surfacesById: {
15
+ home: { id: "home", routeBase: "/home", requiresWorkspace: false },
16
+ app: { id: "app", routeBase: "/w/:workspaceSlug", requiresWorkspace: true },
17
+ admin: { id: "admin", routeBase: "/w/:workspaceSlug/admin", requiresWorkspace: true },
18
+ ops: { id: "ops", routeBase: "/ops", requiresWorkspace: false }
19
+ }
20
+ }
21
+ };
22
+
23
+ assert.equal(resolveSurfaceWorkspacePathFromPlacementContext(context, "app", "", "/"), "");
24
+ });
25
+
26
+ test("resolveAccountSettingsPathFromPlacementContext uses account surface route when available", () => {
27
+ const context = {
28
+ surfaceConfig: {
29
+ defaultSurfaceId: "home",
30
+ enabledSurfaceIds: ["home", "account"],
31
+ surfacesById: {
32
+ home: { id: "home", routeBase: "/home", requiresWorkspace: false },
33
+ account: { id: "account", routeBase: "/profile", requiresWorkspace: false }
34
+ }
35
+ }
36
+ };
37
+
38
+ assert.equal(resolveAccountSettingsPathFromPlacementContext(context), "/profile");
39
+ });