@jskit-ai/users-web 0.1.25 → 0.1.27

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-web",
4
- version: "0.1.25",
4
+ version: "0.1.27",
5
5
  description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
6
6
  dependsOn: [
7
7
  "@jskit-ai/http-runtime",
@@ -241,11 +241,11 @@ export default Object.freeze({
241
241
  "@uppy/dashboard": "^5.1.1",
242
242
  "@uppy/image-editor": "^4.2.0",
243
243
  "@uppy/xhr-upload": "^5.1.1",
244
- "@jskit-ai/http-runtime": "0.1.15",
245
- "@jskit-ai/realtime": "0.1.15",
246
- "@jskit-ai/kernel": "0.1.15",
247
- "@jskit-ai/shell-web": "0.1.15",
248
- "@jskit-ai/users-core": "0.1.20",
244
+ "@jskit-ai/http-runtime": "0.1.16",
245
+ "@jskit-ai/realtime": "0.1.16",
246
+ "@jskit-ai/kernel": "0.1.17",
247
+ "@jskit-ai/shell-web": "0.1.16",
248
+ "@jskit-ai/users-core": "0.1.23",
249
249
  "vuetify": "^4.0.0"
250
250
  },
251
251
  dev: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -21,11 +21,11 @@
21
21
  "dependencies": {
22
22
  "@tanstack/vue-query": "5.92.12",
23
23
  "@mdi/js": "^7.4.47",
24
- "@jskit-ai/users-core": "0.1.20",
25
- "@jskit-ai/realtime": "0.1.15",
26
- "@jskit-ai/http-runtime": "0.1.15",
27
- "@jskit-ai/kernel": "0.1.15",
28
- "@jskit-ai/shell-web": "0.1.15",
24
+ "@jskit-ai/users-core": "0.1.23",
25
+ "@jskit-ai/realtime": "0.1.16",
26
+ "@jskit-ai/http-runtime": "0.1.16",
27
+ "@jskit-ai/kernel": "0.1.17",
28
+ "@jskit-ai/shell-web": "0.1.16",
29
29
  "vuetify": "^4.0.0"
30
30
  }
31
31
  }
@@ -0,0 +1,132 @@
1
+ <template>
2
+ <v-card class="mb-4" rounded="lg" elevation="1" border>
3
+ <v-card-item>
4
+ <v-card-title class="text-h6">Workspace profile</v-card-title>
5
+ <v-card-subtitle>Name and avatar used across the workspace.</v-card-subtitle>
6
+ </v-card-item>
7
+ <v-divider />
8
+ <v-card-text class="pt-4">
9
+ <template v-if="showSkeleton">
10
+ <v-skeleton-loader type="text@2, list-item-two-line@3, button" />
11
+ </template>
12
+
13
+ <p v-else-if="addEdit.loadError" class="text-body-2 text-medium-emphasis mb-4">
14
+ {{ addEdit.loadError }}
15
+ </p>
16
+
17
+ <p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
18
+ You do not have permission to view workspace profile.
19
+ </p>
20
+
21
+ <template v-else>
22
+ <v-form @submit.prevent="addEdit.submit" novalidate>
23
+ <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
24
+ <v-row>
25
+ <v-col cols="12" md="6">
26
+ <v-text-field
27
+ v-model="workspaceProfileForm.name"
28
+ label="Workspace name"
29
+ variant="outlined"
30
+ density="comfortable"
31
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
32
+ :error-messages="addEdit.fieldErrors.name ? [addEdit.fieldErrors.name] : []"
33
+ />
34
+ </v-col>
35
+
36
+ <v-col cols="12" md="6">
37
+ <v-text-field
38
+ v-model="workspaceProfileForm.avatarUrl"
39
+ label="Workspace avatar URL"
40
+ variant="outlined"
41
+ density="comfortable"
42
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
43
+ placeholder="https://..."
44
+ hint="Optional"
45
+ persistent-hint
46
+ :error-messages="addEdit.fieldErrors.avatarUrl ? [addEdit.fieldErrors.avatarUrl] : []"
47
+ />
48
+ </v-col>
49
+
50
+ <v-col cols="12" md="3">
51
+ <v-text-field
52
+ v-model="workspaceProfileForm.color"
53
+ label="Accent color"
54
+ type="color"
55
+ variant="outlined"
56
+ density="comfortable"
57
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
58
+ :error-messages="addEdit.fieldErrors.color ? [addEdit.fieldErrors.color] : []"
59
+ />
60
+ </v-col>
61
+
62
+ <v-col cols="12" class="d-flex align-center justify-end ga-3">
63
+ <v-btn
64
+ v-if="addEdit.canSave"
65
+ type="submit"
66
+ color="primary"
67
+ :loading="addEdit.isSaving"
68
+ :disabled="addEdit.isInitialLoading || addEdit.isRefetching"
69
+ >
70
+ Save workspace profile
71
+ </v-btn>
72
+ <v-chip v-else color="secondary" label>Read-only</v-chip>
73
+ </v-col>
74
+ </v-row>
75
+ </v-form>
76
+ </template>
77
+ </v-card-text>
78
+ </v-card>
79
+ </template>
80
+
81
+ <script setup>
82
+ import { computed, reactive } from "vue";
83
+ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
84
+ import { workspaceResource } from "@jskit-ai/users-core/shared/resources/workspaceResource";
85
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
86
+ import { DEFAULT_WORKSPACE_LIGHT_PALETTE } from "@jskit-ai/users-core/shared/settings";
87
+ import { useAddEdit } from "../composables/useAddEdit.js";
88
+ import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
89
+
90
+ const emit = defineEmits(["saved"]);
91
+
92
+ const workspaceProfileForm = reactive({
93
+ name: "",
94
+ avatarUrl: "",
95
+ color: DEFAULT_WORKSPACE_LIGHT_PALETTE.color
96
+ });
97
+
98
+ const addEdit = useAddEdit({
99
+ ownershipFilter: USERS_ROUTE_VISIBILITY_WORKSPACE,
100
+ resource: workspaceResource,
101
+ apiSuffix: "",
102
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
103
+ buildWorkspaceQueryKey("profile", surfaceId, workspaceSlug),
104
+ viewPermissions: ["workspace.settings.view", "workspace.settings.update"],
105
+ savePermissions: ["workspace.settings.update"],
106
+ placementSource: "users-web.workspace-profile-view",
107
+ fallbackLoadError: "Unable to load workspace profile.",
108
+ fieldErrorKeys: ["name", "avatarUrl", "color"],
109
+ model: workspaceProfileForm,
110
+ parseInput: (rawPayload) =>
111
+ validateOperationSection({
112
+ operation: workspaceResource.operations.patch,
113
+ section: "bodyValidator",
114
+ value: rawPayload
115
+ }),
116
+ mapLoadedToModel: (model, payload = {}) => {
117
+ model.name = String(payload?.name || "");
118
+ model.avatarUrl = String(payload?.avatarUrl || "");
119
+ model.color = String(payload?.color || DEFAULT_WORKSPACE_LIGHT_PALETTE.color).toUpperCase();
120
+ },
121
+ buildRawPayload: (model) => ({
122
+ name: model.name,
123
+ avatarUrl: model.avatarUrl,
124
+ color: model.color
125
+ }),
126
+ onSaveSuccess: async () => {
127
+ emit("saved");
128
+ }
129
+ });
130
+
131
+ const showSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
132
+ </script>
@@ -1,249 +1,22 @@
1
1
  <template>
2
2
  <section class="workspace-settings-client-element">
3
- <v-card rounded="lg" elevation="1" border>
4
- <v-card-item>
5
- <v-card-title class="text-h6">Workspace settings</v-card-title>
6
- <v-card-subtitle>These values apply to everyone in this workspace.</v-card-subtitle>
7
- </v-card-item>
8
- <v-divider />
9
- <v-card-text class="pt-4">
10
- <template v-if="showFormSkeleton">
11
- <v-skeleton-loader type="text@2, list-item-two-line@4, button" />
12
- </template>
13
-
14
- <p v-else-if="addEdit.loadError" class="text-body-2 text-medium-emphasis mb-4">
15
- {{ addEdit.loadError }}
16
- </p>
17
-
18
- <p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
19
- You do not have permission to view workspace settings.
20
- </p>
21
-
22
- <template v-else>
23
- <v-form @submit.prevent="addEdit.submit" novalidate>
24
- <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
25
- <v-row>
26
- <v-col cols="12" md="6">
27
- <v-text-field
28
- v-model="workspaceForm.name"
29
- label="Workspace name"
30
- variant="outlined"
31
- density="comfortable"
32
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
33
- :error-messages="addEdit.fieldErrors.name ? [addEdit.fieldErrors.name] : []"
34
- />
35
- </v-col>
36
-
37
- <v-col cols="12" md="6">
38
- <v-text-field
39
- v-model="workspaceForm.avatarUrl"
40
- label="Workspace avatar URL"
41
- variant="outlined"
42
- density="comfortable"
43
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
44
- placeholder="https://..."
45
- hint="Optional"
46
- persistent-hint
47
- :error-messages="addEdit.fieldErrors.avatarUrl ? [addEdit.fieldErrors.avatarUrl] : []"
48
- />
49
- </v-col>
50
-
51
- <v-col cols="12">
52
- <div class="text-subtitle-2 mb-2">Theme colors</div>
53
- <v-row dense class="mb-2">
54
- <v-col cols="12">
55
- <div class="text-caption text-medium-emphasis mb-2">Light palette</div>
56
- </v-col>
57
- <v-col cols="12" md="3">
58
- <v-text-field
59
- v-model="workspaceForm.lightPrimaryColor"
60
- label="Primary"
61
- type="color"
62
- variant="outlined"
63
- density="comfortable"
64
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
65
- :error-messages="
66
- addEdit.fieldErrors.lightPrimaryColor ? [addEdit.fieldErrors.lightPrimaryColor] : []
67
- "
68
- />
69
- </v-col>
70
-
71
- <v-col cols="12" md="3">
72
- <v-text-field
73
- v-model="workspaceForm.lightSecondaryColor"
74
- label="Secondary"
75
- type="color"
76
- variant="outlined"
77
- density="comfortable"
78
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
79
- :error-messages="
80
- addEdit.fieldErrors.lightSecondaryColor ? [addEdit.fieldErrors.lightSecondaryColor] : []
81
- "
82
- />
83
- </v-col>
84
-
85
- <v-col cols="12" md="3">
86
- <v-text-field
87
- v-model="workspaceForm.lightSurfaceColor"
88
- label="Surface"
89
- type="color"
90
- variant="outlined"
91
- density="comfortable"
92
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
93
- :error-messages="
94
- addEdit.fieldErrors.lightSurfaceColor ? [addEdit.fieldErrors.lightSurfaceColor] : []
95
- "
96
- />
97
- </v-col>
98
-
99
- <v-col cols="12" md="3">
100
- <v-text-field
101
- v-model="workspaceForm.lightSurfaceVariantColor"
102
- label="Surface variant"
103
- type="color"
104
- variant="outlined"
105
- density="comfortable"
106
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
107
- :error-messages="
108
- addEdit.fieldErrors.lightSurfaceVariantColor
109
- ? [addEdit.fieldErrors.lightSurfaceVariantColor]
110
- : []
111
- "
112
- />
113
- </v-col>
114
- </v-row>
115
-
116
- <v-row dense>
117
- <v-col cols="12">
118
- <div class="text-caption text-medium-emphasis mb-2">Dark palette</div>
119
- </v-col>
120
- <v-col cols="12" md="3">
121
- <v-text-field
122
- v-model="workspaceForm.darkPrimaryColor"
123
- label="Primary"
124
- type="color"
125
- variant="outlined"
126
- density="comfortable"
127
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
128
- :error-messages="
129
- addEdit.fieldErrors.darkPrimaryColor ? [addEdit.fieldErrors.darkPrimaryColor] : []
130
- "
131
- />
132
- </v-col>
133
-
134
- <v-col cols="12" md="3">
135
- <v-text-field
136
- v-model="workspaceForm.darkSecondaryColor"
137
- label="Secondary"
138
- type="color"
139
- variant="outlined"
140
- density="comfortable"
141
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
142
- :error-messages="
143
- addEdit.fieldErrors.darkSecondaryColor ? [addEdit.fieldErrors.darkSecondaryColor] : []
144
- "
145
- />
146
- </v-col>
147
-
148
- <v-col cols="12" md="3">
149
- <v-text-field
150
- v-model="workspaceForm.darkSurfaceColor"
151
- label="Surface"
152
- type="color"
153
- variant="outlined"
154
- density="comfortable"
155
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
156
- :error-messages="
157
- addEdit.fieldErrors.darkSurfaceColor ? [addEdit.fieldErrors.darkSurfaceColor] : []
158
- "
159
- />
160
- </v-col>
161
-
162
- <v-col cols="12" md="3">
163
- <v-text-field
164
- v-model="workspaceForm.darkSurfaceVariantColor"
165
- label="Surface variant"
166
- type="color"
167
- variant="outlined"
168
- density="comfortable"
169
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
170
- :error-messages="
171
- addEdit.fieldErrors.darkSurfaceVariantColor
172
- ? [addEdit.fieldErrors.darkSurfaceVariantColor]
173
- : []
174
- "
175
- />
176
- </v-col>
177
- </v-row>
178
- </v-col>
179
-
180
- <v-col cols="12" md="6" class="d-flex align-center">
181
- <v-switch
182
- v-model="workspaceForm.invitesEnabled"
183
- color="primary"
184
- hide-details
185
- label="Enable invites"
186
- :disabled="!addEdit.canSave || !workspaceForm.invitesAvailable || addEdit.isSaving || addEdit.isRefetching"
187
- />
188
- </v-col>
189
-
190
- <v-col cols="12" class="d-flex align-center justify-end ga-3">
191
- <v-btn
192
- v-if="addEdit.canSave"
193
- type="submit"
194
- color="primary"
195
- :loading="addEdit.isSaving"
196
- :disabled="addEdit.isInitialLoading || addEdit.isRefetching"
197
- >
198
- Save workspace settings
199
- </v-btn>
200
- <v-chip v-else color="secondary" label>Read-only</v-chip>
201
- </v-col>
202
- </v-row>
203
- </v-form>
204
- </template>
205
- </v-card-text>
206
- </v-card>
3
+ <WorkspaceProfileClientElement @saved="handleFormSaved" />
4
+ <WorkspaceSettingsFieldsClientElement @saved="handleFormSaved" />
207
5
  </section>
208
6
  </template>
209
7
 
210
8
  <script setup>
211
- import { computed, reactive, watch } from "vue";
212
- import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
213
- import { workspaceSettingsResource } from "@jskit-ai/users-core/shared/resources/workspaceSettingsResource";
214
- import { WORKSPACE_SETTINGS_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
215
- import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
216
- import {
217
- DEFAULT_WORKSPACE_DARK_PALETTE,
218
- DEFAULT_WORKSPACE_LIGHT_PALETTE,
219
- resolveWorkspaceThemePalettes
220
- } from "@jskit-ai/users-core/shared/settings";
9
+ import { computed, watch } from "vue";
221
10
  import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
222
- import { useAddEdit } from "../composables/useAddEdit.js";
223
11
  import { useBootstrapQuery } from "../composables/useBootstrapQuery.js";
224
12
  import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
225
13
  import { findWorkspaceBySlug, normalizeWorkspaceList } from "../lib/bootstrap.js";
226
14
  import { arePermissionListsEqual, normalizePermissionList } from "../lib/permissions.js";
227
- import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
228
- import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
15
+ import WorkspaceProfileClientElement from "./WorkspaceProfileClientElement.vue";
16
+ import WorkspaceSettingsFieldsClientElement from "./WorkspaceSettingsFieldsClientElement.vue";
229
17
 
230
- const workspaceForm = reactive({
231
- name: "",
232
- lightPrimaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.color,
233
- lightSecondaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor,
234
- lightSurfaceColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor,
235
- lightSurfaceVariantColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor,
236
- darkPrimaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.color,
237
- darkSecondaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor,
238
- darkSurfaceColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor,
239
- darkSurfaceVariantColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor,
240
- avatarUrl: "",
241
- invitesEnabled: false,
242
- invitesAvailable: false
243
- });
244
18
  const routeContext = useWorkspaceRouteContext();
245
19
  const { context: placementContext, mergeContext: mergePlacementContext } = useWebPlacementContext();
246
- const OWNERSHIP_WORKSPACE = USERS_ROUTE_VISIBILITY_WORKSPACE;
247
20
  const bootstrapQuery = useBootstrapQuery({
248
21
  workspaceSlug: routeContext.workspaceSlugFromRoute,
249
22
  enabled: computed(() => Boolean(routeContext.workspaceSlugFromRoute.value))
@@ -258,6 +31,7 @@ function toWorkspaceEntrySnapshot(entry = null) {
258
31
  if (!normalizedEntry) {
259
32
  return "";
260
33
  }
34
+
261
35
  return JSON.stringify(normalizedEntry);
262
36
  }
263
37
 
@@ -270,19 +44,7 @@ function toWorkspaceSettingsSnapshot(settings = null) {
270
44
  return "";
271
45
  }
272
46
 
273
- const normalized = resolveWorkspaceThemePalettes(settings);
274
- return JSON.stringify({
275
- lightPrimaryColor: normalized.light.color,
276
- lightSecondaryColor: normalized.light.secondaryColor,
277
- lightSurfaceColor: normalized.light.surfaceColor,
278
- lightSurfaceVariantColor: normalized.light.surfaceVariantColor,
279
- darkPrimaryColor: normalized.dark.color,
280
- darkSecondaryColor: normalized.dark.secondaryColor,
281
- darkSurfaceColor: normalized.dark.surfaceColor,
282
- darkSurfaceVariantColor: normalized.dark.surfaceVariantColor,
283
- invitesEnabled: settings.invitesEnabled !== false,
284
- invitesAvailable: settings.invitesAvailable !== false
285
- });
47
+ return JSON.stringify(settings);
286
48
  }
287
49
 
288
50
  function applyShellWorkspaceContext(payload = {}) {
@@ -334,75 +96,7 @@ watch(
334
96
  { immediate: true }
335
97
  );
336
98
 
337
- const matchesWorkspaceRealtime = createWorkspaceRealtimeMatcher(routeContext.workspaceSlugFromRoute);
338
-
339
- const addEdit = useAddEdit({
340
- ownershipFilter: OWNERSHIP_WORKSPACE,
341
- resource: workspaceSettingsResource,
342
- apiSuffix: "/settings",
343
- queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
344
- buildWorkspaceQueryKey("settings", surfaceId, workspaceSlug),
345
- viewPermissions: ["workspace.settings.view", "workspace.settings.update"],
346
- savePermissions: ["workspace.settings.update"],
347
- placementSource: "users-web.workspace-settings-view",
348
- fallbackLoadError: "Unable to load workspace settings.",
349
- fieldErrorKeys: [
350
- "name",
351
- "avatarUrl",
352
- "lightPrimaryColor",
353
- "lightSecondaryColor",
354
- "lightSurfaceColor",
355
- "lightSurfaceVariantColor",
356
- "darkPrimaryColor",
357
- "darkSecondaryColor",
358
- "darkSurfaceColor",
359
- "darkSurfaceVariantColor"
360
- ],
361
- realtime: {
362
- event: WORKSPACE_SETTINGS_CHANGED_EVENT,
363
- matches: matchesWorkspaceRealtime
364
- },
365
- model: workspaceForm,
366
- parseInput: (rawPayload) =>
367
- validateOperationSection({
368
- operation: workspaceSettingsResource.operations.patch,
369
- section: "bodyValidator",
370
- value: rawPayload
371
- }),
372
- mapLoadedToModel: (model, payload = {}) => {
373
- const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
374
- const normalizedTheme = resolveWorkspaceThemePalettes(settings);
375
-
376
- model.name = String(settings.name || "");
377
- model.lightPrimaryColor = normalizedTheme.light.color;
378
- model.lightSecondaryColor = normalizedTheme.light.secondaryColor;
379
- model.lightSurfaceColor = normalizedTheme.light.surfaceColor;
380
- model.lightSurfaceVariantColor = normalizedTheme.light.surfaceVariantColor;
381
- model.darkPrimaryColor = normalizedTheme.dark.color;
382
- model.darkSecondaryColor = normalizedTheme.dark.secondaryColor;
383
- model.darkSurfaceColor = normalizedTheme.dark.surfaceColor;
384
- model.darkSurfaceVariantColor = normalizedTheme.dark.surfaceVariantColor;
385
- model.avatarUrl = String(settings.avatarUrl || "");
386
- model.invitesEnabled = settings.invitesEnabled !== false;
387
- model.invitesAvailable = settings.invitesAvailable !== false;
388
- },
389
- buildRawPayload: (model) => ({
390
- name: model.name,
391
- lightPrimaryColor: model.lightPrimaryColor,
392
- lightSecondaryColor: model.lightSecondaryColor,
393
- lightSurfaceColor: model.lightSurfaceColor,
394
- lightSurfaceVariantColor: model.lightSurfaceVariantColor,
395
- darkPrimaryColor: model.darkPrimaryColor,
396
- darkSecondaryColor: model.darkSecondaryColor,
397
- darkSurfaceColor: model.darkSurfaceColor,
398
- darkSurfaceVariantColor: model.darkSurfaceVariantColor,
399
- avatarUrl: model.avatarUrl,
400
- invitesEnabled: model.invitesEnabled
401
- }),
402
- onSaveSuccess: async () => {
403
- await bootstrapQuery.query.refetch();
404
- }
405
- });
406
-
407
- const showFormSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
99
+ async function handleFormSaved() {
100
+ await bootstrapQuery.query.refetch();
101
+ }
408
102
  </script>
@@ -0,0 +1,266 @@
1
+ <template>
2
+ <v-card rounded="lg" elevation="1" border>
3
+ <v-card-item>
4
+ <v-card-title class="text-h6">Workspace settings</v-card-title>
5
+ <v-card-subtitle>These values apply to everyone in this workspace.</v-card-subtitle>
6
+ </v-card-item>
7
+ <v-divider />
8
+ <v-card-text class="pt-4">
9
+ <template v-if="showSkeleton">
10
+ <v-skeleton-loader type="text@2, list-item-two-line@4, button" />
11
+ </template>
12
+
13
+ <p v-else-if="addEdit.loadError" class="text-body-2 text-medium-emphasis mb-4">
14
+ {{ addEdit.loadError }}
15
+ </p>
16
+
17
+ <p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
18
+ You do not have permission to view workspace settings.
19
+ </p>
20
+
21
+ <template v-else>
22
+ <v-form @submit.prevent="addEdit.submit" novalidate>
23
+ <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
24
+ <v-row>
25
+ <v-col cols="12">
26
+ <div class="text-subtitle-2 mb-2">Theme colors</div>
27
+ <v-row dense class="mb-2">
28
+ <v-col cols="12">
29
+ <div class="text-caption text-medium-emphasis mb-2">Light palette</div>
30
+ </v-col>
31
+ <v-col cols="12" md="3">
32
+ <v-text-field
33
+ v-model="workspaceSettingsForm.lightPrimaryColor"
34
+ label="Primary"
35
+ type="color"
36
+ variant="outlined"
37
+ density="comfortable"
38
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
39
+ :error-messages="addEdit.fieldErrors.lightPrimaryColor ? [addEdit.fieldErrors.lightPrimaryColor] : []"
40
+ />
41
+ </v-col>
42
+
43
+ <v-col cols="12" md="3">
44
+ <v-text-field
45
+ v-model="workspaceSettingsForm.lightSecondaryColor"
46
+ label="Secondary"
47
+ type="color"
48
+ variant="outlined"
49
+ density="comfortable"
50
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
51
+ :error-messages="
52
+ addEdit.fieldErrors.lightSecondaryColor ? [addEdit.fieldErrors.lightSecondaryColor] : []
53
+ "
54
+ />
55
+ </v-col>
56
+
57
+ <v-col cols="12" md="3">
58
+ <v-text-field
59
+ v-model="workspaceSettingsForm.lightSurfaceColor"
60
+ label="Surface"
61
+ type="color"
62
+ variant="outlined"
63
+ density="comfortable"
64
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
65
+ :error-messages="addEdit.fieldErrors.lightSurfaceColor ? [addEdit.fieldErrors.lightSurfaceColor] : []"
66
+ />
67
+ </v-col>
68
+
69
+ <v-col cols="12" md="3">
70
+ <v-text-field
71
+ v-model="workspaceSettingsForm.lightSurfaceVariantColor"
72
+ label="Surface variant"
73
+ type="color"
74
+ variant="outlined"
75
+ density="comfortable"
76
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
77
+ :error-messages="
78
+ addEdit.fieldErrors.lightSurfaceVariantColor ? [addEdit.fieldErrors.lightSurfaceVariantColor] : []
79
+ "
80
+ />
81
+ </v-col>
82
+ </v-row>
83
+
84
+ <v-row dense>
85
+ <v-col cols="12">
86
+ <div class="text-caption text-medium-emphasis mb-2">Dark palette</div>
87
+ </v-col>
88
+ <v-col cols="12" md="3">
89
+ <v-text-field
90
+ v-model="workspaceSettingsForm.darkPrimaryColor"
91
+ label="Primary"
92
+ type="color"
93
+ variant="outlined"
94
+ density="comfortable"
95
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
96
+ :error-messages="addEdit.fieldErrors.darkPrimaryColor ? [addEdit.fieldErrors.darkPrimaryColor] : []"
97
+ />
98
+ </v-col>
99
+
100
+ <v-col cols="12" md="3">
101
+ <v-text-field
102
+ v-model="workspaceSettingsForm.darkSecondaryColor"
103
+ label="Secondary"
104
+ type="color"
105
+ variant="outlined"
106
+ density="comfortable"
107
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
108
+ :error-messages="addEdit.fieldErrors.darkSecondaryColor ? [addEdit.fieldErrors.darkSecondaryColor] : []"
109
+ />
110
+ </v-col>
111
+
112
+ <v-col cols="12" md="3">
113
+ <v-text-field
114
+ v-model="workspaceSettingsForm.darkSurfaceColor"
115
+ label="Surface"
116
+ type="color"
117
+ variant="outlined"
118
+ density="comfortable"
119
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
120
+ :error-messages="addEdit.fieldErrors.darkSurfaceColor ? [addEdit.fieldErrors.darkSurfaceColor] : []"
121
+ />
122
+ </v-col>
123
+
124
+ <v-col cols="12" md="3">
125
+ <v-text-field
126
+ v-model="workspaceSettingsForm.darkSurfaceVariantColor"
127
+ label="Surface variant"
128
+ type="color"
129
+ variant="outlined"
130
+ density="comfortable"
131
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
132
+ :error-messages="
133
+ addEdit.fieldErrors.darkSurfaceVariantColor ? [addEdit.fieldErrors.darkSurfaceVariantColor] : []
134
+ "
135
+ />
136
+ </v-col>
137
+ </v-row>
138
+ </v-col>
139
+
140
+ <v-col cols="12" md="6" class="d-flex align-center">
141
+ <v-switch
142
+ v-model="workspaceSettingsForm.invitesEnabled"
143
+ color="primary"
144
+ hide-details
145
+ label="Enable invites"
146
+ :disabled="!addEdit.canSave || !workspaceSettingsForm.invitesAvailable || addEdit.isSaving || addEdit.isRefetching"
147
+ />
148
+ </v-col>
149
+
150
+ <v-col cols="12" class="d-flex align-center justify-end ga-3">
151
+ <v-btn
152
+ v-if="addEdit.canSave"
153
+ type="submit"
154
+ color="primary"
155
+ :loading="addEdit.isSaving"
156
+ :disabled="addEdit.isInitialLoading || addEdit.isRefetching"
157
+ >
158
+ Save workspace settings
159
+ </v-btn>
160
+ <v-chip v-else color="secondary" label>Read-only</v-chip>
161
+ </v-col>
162
+ </v-row>
163
+ </v-form>
164
+ </template>
165
+ </v-card-text>
166
+ </v-card>
167
+ </template>
168
+
169
+ <script setup>
170
+ import { computed, reactive } from "vue";
171
+ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
172
+ import { workspaceSettingsResource } from "@jskit-ai/users-core/shared/resources/workspaceSettingsResource";
173
+ import { WORKSPACE_SETTINGS_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
174
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
175
+ import {
176
+ DEFAULT_WORKSPACE_DARK_PALETTE,
177
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
178
+ resolveWorkspaceThemePalettes
179
+ } from "@jskit-ai/users-core/shared/settings";
180
+ import { useAddEdit } from "../composables/useAddEdit.js";
181
+ import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
182
+ import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
183
+ import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
184
+
185
+ const emit = defineEmits(["saved"]);
186
+
187
+ const workspaceSettingsForm = reactive({
188
+ lightPrimaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.color,
189
+ lightSecondaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor,
190
+ lightSurfaceColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor,
191
+ lightSurfaceVariantColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor,
192
+ darkPrimaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.color,
193
+ darkSecondaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor,
194
+ darkSurfaceColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor,
195
+ darkSurfaceVariantColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor,
196
+ invitesEnabled: false,
197
+ invitesAvailable: false
198
+ });
199
+
200
+ const routeContext = useWorkspaceRouteContext();
201
+ const matchesWorkspaceRealtime = createWorkspaceRealtimeMatcher(routeContext.workspaceSlugFromRoute);
202
+
203
+ const addEdit = useAddEdit({
204
+ ownershipFilter: USERS_ROUTE_VISIBILITY_WORKSPACE,
205
+ resource: workspaceSettingsResource,
206
+ apiSuffix: "/settings",
207
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
208
+ buildWorkspaceQueryKey("settings", surfaceId, workspaceSlug),
209
+ viewPermissions: ["workspace.settings.view", "workspace.settings.update"],
210
+ savePermissions: ["workspace.settings.update"],
211
+ placementSource: "users-web.workspace-settings-view",
212
+ fallbackLoadError: "Unable to load workspace settings.",
213
+ fieldErrorKeys: [
214
+ "lightPrimaryColor",
215
+ "lightSecondaryColor",
216
+ "lightSurfaceColor",
217
+ "lightSurfaceVariantColor",
218
+ "darkPrimaryColor",
219
+ "darkSecondaryColor",
220
+ "darkSurfaceColor",
221
+ "darkSurfaceVariantColor"
222
+ ],
223
+ realtime: {
224
+ event: WORKSPACE_SETTINGS_CHANGED_EVENT,
225
+ matches: matchesWorkspaceRealtime
226
+ },
227
+ model: workspaceSettingsForm,
228
+ parseInput: (rawPayload) =>
229
+ validateOperationSection({
230
+ operation: workspaceSettingsResource.operations.patch,
231
+ section: "bodyValidator",
232
+ value: rawPayload
233
+ }),
234
+ mapLoadedToModel: (model, payload = {}) => {
235
+ const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
236
+ const normalizedTheme = resolveWorkspaceThemePalettes(settings);
237
+
238
+ model.lightPrimaryColor = normalizedTheme.light.color;
239
+ model.lightSecondaryColor = normalizedTheme.light.secondaryColor;
240
+ model.lightSurfaceColor = normalizedTheme.light.surfaceColor;
241
+ model.lightSurfaceVariantColor = normalizedTheme.light.surfaceVariantColor;
242
+ model.darkPrimaryColor = normalizedTheme.dark.color;
243
+ model.darkSecondaryColor = normalizedTheme.dark.secondaryColor;
244
+ model.darkSurfaceColor = normalizedTheme.dark.surfaceColor;
245
+ model.darkSurfaceVariantColor = normalizedTheme.dark.surfaceVariantColor;
246
+ model.invitesEnabled = settings.invitesEnabled !== false;
247
+ model.invitesAvailable = settings.invitesAvailable !== false;
248
+ },
249
+ buildRawPayload: (model) => ({
250
+ lightPrimaryColor: model.lightPrimaryColor,
251
+ lightSecondaryColor: model.lightSecondaryColor,
252
+ lightSurfaceColor: model.lightSurfaceColor,
253
+ lightSurfaceVariantColor: model.lightSurfaceVariantColor,
254
+ darkPrimaryColor: model.darkPrimaryColor,
255
+ darkSecondaryColor: model.darkSecondaryColor,
256
+ darkSurfaceColor: model.darkSurfaceColor,
257
+ darkSurfaceVariantColor: model.darkSurfaceVariantColor,
258
+ invitesEnabled: model.invitesEnabled
259
+ }),
260
+ onSaveSuccess: async () => {
261
+ emit("saved");
262
+ }
263
+ });
264
+
265
+ const showSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
266
+ </script>