Set a user's bots and workspaces in their own groups, saved at once

Take the Bots and Workspaces pickers out of the user form. A user's
settings page lists what they reach in two groups, like a workspace's
Bots group: each row links to its settings and can be taken away, and
a select adds one, saved straight away through users/add-access and
users/remove-access (setUserGrant).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 15:44:59 +01:00
co-authored by Claude Opus 5.5
parent 6294b911e6
commit 391943a1c2
19 changed files with 396 additions and 162 deletions
@@ -8,10 +8,10 @@ import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/
export default async function grabCrewOptions() {
const [bots, workspaces] = await Promise.all([
BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
sql: `SELECT id, name, role FROM bots WHERE kind = 'worker' ORDER BY name COLLATE NOCASE ASC, id ASC`,
sql: `SELECT id, name, role, hue, image_media_id FROM bots WHERE kind = 'worker' ORDER BY name COLLATE NOCASE ASC, id ASC`,
}),
BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
sql: `SELECT id, name FROM workspaces WHERE archived = 0 ORDER BY name COLLATE NOCASE ASC, id ASC`,
sql: `SELECT id, name, description, image_media_id FROM workspaces WHERE archived = 0 ORDER BY name COLLATE NOCASE ASC, id ASC`,
}),
]);
@@ -1,49 +0,0 @@
import BunSQLite from "@moduletrace/bun-sqlite";
type Params = {
user_id: number | string;
/**
* Every worker assigned to them, replacing the old list. The
* coordinator is never assigned. Left out: unchanged.
*/
bot_ids?: number[];
/**
* Every workspace they're in, replacing the old list. Left out:
* unchanged.
*/
workspace_ids?: number[];
};
/**
* Sets which workers a user chats with in their own chat (`bot_users`)
* and which workspaces they work in (`workspace_users`). The caller checks
* the actor manages the crew. Ids that aren't a worker or a workspace are
* skipped.
*/
export default async function setUserAccess({ user_id, bot_ids, workspace_ids }: Params) {
const now = Date.now();
if (bot_ids) {
await BunSQLite.sql({ sql: `DELETE FROM bot_users WHERE user_id = ?`, values: [user_id] });
for (const bot_id of new Set(bot_ids)) {
await BunSQLite.sql({
sql: `INSERT OR IGNORE INTO bot_users (created_at, updated_at, bot_id, user_id)
SELECT ?, ?, id, ? FROM bots WHERE id = ? AND kind = 'worker'`,
values: [now, now, user_id, bot_id],
});
}
}
if (workspace_ids) {
await BunSQLite.sql({ sql: `DELETE FROM workspace_users WHERE user_id = ?`, values: [user_id] });
for (const workspace_id of new Set(workspace_ids)) {
await BunSQLite.sql({
sql: `INSERT OR IGNORE INTO workspace_users (created_at, updated_at, workspace_id, user_id)
SELECT ?, ?, id, ? FROM workspaces WHERE id = ?`,
values: [now, now, user_id, workspace_id],
});
}
}
}
@@ -0,0 +1,40 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { UserGrantKind } from "@/src/schemas/user-schema";
type Params = {
user_id: number | string;
/**
* A worker assigned to them (`bot_users`, for their own chat) or a
* workspace they're in (`workspace_users`).
*/
kind: UserGrantKind;
id: number;
/**
* Gives it when true, takes it away when false.
*/
granted: boolean;
};
/**
* Gives a user one worker or workspace, or takes it away. The caller
* checks the actor manages the crew. An id that isn't a worker (the
* coordinator is never assigned) or a workspace is skipped.
*/
export default async function setUserGrant({ user_id, kind, id, granted }: Params) {
const table = kind == "bot" ? "bot_users" : "workspace_users";
const column = kind == "bot" ? "bot_id" : "workspace_id";
if (!granted) {
await BunSQLite.sql({ sql: `DELETE FROM ${table} WHERE user_id = ? AND ${column} = ?`, values: [user_id, id] });
return;
}
const now = Date.now();
const source = kind == "bot" ? `bots WHERE id = ? AND kind = 'worker'` : `workspaces WHERE id = ?`;
await BunSQLite.sql({
sql: `INSERT OR IGNORE INTO ${table} (created_at, updated_at, ${column}, user_id)
SELECT ?, ?, id, ? FROM ${source}`,
values: [now, now, user_id, id],
});
}