@jskit-ai/workspaces-web 0.1.61 → 0.1.63

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,9 +1,9 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/workspaces-web",
4
- version: "0.1.61",
4
+ version: "0.1.63",
5
5
  kind: "runtime",
6
- description: "Workspace web module: workspace selector, tools widget, workspace surfaces, and members/settings UI.",
6
+ description: "Workspace web module: workspace selector, tools widget, workspace surfaces, members UI, and settings hosts.",
7
7
  dependsOn: [
8
8
  "@jskit-ai/workspaces-core",
9
9
  "@jskit-ai/users-web"
@@ -41,10 +41,6 @@ export default Object.freeze({
41
41
  subpath: "./client/components/AccountSettingsInvitesSection",
42
42
  summary: "Exports the default account invites section component used by multihoming installs."
43
43
  },
44
- {
45
- subpath: "./client/components/WorkspaceSettingsClientElement",
46
- summary: "Exports the workspace settings client element used by the workspace admin settings starter page."
47
- },
48
44
  {
49
45
  subpath: "./client/providers/WorkspacesWebClientProvider",
50
46
  summary: "Exports workspaces-web client provider class."
@@ -231,8 +227,8 @@ export default Object.freeze({
231
227
  mutations: {
232
228
  dependencies: {
233
229
  runtime: {
234
- "@jskit-ai/workspaces-core": "0.1.61",
235
- "@jskit-ai/users-web": "0.1.100"
230
+ "@jskit-ai/workspaces-core": "0.1.62",
231
+ "@jskit-ai/users-web": "0.1.101"
236
232
  },
237
233
  dev: {}
238
234
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-web",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -10,17 +10,16 @@
10
10
  "./client/components/AccountSettingsInvitesSection": "./src/client/components/AccountSettingsInvitesSection.vue",
11
11
  "./client/providers/WorkspacesWebClientProvider": "./src/client/providers/WorkspacesWebClientProvider.js",
12
12
  "./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
13
- "./client/components/WorkspaceSettingsClientElement": "./src/client/components/WorkspaceSettingsClientElement.vue",
14
13
  "./client/composables/useWorkspaceRouteContext": "./src/client/composables/useWorkspaceRouteContext.js"
15
14
  },
16
15
  "dependencies": {
17
16
  "@mdi/js": "^7.4.47",
18
- "@jskit-ai/http-runtime": "0.1.84",
19
- "@jskit-ai/kernel": "0.1.85",
20
- "@jskit-ai/shell-web": "0.1.84",
21
- "@jskit-ai/users-core": "0.1.95",
22
- "@jskit-ai/users-web": "0.1.100",
23
- "@jskit-ai/workspaces-core": "0.1.61"
17
+ "@jskit-ai/http-runtime": "0.1.85",
18
+ "@jskit-ai/kernel": "0.1.86",
19
+ "@jskit-ai/shell-web": "0.1.85",
20
+ "@jskit-ai/users-core": "0.1.96",
21
+ "@jskit-ai/users-web": "0.1.101",
22
+ "@jskit-ai/workspaces-core": "0.1.62"
24
23
  },
25
24
  "peerDependencies": {
26
25
  "@tanstack/vue-query": "^5.90.5",
@@ -1,7 +1,23 @@
1
- <script setup>
2
- import WorkspaceSettingsClientElement from "@jskit-ai/workspaces-web/client/components/WorkspaceSettingsClientElement";
3
- </script>
4
-
5
1
  <template>
6
- <WorkspaceSettingsClientElement />
2
+ <section class="workspace-settings-landing">
3
+ <v-sheet rounded="lg" border class="workspace-settings-landing__panel">
4
+ <h2 class="workspace-settings-landing__title">No settings sections yet</h2>
5
+ <p class="text-body-2 text-medium-emphasis mb-0">
6
+ This app has no workspace-level settings configured.
7
+ </p>
8
+ </v-sheet>
9
+ </section>
7
10
  </template>
11
+
12
+ <style scoped>
13
+ .workspace-settings-landing__panel {
14
+ padding: 1rem;
15
+ }
16
+
17
+ .workspace-settings-landing__title {
18
+ font-size: 1rem;
19
+ font-weight: 650;
20
+ line-height: 1.2;
21
+ margin: 0 0 0.25rem;
22
+ }
23
+ </style>
@@ -13,7 +13,7 @@ import { RouterView } from "vue-router";
13
13
 
14
14
  <v-sheet rounded="lg" border class="settings-shell__panel">
15
15
  <nav class="settings-shell__nav" aria-label="Workspace settings sections">
16
- <v-list nav density="comfortable" class="settings-shell__nav-list">
16
+ <v-list nav density="compact" class="settings-shell__nav-list">
17
17
  <ShellOutlet target="admin-settings:primary-menu" />
18
18
  </v-list>
19
19
  </nav>
@@ -41,11 +41,31 @@ import { RouterView } from "vue-router";
41
41
  }
42
42
 
43
43
  .settings-shell__nav-list {
44
- padding: 0;
44
+ padding: 0.35rem;
45
+ }
46
+
47
+ .settings-shell__nav-list :deep(.v-list-item) {
48
+ min-height: 48px;
49
+ padding-inline: 0.6rem 0.7rem;
50
+ }
51
+
52
+ .settings-shell__nav-list :deep(.v-list-item__prepend) {
53
+ margin-inline-end: 0;
54
+ }
55
+
56
+ .settings-shell__nav-list :deep(.v-list-item__spacer) {
57
+ min-width: 0.5rem;
58
+ width: 0.5rem;
59
+ }
60
+
61
+ .settings-shell__nav-list :deep(.v-list-item-title) {
62
+ line-height: 1.2;
45
63
  }
46
64
 
47
65
  .settings-shell__content {
66
+ border-left: 1px solid rgba(var(--v-theme-on-surface), 0.18);
48
67
  min-width: 0;
68
+ padding-left: 1rem;
49
69
  }
50
70
 
51
71
  @media (max-width: 960px) {
@@ -59,12 +79,17 @@ import { RouterView } from "vue-router";
59
79
 
60
80
  .settings-shell__nav-list {
61
81
  display: flex;
62
- gap: 0.5rem;
82
+ gap: 0.25rem;
63
83
  min-width: max-content;
64
84
  }
65
85
 
66
86
  .settings-shell__nav-list :deep(.v-list-item) {
67
87
  min-height: 48px;
68
88
  }
89
+
90
+ .settings-shell__content {
91
+ border-left: 0;
92
+ padding-left: 0;
93
+ }
69
94
  }
70
95
  </style>
@@ -16,7 +16,6 @@ test("workspaces-web exports are explicit and aligned with production usage", ()
16
16
  requiredExports: [
17
17
  "./client",
18
18
  "./client/components/AccountSettingsInvitesSection",
19
- "./client/components/WorkspaceSettingsClientElement",
20
19
  "./client/providers/WorkspacesWebClientProvider",
21
20
  "./client/composables/useWorkspaceRouteContext"
22
21
  ]
@@ -101,8 +101,6 @@ test("workspaces-web installs an app-owned account invites section wrapper", asy
101
101
  test("workspaces-web settings components use direct panels instead of card scaffolds", async () => {
102
102
  for (const relativePath of [
103
103
  path.join("templates", "src", "components", "WorkspaceNotFoundCard.vue"),
104
- path.join("src", "client", "components", "WorkspaceProfileClientElement.vue"),
105
- path.join("src", "client", "components", "WorkspaceSettingsFieldsClientElement.vue"),
106
104
  path.join("src", "client", "components", "WorkspacesClientElement.vue"),
107
105
  path.join("src", "client", "components", "AccountSettingsInvitesSection.vue")
108
106
  ]) {
@@ -118,8 +116,6 @@ test("workspaces-web settings components use direct panels instead of card scaff
118
116
 
119
117
  test("workspaces-web resource load states expose local retry actions", async () => {
120
118
  const expectations = new Map([
121
- ["src/client/components/WorkspaceProfileClientElement.vue", /addEdit\.canRetryLoad[\s\S]*@click="addEdit\.refresh"/],
122
- ["src/client/components/WorkspaceSettingsFieldsClientElement.vue", /addEdit\.canRetryLoad[\s\S]*@click="addEdit\.refresh"/],
123
119
  ["src/client/components/WorkspacesClientElement.vue", /bootstrapLoadError[\s\S]*@click="refreshBootstrap"/],
124
120
  ["src/client/components/WorkspaceMembersClientElement.vue", /canRetryLoad[\s\S]*@click="refreshLoad"/]
125
121
  ]);
@@ -149,14 +145,25 @@ test("workspaces-web installs an account invites cue scaffold that reads placeme
149
145
  });
150
146
  });
151
147
 
152
- test("workspaces-web admin settings index template renders real workspace settings controls", async () => {
148
+ test("workspaces-web admin settings index template is an app-owned landing slot", async () => {
153
149
  const source = await readFile(
154
150
  path.join(PACKAGE_DIR, "templates", "src", "pages", "admin", "workspace", "settings", "index.vue"),
155
151
  "utf8"
156
152
  );
157
153
 
158
- assert.match(source, /@jskit-ai\/workspaces-web\/client\/components\/WorkspaceSettingsClientElement/);
159
- assert.match(source, /<WorkspaceSettingsClientElement \/>/);
154
+ assertGeneratedUiSourceContract(source, {
155
+ forbidCardShell: true,
156
+ sourceName: "admin/workspace/settings/index.vue",
157
+ requiredPatterns: [
158
+ {
159
+ id: "app-owned-settings-landing",
160
+ pattern: /No settings sections yet/,
161
+ message: "Workspace settings index needs a product-shaped app-owned landing state."
162
+ }
163
+ ]
164
+ });
165
+ assert.doesNotMatch(source, /@jskit-ai\/workspaces-web\/client\/components\/WorkspaceSettingsClientElement/);
166
+ assert.doesNotMatch(source, /<WorkspaceSettingsClientElement \/>/);
160
167
  assert.doesNotMatch(source, /your_child_segment|To redirect this settings shell/);
161
168
  });
162
169
 
@@ -1,155 +0,0 @@
1
- <template>
2
- <v-sheet rounded="lg" border class="workspace-profile-panel">
3
- <header class="workspace-profile-panel__header">
4
- <h2 class="workspace-profile-panel__title">Workspace profile</h2>
5
- <p class="text-body-2 text-medium-emphasis mb-0">Name and avatar used across the workspace.</p>
6
- </header>
7
-
8
- <div class="workspace-profile-panel__body">
9
- <template v-if="showSkeleton">
10
- <v-skeleton-loader type="text@2, list-item-two-line@3, button" />
11
- </template>
12
-
13
- <div v-else-if="addEdit.loadError" class="workspace-profile-panel__state">
14
- <p class="text-body-2 text-medium-emphasis mb-4">{{ addEdit.loadError }}</p>
15
- <v-btn
16
- v-if="addEdit.canRetryLoad"
17
- color="primary"
18
- variant="tonal"
19
- :loading="addEdit.isFetching"
20
- @click="addEdit.refresh"
21
- >
22
- Retry
23
- </v-btn>
24
- </div>
25
-
26
- <p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
27
- You do not have permission to view workspace profile.
28
- </p>
29
-
30
- <template v-else>
31
- <v-form @submit.prevent="addEdit.submit" novalidate>
32
- <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
33
- <v-row>
34
- <v-col cols="12" md="6">
35
- <v-text-field
36
- v-model="workspaceProfileForm.name"
37
- label="Workspace name"
38
- variant="outlined"
39
- density="comfortable"
40
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
41
- :error-messages="addEdit.fieldErrors.name ? [addEdit.fieldErrors.name] : []"
42
- />
43
- </v-col>
44
-
45
- <v-col cols="12" md="6">
46
- <v-text-field
47
- v-model="workspaceProfileForm.avatarUrl"
48
- label="Workspace avatar URL"
49
- variant="outlined"
50
- density="comfortable"
51
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
52
- placeholder="https://..."
53
- hint="Optional"
54
- persistent-hint
55
- :error-messages="addEdit.fieldErrors.avatarUrl ? [addEdit.fieldErrors.avatarUrl] : []"
56
- />
57
- </v-col>
58
-
59
- <v-col cols="12" class="d-flex align-center justify-end ga-3">
60
- <v-btn
61
- v-if="addEdit.canSave"
62
- type="submit"
63
- color="primary"
64
- :loading="addEdit.isSaving"
65
- :disabled="addEdit.isInitialLoading || addEdit.isRefetching"
66
- >
67
- Save workspace profile
68
- </v-btn>
69
- <v-chip v-else color="secondary" label>Read-only</v-chip>
70
- </v-col>
71
- </v-row>
72
- </v-form>
73
- </template>
74
- </div>
75
- </v-sheet>
76
- </template>
77
-
78
- <script setup>
79
- import { computed, reactive } from "vue";
80
- import { ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/kernel/shared/support/visibility";
81
- import { WORKSPACES_TRANSPORT } from "@jskit-ai/workspaces-core/shared/jsonApiTransports";
82
- import { workspaceResource } from "@jskit-ai/workspaces-core/shared/resources/workspaceResource";
83
- import { useAddEdit } from "@jskit-ai/users-web/client/composables/useAddEdit";
84
- import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
85
-
86
- const emit = defineEmits(["saved"]);
87
-
88
- const workspaceProfileForm = reactive({
89
- name: "",
90
- avatarUrl: ""
91
- });
92
-
93
- const addEdit = useAddEdit({
94
- ownershipFilter: ROUTE_VISIBILITY_WORKSPACE,
95
- resource: workspaceResource,
96
- apiSuffix: "/",
97
- queryKeyFactory: (surfaceId = "", scopeParamValue = "") =>
98
- buildWorkspaceQueryKey("profile", surfaceId, scopeParamValue),
99
- viewPermissions: ["workspace.settings.view", "workspace.settings.update"],
100
- savePermissions: ["workspace.settings.update"],
101
- transport: WORKSPACES_TRANSPORT,
102
- placementSource: "workspaces-web.workspace-profile-view",
103
- fallbackLoadError: "Unable to load workspace profile.",
104
- fieldErrorKeys: ["name", "avatarUrl"],
105
- model: workspaceProfileForm,
106
- input: workspaceResource.operations.patch.body,
107
- mapLoadedToModel: (model, payload = {}) => {
108
- model.name = String(payload?.name || "");
109
- model.avatarUrl = String(payload?.avatarUrl || "");
110
- },
111
- buildRawPayload: (model) => ({
112
- name: model.name,
113
- avatarUrl: model.avatarUrl
114
- }),
115
- onSaveSuccess: async () => {
116
- emit("saved");
117
- }
118
- });
119
-
120
- const showSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
121
- </script>
122
-
123
- <style scoped>
124
- .workspace-profile-panel {
125
- overflow: hidden;
126
- }
127
-
128
- .workspace-profile-panel__header {
129
- padding: 1rem 1rem 0;
130
- }
131
-
132
- .workspace-profile-panel__title {
133
- font-size: 1rem;
134
- font-weight: 650;
135
- line-height: 1.2;
136
- margin: 0 0 0.25rem;
137
- }
138
-
139
- .workspace-profile-panel__body {
140
- padding: 1rem;
141
- }
142
-
143
- .workspace-profile-panel__state {
144
- margin-inline: auto;
145
- max-width: 30rem;
146
- padding: 1.5rem 1rem;
147
- text-align: center;
148
- }
149
-
150
- @media (max-width: 640px) {
151
- .workspace-profile-panel__body :deep(.v-btn) {
152
- min-height: 48px;
153
- }
154
- }
155
- </style>
@@ -1,102 +0,0 @@
1
- <template>
2
- <section class="workspace-settings-client-element d-flex flex-column ga-4">
3
- <WorkspaceProfileClientElement @saved="handleFormSaved" />
4
- <WorkspaceSettingsFieldsClientElement @saved="handleFormSaved" />
5
- </section>
6
- </template>
7
-
8
- <script setup>
9
- import { computed, watch } from "vue";
10
- import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
11
- import { useBootstrapQuery } from "../composables/useBootstrapQuery.js";
12
- import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
13
- import { findWorkspaceBySlug, normalizeWorkspaceList } from "../lib/bootstrap.js";
14
- import { arePermissionListsEqual, normalizePermissionList } from "@jskit-ai/users-web/client/lib/permissions";
15
- import WorkspaceProfileClientElement from "./WorkspaceProfileClientElement.vue";
16
- import WorkspaceSettingsFieldsClientElement from "./WorkspaceSettingsFieldsClientElement.vue";
17
-
18
- const routeContext = useWorkspaceRouteContext();
19
- const { context: placementContext, mergeContext: mergePlacementContext } = useWebPlacementContext();
20
- const bootstrapQuery = useBootstrapQuery({
21
- workspaceSlug: routeContext.workspaceSlugFromRoute,
22
- enabled: computed(() => Boolean(routeContext.workspaceSlugFromRoute.value))
23
- });
24
-
25
- function toWorkspaceEntrySnapshot(entry = null) {
26
- if (!entry || typeof entry !== "object") {
27
- return "";
28
- }
29
-
30
- const normalizedEntry = normalizeWorkspaceList([entry])[0] || null;
31
- if (!normalizedEntry) {
32
- return "";
33
- }
34
-
35
- return JSON.stringify(normalizedEntry);
36
- }
37
-
38
- function toWorkspaceListSnapshot(list = []) {
39
- return JSON.stringify(normalizeWorkspaceList(list));
40
- }
41
-
42
- function toWorkspaceSettingsSnapshot(settings = null) {
43
- if (!settings || typeof settings !== "object") {
44
- return "";
45
- }
46
-
47
- return JSON.stringify(settings);
48
- }
49
-
50
- function applyShellWorkspaceContext(payload = {}) {
51
- const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
52
- const currentWorkspace = findWorkspaceBySlug(
53
- availableWorkspaces,
54
- routeContext.workspaceSlugFromRoute.value
55
- );
56
- const permissions = normalizePermissionList(payload?.permissions);
57
- const currentContext =
58
- placementContext.value && typeof placementContext.value === "object"
59
- ? placementContext.value
60
- : {};
61
- const currentPermissions = normalizePermissionList(currentContext.permissions);
62
- const samePermissions = arePermissionListsEqual(permissions, currentPermissions);
63
- const sameWorkspace = toWorkspaceEntrySnapshot(currentContext.workspace) === toWorkspaceEntrySnapshot(currentWorkspace);
64
- const sameWorkspaces =
65
- toWorkspaceListSnapshot(currentContext.workspaces) === toWorkspaceListSnapshot(availableWorkspaces);
66
- const sameWorkspaceSettings =
67
- toWorkspaceSettingsSnapshot(currentContext.workspaceSettings) ===
68
- toWorkspaceSettingsSnapshot(payload?.workspaceSettings);
69
-
70
- if (samePermissions && sameWorkspace && sameWorkspaces && sameWorkspaceSettings) {
71
- return;
72
- }
73
-
74
- mergePlacementContext(
75
- {
76
- workspace: currentWorkspace,
77
- workspaceSettings:
78
- payload?.workspaceSettings && typeof payload.workspaceSettings === "object"
79
- ? payload.workspaceSettings
80
- : null,
81
- workspaces: availableWorkspaces,
82
- permissions
83
- },
84
- "workspaces-web.workspace-settings-view"
85
- );
86
- }
87
-
88
- watch(
89
- () => bootstrapQuery.query.data.value,
90
- (payload) => {
91
- if (!payload || typeof payload !== "object") {
92
- return;
93
- }
94
- applyShellWorkspaceContext(payload);
95
- },
96
- { immediate: true }
97
- );
98
-
99
- async function handleFormSaved() {
100
- await bootstrapQuery.query.refetch();
101
- }
102
- </script>
@@ -1,304 +0,0 @@
1
- <template>
2
- <v-sheet rounded="lg" border class="workspace-settings-panel">
3
- <header class="workspace-settings-panel__header">
4
- <h2 class="workspace-settings-panel__title">Workspace settings</h2>
5
- <p class="text-body-2 text-medium-emphasis mb-0">These values apply to everyone in this workspace.</p>
6
- </header>
7
-
8
- <div class="workspace-settings-panel__body">
9
- <template v-if="showSkeleton">
10
- <v-skeleton-loader type="text@2, list-item-two-line@4, button" />
11
- </template>
12
-
13
- <div v-else-if="addEdit.loadError" class="workspace-settings-panel__state">
14
- <p class="text-body-2 text-medium-emphasis mb-4">{{ addEdit.loadError }}</p>
15
- <v-btn
16
- v-if="addEdit.canRetryLoad"
17
- color="primary"
18
- variant="tonal"
19
- :loading="addEdit.isFetching"
20
- @click="addEdit.refresh"
21
- >
22
- Retry
23
- </v-btn>
24
- </div>
25
-
26
- <p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
27
- You do not have permission to view workspace settings.
28
- </p>
29
-
30
- <template v-else>
31
- <v-form @submit.prevent="addEdit.submit" novalidate>
32
- <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
33
- <v-row>
34
- <v-col cols="12">
35
- <div class="text-subtitle-2 mb-2">Theme colors</div>
36
- <v-row dense class="mb-2">
37
- <v-col cols="12">
38
- <div class="text-caption text-medium-emphasis mb-2">Light palette</div>
39
- </v-col>
40
- <v-col cols="12" md="3">
41
- <v-text-field
42
- v-model="workspaceSettingsForm.lightPrimaryColor"
43
- label="Primary"
44
- type="color"
45
- variant="outlined"
46
- density="comfortable"
47
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
48
- :error-messages="addEdit.fieldErrors.lightPrimaryColor ? [addEdit.fieldErrors.lightPrimaryColor] : []"
49
- />
50
- </v-col>
51
-
52
- <v-col cols="12" md="3">
53
- <v-text-field
54
- v-model="workspaceSettingsForm.lightSecondaryColor"
55
- label="Secondary"
56
- type="color"
57
- variant="outlined"
58
- density="comfortable"
59
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
60
- :error-messages="
61
- addEdit.fieldErrors.lightSecondaryColor ? [addEdit.fieldErrors.lightSecondaryColor] : []
62
- "
63
- />
64
- </v-col>
65
-
66
- <v-col cols="12" md="3">
67
- <v-text-field
68
- v-model="workspaceSettingsForm.lightSurfaceColor"
69
- label="Surface"
70
- type="color"
71
- variant="outlined"
72
- density="comfortable"
73
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
74
- :error-messages="addEdit.fieldErrors.lightSurfaceColor ? [addEdit.fieldErrors.lightSurfaceColor] : []"
75
- />
76
- </v-col>
77
-
78
- <v-col cols="12" md="3">
79
- <v-text-field
80
- v-model="workspaceSettingsForm.lightSurfaceVariantColor"
81
- label="Surface variant"
82
- type="color"
83
- variant="outlined"
84
- density="comfortable"
85
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
86
- :error-messages="
87
- addEdit.fieldErrors.lightSurfaceVariantColor ? [addEdit.fieldErrors.lightSurfaceVariantColor] : []
88
- "
89
- />
90
- </v-col>
91
- </v-row>
92
-
93
- <v-row dense>
94
- <v-col cols="12">
95
- <div class="text-caption text-medium-emphasis mb-2">Dark palette</div>
96
- </v-col>
97
- <v-col cols="12" md="3">
98
- <v-text-field
99
- v-model="workspaceSettingsForm.darkPrimaryColor"
100
- label="Primary"
101
- type="color"
102
- variant="outlined"
103
- density="comfortable"
104
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
105
- :error-messages="addEdit.fieldErrors.darkPrimaryColor ? [addEdit.fieldErrors.darkPrimaryColor] : []"
106
- />
107
- </v-col>
108
-
109
- <v-col cols="12" md="3">
110
- <v-text-field
111
- v-model="workspaceSettingsForm.darkSecondaryColor"
112
- label="Secondary"
113
- type="color"
114
- variant="outlined"
115
- density="comfortable"
116
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
117
- :error-messages="addEdit.fieldErrors.darkSecondaryColor ? [addEdit.fieldErrors.darkSecondaryColor] : []"
118
- />
119
- </v-col>
120
-
121
- <v-col cols="12" md="3">
122
- <v-text-field
123
- v-model="workspaceSettingsForm.darkSurfaceColor"
124
- label="Surface"
125
- type="color"
126
- variant="outlined"
127
- density="comfortable"
128
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
129
- :error-messages="addEdit.fieldErrors.darkSurfaceColor ? [addEdit.fieldErrors.darkSurfaceColor] : []"
130
- />
131
- </v-col>
132
-
133
- <v-col cols="12" md="3">
134
- <v-text-field
135
- v-model="workspaceSettingsForm.darkSurfaceVariantColor"
136
- label="Surface variant"
137
- type="color"
138
- variant="outlined"
139
- density="comfortable"
140
- :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
141
- :error-messages="
142
- addEdit.fieldErrors.darkSurfaceVariantColor ? [addEdit.fieldErrors.darkSurfaceVariantColor] : []
143
- "
144
- />
145
- </v-col>
146
- </v-row>
147
- </v-col>
148
-
149
- <v-col cols="12" md="6" class="d-flex align-center">
150
- <v-switch
151
- v-model="workspaceSettingsForm.invitesEnabled"
152
- color="primary"
153
- hide-details
154
- label="Enable invites"
155
- :disabled="!addEdit.canSave || !workspaceSettingsForm.invitesAvailable || addEdit.isSaving || addEdit.isRefetching"
156
- />
157
- </v-col>
158
-
159
- <v-col cols="12" class="d-flex align-center justify-end ga-3">
160
- <v-btn
161
- v-if="addEdit.canSave"
162
- type="submit"
163
- color="primary"
164
- :loading="addEdit.isSaving"
165
- :disabled="addEdit.isInitialLoading || addEdit.isRefetching"
166
- >
167
- Save workspace settings
168
- </v-btn>
169
- <v-chip v-else color="secondary" label>Read-only</v-chip>
170
- </v-col>
171
- </v-row>
172
- </v-form>
173
- </template>
174
- </div>
175
- </v-sheet>
176
- </template>
177
-
178
- <script setup>
179
- import { computed, reactive } from "vue";
180
- import { ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/kernel/shared/support/visibility";
181
- import { WORKSPACE_SETTINGS_TRANSPORT } from "@jskit-ai/workspaces-core/shared/jsonApiTransports";
182
- import { workspaceSettingsResource } from "@jskit-ai/workspaces-core/shared/resources/workspaceSettingsResource";
183
- import {
184
- DEFAULT_WORKSPACE_DARK_PALETTE,
185
- DEFAULT_WORKSPACE_LIGHT_PALETTE,
186
- resolveWorkspaceThemePalettes
187
- } from "@jskit-ai/workspaces-core/shared/settings";
188
- import { useAddEdit } from "@jskit-ai/users-web/client/composables/useAddEdit";
189
- import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
190
- import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
191
- import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
192
-
193
- const emit = defineEmits(["saved"]);
194
-
195
- const workspaceSettingsForm = reactive({
196
- lightPrimaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.color,
197
- lightSecondaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor,
198
- lightSurfaceColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor,
199
- lightSurfaceVariantColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor,
200
- darkPrimaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.color,
201
- darkSecondaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor,
202
- darkSurfaceColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor,
203
- darkSurfaceVariantColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor,
204
- invitesEnabled: false,
205
- invitesAvailable: false
206
- });
207
-
208
- const routeContext = useWorkspaceRouteContext();
209
- const matchesWorkspaceRealtime = createWorkspaceRealtimeMatcher(routeContext.workspaceSlugFromRoute);
210
-
211
- const addEdit = useAddEdit({
212
- ownershipFilter: ROUTE_VISIBILITY_WORKSPACE,
213
- resource: workspaceSettingsResource,
214
- apiSuffix: "/settings",
215
- queryKeyFactory: (surfaceId = "", scopeParamValue = "") =>
216
- buildWorkspaceQueryKey("settings", surfaceId, scopeParamValue),
217
- viewPermissions: ["workspace.settings.view", "workspace.settings.update"],
218
- savePermissions: ["workspace.settings.update"],
219
- transport: WORKSPACE_SETTINGS_TRANSPORT,
220
- placementSource: "workspaces-web.workspace-settings-view",
221
- fallbackLoadError: "Unable to load workspace settings.",
222
- fieldErrorKeys: [
223
- "lightPrimaryColor",
224
- "lightSecondaryColor",
225
- "lightSurfaceColor",
226
- "lightSurfaceVariantColor",
227
- "darkPrimaryColor",
228
- "darkSecondaryColor",
229
- "darkSurfaceColor",
230
- "darkSurfaceVariantColor"
231
- ],
232
- realtime: {
233
- event: "workspace.settings.changed",
234
- matches: matchesWorkspaceRealtime
235
- },
236
- model: workspaceSettingsForm,
237
- input: workspaceSettingsResource.operations.patch.body,
238
- mapLoadedToModel: (model, payload = {}) => {
239
- const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
240
- const normalizedTheme = resolveWorkspaceThemePalettes(settings);
241
-
242
- model.lightPrimaryColor = normalizedTheme.light.color;
243
- model.lightSecondaryColor = normalizedTheme.light.secondaryColor;
244
- model.lightSurfaceColor = normalizedTheme.light.surfaceColor;
245
- model.lightSurfaceVariantColor = normalizedTheme.light.surfaceVariantColor;
246
- model.darkPrimaryColor = normalizedTheme.dark.color;
247
- model.darkSecondaryColor = normalizedTheme.dark.secondaryColor;
248
- model.darkSurfaceColor = normalizedTheme.dark.surfaceColor;
249
- model.darkSurfaceVariantColor = normalizedTheme.dark.surfaceVariantColor;
250
- model.invitesEnabled = settings.invitesEnabled !== false;
251
- model.invitesAvailable = settings.invitesAvailable !== false;
252
- },
253
- buildRawPayload: (model) => ({
254
- lightPrimaryColor: model.lightPrimaryColor,
255
- lightSecondaryColor: model.lightSecondaryColor,
256
- lightSurfaceColor: model.lightSurfaceColor,
257
- lightSurfaceVariantColor: model.lightSurfaceVariantColor,
258
- darkPrimaryColor: model.darkPrimaryColor,
259
- darkSecondaryColor: model.darkSecondaryColor,
260
- darkSurfaceColor: model.darkSurfaceColor,
261
- darkSurfaceVariantColor: model.darkSurfaceVariantColor,
262
- invitesEnabled: model.invitesEnabled
263
- }),
264
- onSaveSuccess: async () => {
265
- emit("saved");
266
- }
267
- });
268
-
269
- const showSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
270
- </script>
271
-
272
- <style scoped>
273
- .workspace-settings-panel {
274
- overflow: hidden;
275
- }
276
-
277
- .workspace-settings-panel__header {
278
- padding: 1rem 1rem 0;
279
- }
280
-
281
- .workspace-settings-panel__title {
282
- font-size: 1rem;
283
- font-weight: 650;
284
- line-height: 1.2;
285
- margin: 0 0 0.25rem;
286
- }
287
-
288
- .workspace-settings-panel__body {
289
- padding: 1rem;
290
- }
291
-
292
- .workspace-settings-panel__state {
293
- margin-inline: auto;
294
- max-width: 30rem;
295
- padding: 1.5rem 1rem;
296
- text-align: center;
297
- }
298
-
299
- @media (max-width: 640px) {
300
- .workspace-settings-panel__body :deep(.v-btn) {
301
- min-height: 48px;
302
- }
303
- }
304
- </style>
@@ -1,52 +0,0 @@
1
- import {
2
- computed
3
- } from "vue";
4
- import { useQuery } from "@tanstack/vue-query";
5
- import { normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
6
- import { workspacesWebHttpClient } from "../lib/httpClient.js";
7
- import { buildBootstrapApiPath } from "../lib/bootstrap.js";
8
- import { resolveEnabledRef, resolveTextRef } from "./support/refValueHelpers.js";
9
-
10
- const DEFAULT_BOOTSTRAP_STALE_TIME_MS = 60_000;
11
-
12
- function normalizeStaleTime(value, fallback = DEFAULT_BOOTSTRAP_STALE_TIME_MS) {
13
- const parsed = Number(value);
14
- if (!Number.isFinite(parsed) || parsed < 0) {
15
- return fallback;
16
- }
17
- return parsed;
18
- }
19
-
20
- function useBootstrapQuery({
21
- workspaceSlug = "",
22
- enabled = true,
23
- staleTime = DEFAULT_BOOTSTRAP_STALE_TIME_MS,
24
- refetchOnMount = false,
25
- refetchOnWindowFocus = false
26
- } = {}) {
27
- const normalizedWorkspaceSlug = computed(() => resolveTextRef(workspaceSlug));
28
- const queryKey = computed(() => ["workspaces-web", "bootstrap", normalizeQueryToken(normalizedWorkspaceSlug.value)]);
29
- const bootstrapPath = computed(() => buildBootstrapApiPath(normalizedWorkspaceSlug.value));
30
- const queryEnabled = computed(() => resolveEnabledRef(enabled));
31
- const queryStaleTime = normalizeStaleTime(staleTime, DEFAULT_BOOTSTRAP_STALE_TIME_MS);
32
-
33
- const query = useQuery({
34
- queryKey,
35
- queryFn: () =>
36
- workspacesWebHttpClient.request(bootstrapPath.value, {
37
- method: "GET"
38
- }),
39
- enabled: queryEnabled,
40
- staleTime: queryStaleTime,
41
- refetchOnMount,
42
- refetchOnWindowFocus
43
- });
44
-
45
- return Object.freeze({
46
- query,
47
- queryKey,
48
- workspaceSlug: normalizedWorkspaceSlug
49
- });
50
- }
51
-
52
- export { useBootstrapQuery };