@jskit-ai/users-web 0.1.26 → 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.26",
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.16",
247
- "@jskit-ai/shell-web": "0.1.15",
248
- "@jskit-ai/users-core": "0.1.21",
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.26",
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.21",
25
- "@jskit-ai/realtime": "0.1.15",
26
- "@jskit-ai/http-runtime": "0.1.15",
27
- "@jskit-ai/kernel": "0.1.16",
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,237 +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.avatarUrl"
29
- label="Workspace avatar URL"
30
- variant="outlined"
31
- density="comfortable"
32
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
33
- placeholder="https://..."
34
- hint="Optional"
35
- persistent-hint
36
- :error-messages="addEdit.fieldErrors.avatarUrl ? [addEdit.fieldErrors.avatarUrl] : []"
37
- />
38
- </v-col>
39
-
40
- <v-col cols="12">
41
- <div class="text-subtitle-2 mb-2">Theme colors</div>
42
- <v-row dense class="mb-2">
43
- <v-col cols="12">
44
- <div class="text-caption text-medium-emphasis mb-2">Light palette</div>
45
- </v-col>
46
- <v-col cols="12" md="3">
47
- <v-text-field
48
- v-model="workspaceForm.lightPrimaryColor"
49
- label="Primary"
50
- type="color"
51
- variant="outlined"
52
- density="comfortable"
53
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
54
- :error-messages="
55
- addEdit.fieldErrors.lightPrimaryColor ? [addEdit.fieldErrors.lightPrimaryColor] : []
56
- "
57
- />
58
- </v-col>
59
-
60
- <v-col cols="12" md="3">
61
- <v-text-field
62
- v-model="workspaceForm.lightSecondaryColor"
63
- label="Secondary"
64
- type="color"
65
- variant="outlined"
66
- density="comfortable"
67
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
68
- :error-messages="
69
- addEdit.fieldErrors.lightSecondaryColor ? [addEdit.fieldErrors.lightSecondaryColor] : []
70
- "
71
- />
72
- </v-col>
73
-
74
- <v-col cols="12" md="3">
75
- <v-text-field
76
- v-model="workspaceForm.lightSurfaceColor"
77
- label="Surface"
78
- type="color"
79
- variant="outlined"
80
- density="comfortable"
81
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
82
- :error-messages="
83
- addEdit.fieldErrors.lightSurfaceColor ? [addEdit.fieldErrors.lightSurfaceColor] : []
84
- "
85
- />
86
- </v-col>
87
-
88
- <v-col cols="12" md="3">
89
- <v-text-field
90
- v-model="workspaceForm.lightSurfaceVariantColor"
91
- label="Surface variant"
92
- type="color"
93
- variant="outlined"
94
- density="comfortable"
95
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
96
- :error-messages="
97
- addEdit.fieldErrors.lightSurfaceVariantColor
98
- ? [addEdit.fieldErrors.lightSurfaceVariantColor]
99
- : []
100
- "
101
- />
102
- </v-col>
103
- </v-row>
104
-
105
- <v-row dense>
106
- <v-col cols="12">
107
- <div class="text-caption text-medium-emphasis mb-2">Dark palette</div>
108
- </v-col>
109
- <v-col cols="12" md="3">
110
- <v-text-field
111
- v-model="workspaceForm.darkPrimaryColor"
112
- label="Primary"
113
- type="color"
114
- variant="outlined"
115
- density="comfortable"
116
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
117
- :error-messages="
118
- addEdit.fieldErrors.darkPrimaryColor ? [addEdit.fieldErrors.darkPrimaryColor] : []
119
- "
120
- />
121
- </v-col>
122
-
123
- <v-col cols="12" md="3">
124
- <v-text-field
125
- v-model="workspaceForm.darkSecondaryColor"
126
- label="Secondary"
127
- type="color"
128
- variant="outlined"
129
- density="comfortable"
130
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
131
- :error-messages="
132
- addEdit.fieldErrors.darkSecondaryColor ? [addEdit.fieldErrors.darkSecondaryColor] : []
133
- "
134
- />
135
- </v-col>
136
-
137
- <v-col cols="12" md="3">
138
- <v-text-field
139
- v-model="workspaceForm.darkSurfaceColor"
140
- label="Surface"
141
- type="color"
142
- variant="outlined"
143
- density="comfortable"
144
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
145
- :error-messages="
146
- addEdit.fieldErrors.darkSurfaceColor ? [addEdit.fieldErrors.darkSurfaceColor] : []
147
- "
148
- />
149
- </v-col>
150
-
151
- <v-col cols="12" md="3">
152
- <v-text-field
153
- v-model="workspaceForm.darkSurfaceVariantColor"
154
- label="Surface variant"
155
- type="color"
156
- variant="outlined"
157
- density="comfortable"
158
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
159
- :error-messages="
160
- addEdit.fieldErrors.darkSurfaceVariantColor
161
- ? [addEdit.fieldErrors.darkSurfaceVariantColor]
162
- : []
163
- "
164
- />
165
- </v-col>
166
- </v-row>
167
- </v-col>
168
-
169
- <v-col cols="12" md="6" class="d-flex align-center">
170
- <v-switch
171
- v-model="workspaceForm.invitesEnabled"
172
- color="primary"
173
- hide-details
174
- label="Enable invites"
175
- :disabled="!addEdit.canSave || !workspaceForm.invitesAvailable || addEdit.isSaving || addEdit.isRefetching"
176
- />
177
- </v-col>
178
-
179
- <v-col cols="12" class="d-flex align-center justify-end ga-3">
180
- <v-btn
181
- v-if="addEdit.canSave"
182
- type="submit"
183
- color="primary"
184
- :loading="addEdit.isSaving"
185
- :disabled="addEdit.isInitialLoading || addEdit.isRefetching"
186
- >
187
- Save workspace settings
188
- </v-btn>
189
- <v-chip v-else color="secondary" label>Read-only</v-chip>
190
- </v-col>
191
- </v-row>
192
- </v-form>
193
- </template>
194
- </v-card-text>
195
- </v-card>
3
+ <WorkspaceProfileClientElement @saved="handleFormSaved" />
4
+ <WorkspaceSettingsFieldsClientElement @saved="handleFormSaved" />
196
5
  </section>
197
6
  </template>
198
7
 
199
8
  <script setup>
200
- import { computed, reactive, watch } from "vue";
201
- import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
202
- import { workspaceSettingsResource } from "@jskit-ai/users-core/shared/resources/workspaceSettingsResource";
203
- import { WORKSPACE_SETTINGS_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
204
- import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
205
- import {
206
- DEFAULT_WORKSPACE_DARK_PALETTE,
207
- DEFAULT_WORKSPACE_LIGHT_PALETTE,
208
- resolveWorkspaceThemePalettes
209
- } from "@jskit-ai/users-core/shared/settings";
9
+ import { computed, watch } from "vue";
210
10
  import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
211
- import { useAddEdit } from "../composables/useAddEdit.js";
212
11
  import { useBootstrapQuery } from "../composables/useBootstrapQuery.js";
213
12
  import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
214
13
  import { findWorkspaceBySlug, normalizeWorkspaceList } from "../lib/bootstrap.js";
215
14
  import { arePermissionListsEqual, normalizePermissionList } from "../lib/permissions.js";
216
- import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
217
- import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
15
+ import WorkspaceProfileClientElement from "./WorkspaceProfileClientElement.vue";
16
+ import WorkspaceSettingsFieldsClientElement from "./WorkspaceSettingsFieldsClientElement.vue";
218
17
 
219
- const workspaceForm = reactive({
220
- lightPrimaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.color,
221
- lightSecondaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor,
222
- lightSurfaceColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor,
223
- lightSurfaceVariantColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor,
224
- darkPrimaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.color,
225
- darkSecondaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor,
226
- darkSurfaceColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor,
227
- darkSurfaceVariantColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor,
228
- avatarUrl: "",
229
- invitesEnabled: false,
230
- invitesAvailable: false
231
- });
232
18
  const routeContext = useWorkspaceRouteContext();
233
19
  const { context: placementContext, mergeContext: mergePlacementContext } = useWebPlacementContext();
234
- const OWNERSHIP_WORKSPACE = USERS_ROUTE_VISIBILITY_WORKSPACE;
235
20
  const bootstrapQuery = useBootstrapQuery({
236
21
  workspaceSlug: routeContext.workspaceSlugFromRoute,
237
22
  enabled: computed(() => Boolean(routeContext.workspaceSlugFromRoute.value))
@@ -246,6 +31,7 @@ function toWorkspaceEntrySnapshot(entry = null) {
246
31
  if (!normalizedEntry) {
247
32
  return "";
248
33
  }
34
+
249
35
  return JSON.stringify(normalizedEntry);
250
36
  }
251
37
 
@@ -258,19 +44,7 @@ function toWorkspaceSettingsSnapshot(settings = null) {
258
44
  return "";
259
45
  }
260
46
 
261
- const normalized = resolveWorkspaceThemePalettes(settings);
262
- return JSON.stringify({
263
- lightPrimaryColor: normalized.light.color,
264
- lightSecondaryColor: normalized.light.secondaryColor,
265
- lightSurfaceColor: normalized.light.surfaceColor,
266
- lightSurfaceVariantColor: normalized.light.surfaceVariantColor,
267
- darkPrimaryColor: normalized.dark.color,
268
- darkSecondaryColor: normalized.dark.secondaryColor,
269
- darkSurfaceColor: normalized.dark.surfaceColor,
270
- darkSurfaceVariantColor: normalized.dark.surfaceVariantColor,
271
- invitesEnabled: settings.invitesEnabled !== false,
272
- invitesAvailable: settings.invitesAvailable !== false
273
- });
47
+ return JSON.stringify(settings);
274
48
  }
275
49
 
276
50
  function applyShellWorkspaceContext(payload = {}) {
@@ -322,72 +96,7 @@ watch(
322
96
  { immediate: true }
323
97
  );
324
98
 
325
- const matchesWorkspaceRealtime = createWorkspaceRealtimeMatcher(routeContext.workspaceSlugFromRoute);
326
-
327
- const addEdit = useAddEdit({
328
- ownershipFilter: OWNERSHIP_WORKSPACE,
329
- resource: workspaceSettingsResource,
330
- apiSuffix: "/settings",
331
- queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
332
- buildWorkspaceQueryKey("settings", surfaceId, workspaceSlug),
333
- viewPermissions: ["workspace.settings.view", "workspace.settings.update"],
334
- savePermissions: ["workspace.settings.update"],
335
- placementSource: "users-web.workspace-settings-view",
336
- fallbackLoadError: "Unable to load workspace settings.",
337
- fieldErrorKeys: [
338
- "avatarUrl",
339
- "lightPrimaryColor",
340
- "lightSecondaryColor",
341
- "lightSurfaceColor",
342
- "lightSurfaceVariantColor",
343
- "darkPrimaryColor",
344
- "darkSecondaryColor",
345
- "darkSurfaceColor",
346
- "darkSurfaceVariantColor"
347
- ],
348
- realtime: {
349
- event: WORKSPACE_SETTINGS_CHANGED_EVENT,
350
- matches: matchesWorkspaceRealtime
351
- },
352
- model: workspaceForm,
353
- parseInput: (rawPayload) =>
354
- validateOperationSection({
355
- operation: workspaceSettingsResource.operations.patch,
356
- section: "bodyValidator",
357
- value: rawPayload
358
- }),
359
- mapLoadedToModel: (model, payload = {}) => {
360
- const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
361
- const normalizedTheme = resolveWorkspaceThemePalettes(settings);
362
-
363
- model.lightPrimaryColor = normalizedTheme.light.color;
364
- model.lightSecondaryColor = normalizedTheme.light.secondaryColor;
365
- model.lightSurfaceColor = normalizedTheme.light.surfaceColor;
366
- model.lightSurfaceVariantColor = normalizedTheme.light.surfaceVariantColor;
367
- model.darkPrimaryColor = normalizedTheme.dark.color;
368
- model.darkSecondaryColor = normalizedTheme.dark.secondaryColor;
369
- model.darkSurfaceColor = normalizedTheme.dark.surfaceColor;
370
- model.darkSurfaceVariantColor = normalizedTheme.dark.surfaceVariantColor;
371
- model.avatarUrl = String(settings.avatarUrl || "");
372
- model.invitesEnabled = settings.invitesEnabled !== false;
373
- model.invitesAvailable = settings.invitesAvailable !== false;
374
- },
375
- buildRawPayload: (model) => ({
376
- lightPrimaryColor: model.lightPrimaryColor,
377
- lightSecondaryColor: model.lightSecondaryColor,
378
- lightSurfaceColor: model.lightSurfaceColor,
379
- lightSurfaceVariantColor: model.lightSurfaceVariantColor,
380
- darkPrimaryColor: model.darkPrimaryColor,
381
- darkSecondaryColor: model.darkSecondaryColor,
382
- darkSurfaceColor: model.darkSurfaceColor,
383
- darkSurfaceVariantColor: model.darkSurfaceVariantColor,
384
- avatarUrl: model.avatarUrl,
385
- invitesEnabled: model.invitesEnabled
386
- }),
387
- onSaveSuccess: async () => {
388
- await bootstrapQuery.query.refetch();
389
- }
390
- });
391
-
392
- const showFormSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
99
+ async function handleFormSaved() {
100
+ await bootstrapQuery.query.refetch();
101
+ }
393
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>