Add org switcher with create, switch, and delete
The sidebar org button was inert: clicking it did nothing. Add a real dropdown menu listing every organization, a "New organization" entry that re-enters onboarding without resetting the database, and a per-org delete affordance with inline confirmation. Active org is now persisted in a single-row app_state table so the snapshot keeps a stable selection across restarts and the renderer drives switching via two new IPC channels (setActiveOrganization, deleteOrganization). - migration 006 adds app_state(id=1, active_org_id, updated_at) - snapshot resolves active org from app_state with the first-org fallback - OrganizationService gains setActive + delete + bootstrap upsert - new OrgSwitcher renderer component keeps Shell.tsx lean
This commit is contained in:
1 parent
060fddf852
commit
f2ac1be5de
14 files changed
+385
-16
No files matched your search
@@ -67,6 +67,8 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.createOrganization, (_event, input) => mutate(() => organization.createOrganization(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setActiveOrganization, (_event, input) => mutate(() => organization.setActiveOrganization(input.orgId)));
|
||||
ipcMain.handle(IPC_CHANNELS.deleteOrganization, (_event, input) => mutate(() => organization.deleteOrganization(input.orgId)));
|
||||
ipcMain.handle(IPC_CHANNELS.createAgent, (_event, input) => mutate(() => organization.createAgent(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateTaskStatus, (_event, input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status)));
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const appStateSchema = `
|
||||
create table if not exists app_state (
|
||||
id integer primary key check (id = 1),
|
||||
active_org_id text references organizations(id) on delete set null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
insert into app_state(id, active_org_id, updated_at)
|
||||
select 1, (select id from organizations order by created_at asc limit 1), datetime('now')
|
||||
where not exists (select 1 from app_state where id = 1);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (6, 'app_state', datetime('now'));
|
||||
`;
|
||||
@@ -4,6 +4,7 @@ import { collaborationProviderPolicySchema } from "./migrations/002-collaboratio
|
||||
import { onboardingExecutionSchema } from "./migrations/003-onboarding";
|
||||
import { filesPinnedAndScheduledPosts } from "./migrations/004-files-posts";
|
||||
import { chatReadState } from "./migrations/005-chat-read-state";
|
||||
import { appStateSchema } from "./migrations/006-app-state";
|
||||
|
||||
const MIGRATIONS: Array<{ version: number; sql: string }> = [
|
||||
{ version: 1, sql: initialEnterpriseSchema },
|
||||
@@ -11,6 +12,7 @@ const MIGRATIONS: Array<{ version: number; sql: string }> = [
|
||||
{ version: 3, sql: onboardingExecutionSchema },
|
||||
{ version: 4, sql: filesPinnedAndScheduledPosts },
|
||||
{ version: 5, sql: chatReadState },
|
||||
{ version: 6, sql: appStateSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -74,6 +74,11 @@ export class OrganizationService {
|
||||
}
|
||||
|
||||
this.policy.seedOrganizationPolicies(orgId, now);
|
||||
this.db.prepare(`
|
||||
insert into app_state(id, active_org_id, updated_at)
|
||||
values (1, ?, ?)
|
||||
on conflict(id) do update set active_org_id = excluded.active_org_id, updated_at = excluded.updated_at
|
||||
`).run(orgId, now);
|
||||
this.audit.record({
|
||||
orgId,
|
||||
entityType: "organization",
|
||||
@@ -86,6 +91,41 @@ export class OrganizationService {
|
||||
return orgId;
|
||||
}
|
||||
|
||||
setActiveOrganization(orgId: string): void {
|
||||
const row = this.db.prepare("select id from organizations where id = ?").get(orgId) as { id: string } | undefined;
|
||||
if (!row) throw new Error("Organization was not found.");
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into app_state(id, active_org_id, updated_at)
|
||||
values (1, ?, ?)
|
||||
on conflict(id) do update set active_org_id = excluded.active_org_id, updated_at = excluded.updated_at
|
||||
`).run(orgId, now);
|
||||
this.audit.record({
|
||||
orgId,
|
||||
entityType: "app_state",
|
||||
entityId: "active_org",
|
||||
actorType: "employee",
|
||||
actorId: null,
|
||||
action: "switched",
|
||||
payload: { orgId },
|
||||
});
|
||||
}
|
||||
|
||||
deleteOrganization(orgId: string): void {
|
||||
const row = this.db.prepare("select id from organizations where id = ?")
|
||||
.get(orgId) as { id: string } | undefined;
|
||||
if (!row) throw new Error("Organization was not found.");
|
||||
this.db.prepare("delete from organizations where id = ?").run(orgId);
|
||||
const fallback = this.db.prepare("select id from organizations order by created_at asc limit 1")
|
||||
.get() as { id: string } | undefined;
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into app_state(id, active_org_id, updated_at)
|
||||
values (1, ?, ?)
|
||||
on conflict(id) do update set active_org_id = excluded.active_org_id, updated_at = excluded.updated_at
|
||||
`).run(fallback?.id ?? null, now);
|
||||
}
|
||||
|
||||
createAgent(input: CreateAgentInput): string {
|
||||
const id = createId("agt");
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -34,7 +34,7 @@ export class SnapshotService {
|
||||
autoApproveHires: toBool(org.autoApproveHires),
|
||||
reviewDeliverables: toBool(org.reviewDeliverables),
|
||||
}));
|
||||
const activeOrgId = organizations[0]?.id ?? null;
|
||||
const activeOrgId = this.resolveActiveOrgId(organizations.map((org) => org.id));
|
||||
const agents = list<AppSnapshot["agents"][number]>(this.db, "agents", "created_at asc")
|
||||
.map((agent) => ({ ...agent, heartbeatEnabled: toBool(agent.heartbeatEnabled) }));
|
||||
const tasks = list<AppSnapshot["tasks"][number]>(this.db, "tasks", "created_at asc")
|
||||
@@ -82,4 +82,12 @@ export class SnapshotService {
|
||||
auditEvents: list<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc").slice(0, 200),
|
||||
};
|
||||
}
|
||||
|
||||
private resolveActiveOrgId(known: string[]): string | null {
|
||||
const row = this.db.prepare("select active_org_id from app_state where id = 1")
|
||||
.get() as { active_org_id: string | null } | undefined;
|
||||
const stored = row?.active_org_id ?? null;
|
||||
if (stored && known.includes(stored)) return stored;
|
||||
return known[0] ?? null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user