Frank 4 месяцев назад
Родитель
Сommit
c2f57ea74d

+ 3 - 0
packages/console/app/src/routes/workspace/[id].tsx

@@ -6,6 +6,7 @@ import { PaymentSection } from "./payment-section"
 import { UsageSection } from "./usage-section"
 import { KeySection } from "./key-section"
 import { MemberSection } from "./member-section"
+import { SettingsSection } from "./settings-section"
 import { Show } from "solid-js"
 import { createAsync, query, useParams } from "@solidjs/router"
 import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -28,6 +29,7 @@ export default function () {
   const params = useParams()
   const userInfo = createAsync(() => getUserInfo(params.id))
   const isBeta = createAsync(() => beta(params.id))
+
   return (
     <div data-page="workspace-[id]">
       <section data-component="title-section">
@@ -46,6 +48,7 @@ export default function () {
         <KeySection />
         <Show when={userInfo()?.isAdmin}>
           <Show when={isBeta()}>
+            <SettingsSection />
             <MemberSection />
           </Show>
           <BillingSection />

+ 95 - 0
packages/console/app/src/routes/workspace/settings-section.module.css

@@ -0,0 +1,95 @@
+.root {
+  [data-slot="section-content"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-4);
+  }
+
+  [data-slot="setting"] {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: var(--space-4);
+    padding: var(--space-4);
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius-sm);
+
+    @media (max-width: 30rem) {
+      flex-direction: column;
+      gap: var(--space-3);
+    }
+  }
+
+  [data-slot="setting-info"] {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-1);
+
+    h3 {
+      font-size: var(--font-size-md);
+      font-weight: 500;
+      line-height: 1.2;
+      margin: 0;
+      color: var(--color-text);
+    }
+
+    [data-slot="current-value"] {
+      font-size: var(--font-size-sm);
+      color: var(--color-text-muted);
+      line-height: 1.4;
+      margin: 0;
+    }
+  }
+
+  [data-slot="create-form"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-3);
+    min-width: 15rem;
+    width: fit-content;
+
+    @media (max-width: 30rem) {
+      width: 100%;
+      min-width: auto;
+    }
+
+    [data-slot="input-container"] {
+      display: flex;
+      flex-direction: column;
+      gap: var(--space-1);
+    }
+
+    input {
+      flex: 1;
+      padding: var(--space-2) var(--space-3);
+      border: 1px solid var(--color-border);
+      border-radius: var(--border-radius-sm);
+      background-color: var(--color-bg);
+      color: var(--color-text);
+      font-size: var(--font-size-sm);
+      font-family: var(--font-mono);
+
+      &:focus {
+        outline: none;
+        border-color: var(--color-accent);
+      }
+
+      &::placeholder {
+        color: var(--color-text-disabled);
+      }
+    }
+
+    [data-slot="form-actions"] {
+      display: flex;
+      gap: var(--space-2);
+      justify-content: flex-end;
+    }
+
+    [data-slot="form-error"] {
+      color: var(--color-danger);
+      font-size: var(--font-size-sm);
+      line-height: 1.4;
+    }
+  }
+}

+ 124 - 0
packages/console/app/src/routes/workspace/settings-section.tsx

@@ -0,0 +1,124 @@
+import { json, action, useParams, useSubmission, createAsync, query } from "@solidjs/router"
+import { createEffect, Show } from "solid-js"
+import { createStore } from "solid-js/store"
+import { withActor } from "~/context/auth.withActor"
+import { Workspace } from "@opencode-ai/console-core/workspace.js"
+import styles from "./settings-section.module.css"
+import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
+import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
+
+const getWorkspaceInfo = query(async (workspaceID: string) => {
+  "use server"
+  return withActor(
+    () =>
+      Database.use((tx) =>
+        tx
+          .select({
+            id: WorkspaceTable.id,
+            name: WorkspaceTable.name,
+            slug: WorkspaceTable.slug,
+          })
+          .from(WorkspaceTable)
+          .where(eq(WorkspaceTable.id, workspaceID))
+          .then((rows) => rows[0] || null),
+      ),
+    workspaceID,
+  )
+}, "workspace.get")
+
+const updateWorkspace = action(async (form: FormData) => {
+  "use server"
+  const name = form.get("name")?.toString().trim()
+  if (!name) return { error: "Workspace name is required." }
+  if (name.length > 255) return { error: "Name must be 255 characters or less." }
+  const workspaceID = form.get("workspaceID")?.toString()
+  if (!workspaceID) return { error: "Workspace ID is required." }
+  return json(
+    await withActor(
+      () =>
+        Workspace.update({ name })
+          .then(() => ({ error: undefined }))
+          .catch((e) => ({ error: e.message as string })),
+      workspaceID,
+    ),
+  )
+}, "workspace.update")
+
+export function SettingsSection() {
+  const params = useParams()
+  const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id))
+  const submission = useSubmission(updateWorkspace)
+  const [store, setStore] = createStore({ show: false })
+
+  let input: HTMLInputElement
+
+  createEffect(() => {
+    if (!submission.pending && submission.result && !submission.result.error) {
+      hide()
+    }
+  })
+
+  function show() {
+    while (true) {
+      submission.clear()
+      if (!submission.result) break
+    }
+    setStore("show", true)
+    input.focus()
+  }
+
+  function hide() {
+    setStore("show", false)
+  }
+
+  return (
+    <section class={styles.root}>
+      <div data-slot="section-title">
+        <h2>Settings</h2>
+        <p>Update your workspace name and preferences.</p>
+      </div>
+      <div data-slot="section-content">
+        <div data-slot="setting">
+          <div data-slot="setting-info">
+            <h3>Workspace Name</h3>
+            <p data-slot="current-value">{workspaceInfo()?.name}</p>
+          </div>
+          <Show
+            when={!store.show}
+            fallback={
+              <form action={updateWorkspace} method="post" data-slot="create-form">
+                <div data-slot="input-container">
+                  <input
+                    required
+                    ref={(r) => (input = r)}
+                    data-component="input"
+                    name="name"
+                    type="text"
+                    placeholder="Workspace name"
+                    value={workspaceInfo()?.name ?? "Default"}
+                  />
+                  <Show when={submission.result && submission.result.error}>
+                    {(err) => <div data-slot="form-error">{err()}</div>}
+                  </Show>
+                </div>
+                <input type="hidden" name="workspaceID" value={params.id} />
+                <div data-slot="form-actions">
+                  <button type="reset" data-color="ghost" onClick={() => hide()}>
+                    Cancel
+                  </button>
+                  <button type="submit" data-color="primary" disabled={submission.pending}>
+                    {submission.pending ? "Updating..." : "Update"}
+                  </button>
+                </div>
+              </form>
+            }
+          >
+            <button data-color="primary" onClick={() => show()}>
+              Edit Name
+            </button>
+          </Show>
+        </div>
+      </div>
+    </section>
+  )
+}

+ 18 - 0
packages/console/core/src/workspace.ts

@@ -7,6 +7,7 @@ import { UserTable } from "./schema/user.sql"
 import { BillingTable } from "./schema/billing.sql"
 import { WorkspaceTable } from "./schema/workspace.sql"
 import { Key } from "./key"
+import { eq } from "drizzle-orm"
 
 export namespace Workspace {
   export const create = fn(
@@ -45,4 +46,21 @@ export namespace Workspace {
       return workspaceID
     },
   )
+
+  export const update = fn(
+    z.object({
+      name: z.string().min(1).max(255),
+    }),
+    async ({ name }) => {
+      const workspaceID = Actor.workspace()
+      return await Database.use((tx) =>
+        tx
+          .update(WorkspaceTable)
+          .set({
+            name,
+          })
+          .where(eq(WorkspaceTable.id, workspaceID)),
+      )
+    },
+  )
 }