diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 2e59395b..09b14056 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/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 (
@@ -46,6 +48,7 @@ export default function () { + diff --git a/packages/console/app/src/routes/workspace/settings-section.module.css b/packages/console/app/src/routes/workspace/settings-section.module.css new file mode 100644 index 00000000..e3a5ad50 --- /dev/null +++ b/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; + } + } +} diff --git a/packages/console/app/src/routes/workspace/settings-section.tsx b/packages/console/app/src/routes/workspace/settings-section.tsx new file mode 100644 index 00000000..0fc0158d --- /dev/null +++ b/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 ( +
+
+

Settings

+

Update your workspace name and preferences.

+
+
+
+
+

Workspace Name

+

{workspaceInfo()?.name}

+
+ +
+ (input = r)} + data-component="input" + name="name" + type="text" + placeholder="Workspace name" + value={workspaceInfo()?.name ?? "Default"} + /> + + {(err) =>
{err()}
} +
+
+ +
+ + +
+ + } + > + +
+
+
+
+ ) +} diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index 36d66e15..f9591632 100644 --- a/packages/console/core/src/workspace.ts +++ b/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)), + ) + }, + ) }