Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions apps/hub/src/artifacts/approval-rounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
type StoredApprovalRound,
type StoredPackageVersion,
} from "./approval-rounds.ts";
import { DEFAULT_WORKSPACE_REVIEWERS } from "../config/approval-rounds.ts";
import type { WorkspaceReviewer } from "@corbits-builder/schemas/approval-round";
import { ProjectError } from "../projects/errors.ts";
import type { Actor } from "../lib/actor.ts";
Expand Down Expand Up @@ -111,9 +110,7 @@ function createFakeStore(options?: {
sha256: string;
}> = [];
const rounds = new Map<string, StoredApprovalRound>();
const workspaceReviewers: WorkspaceReviewer[] = [
...DEFAULT_WORKSPACE_REVIEWERS,
];
const workspaceReviewers: WorkspaceReviewer[] = [];

const store: ApprovalRoundStore = {
now: () => currentNow,
Expand Down
11 changes: 6 additions & 5 deletions apps/hub/src/artifacts/approval-rounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import {
settingsRecord,
} from "../db/schema.ts";
import {
DEFAULT_WORKSPACE_REVIEWERS,
incorporatedFeedbackMessage,
packageReadme,
type PackageManifestSource,
Expand Down Expand Up @@ -472,10 +471,12 @@ export function createPgApprovalRoundStore(
eq(settingsRecord.scopeKey, WORKSPACE_REVIEWERS_SCOPE_KEY),
),
);
const stored =
row === undefined ? null : parseWorkspaceReviewersField(row.fields);
if (stored !== null) return stored;
return DEFAULT_WORKSPACE_REVIEWERS.map((entry) => ({ ...entry }));
if (row === undefined) return [];
const reviewers = parseWorkspaceReviewersField(row.fields);
// Every writer stores the shape read here, so a row that does not parse is a bug to surface, not "no reviewers".
if (reviewers === null)
throw new Error("Workspace reviewers setting is not readable");
return reviewers;
}

return {
Expand Down
24 changes: 0 additions & 24 deletions apps/hub/src/config/approval-rounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,6 @@ export type PackageManifestSource = {
title?: string;
};

export const DEFAULT_WORKSPACE_REVIEWERS = [
{
id: "ws-rev-dana-chen",
name: "Dana Chen",
persona: "CEO",
email: "dana.chen@example.com",
goal: "Approve Aurora Weather Studio scope, timeline, and Q3 budget to begin the Coder phase",
},
{
id: "ws-rev-marcus-vance",
name: "Marcus Vance",
persona: "CTO",
email: "marcus.vance@example.com",
goal: "Sign off on radar tile payload budget, latency SLAs, and offline caching architecture",
},
{
id: "ws-rev-priya-patel",
name: "Priya Patel",
persona: "VP Product",
email: "priya.patel@example.com",
goal: "Confirm user journey, severe alert triage flow, and accessibility targets",
},
] as const;

export function packageReadme(input: {
projectTitle: string;
packageTitle: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1273,7 +1273,7 @@ export const wording = {
},
reviewerRowLabel: (index: number) => `Reviewer ${String(index)}`,
reviewerName: "Reviewer name",
reviewerNamePlaceholder: "Dana Chen",
reviewerNamePlaceholder: "Reviewer name",
reviewerPersona: "Persona",
reviewerPersonaPlaceholder: "Select role or persona…",
reviewerPersonas: [
Expand Down
53 changes: 38 additions & 15 deletions apps/web/src/pages/workspace/approval-rounds.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { WhatsNewDialog } from "../../App.tsx";
import { wording } from "../../config.ts";
import { queryClient } from "../../queries/client.ts";
import { Settings, WorkspaceApprovals } from "../settings.tsx";
import {
readWorkspaceReviewers,
writeWorkspaceReviewers,
} from "../../workspace-approvals-setting.ts";

const realArtifacts = { ...actualArtifacts };
const realConversation = { ...actualConversation };
Expand Down Expand Up @@ -1082,11 +1086,7 @@ describe("approval rounds panel", () => {
expect(html).toContain(
'class="approval-inline-fields settings-reviewer-form"',
);
expect(html).toContain('aria-label="Reviewer 1 Pick from workspace"');
expect(html).toContain("Choose saved reviewer…");
expect(html).toContain("Dana Chen (CEO)");
expect(html).toContain("Marcus Vance (CTO)");
expect(html).toContain("Priya Patel (VP Product)");
expect(html).not.toContain("Pick from workspace");
expect(html).toContain('aria-label="Reviewer 1 Reviewer name"');
expect(html).toContain('aria-label="Reviewer 1 Persona"');
expect(html).toContain("Budget approver");
Expand All @@ -1104,28 +1104,51 @@ describe("approval rounds panel", () => {
expect(html).toContain("Cancel");
});

test("renders WorkspaceApprovals settings section with require stakeholder approval toggle and seeded workspace reviewers", () => {
test("renders WorkspaceApprovals settings section with require stakeholder approval toggle and no workspace reviewers until some are saved", () => {
const html = renderToStaticMarkup(<WorkspaceApprovals />);
expect(html).toContain("Workspace approvals");
expect(html).toContain("Require stakeholder approval");
expect(html).toContain("Workspace reviewers");
expect(html).toContain("Dana Chen");
expect(html).toContain('<span class="approval-status-text">CEO</span>');
expect(html).toContain("Marcus Vance");
expect(html).toContain('<span class="approval-status-text">CTO</span>');
expect(html).toContain("Priya Patel");
expect(html).toContain(
'<span class="approval-status-text">VP Product</span>',
);
expect(html).toContain("No saved workspace reviewers yet.");
expect(html).not.toContain("invite-form");
expect(html).toContain("settings-reviewer-form");
expect(html).toContain("approval-lean-input");
expect(html).toContain("approval-lean-select");
expect(html).toContain("Remove");
expect(html).toContain("Add reviewer");
expect(html).toContain("Other…");
});

test("lists saved workspace reviewers in Settings and the share dialog picker", () => {
writeWorkspaceReviewers([
{ id: "ws-rev-jordan-lee", name: "Jordan Lee", persona: "CTO" },
]);
readWorkspaceReviewers();
try {
const settingsHtml = renderToStaticMarkup(<WorkspaceApprovals />);
expect(settingsHtml).toContain("Jordan Lee");
expect(settingsHtml).toContain(
'<span class="approval-status-text">CTO</span>',
);
expect(settingsHtml).toContain("Remove");
const dialogHtml = renderToStaticMarkup(
<SharePackageDialog
projectId="proj_1"
packageVersion={1}
onCreated={() => {}}
onCancel={() => {}}
onFault={() => {}}
/>,
);
expect(dialogHtml).toContain(
'aria-label="Reviewer 1 Pick from workspace"',
);
expect(dialogHtml).toContain("Jordan Lee (CTO)");
} finally {
writeWorkspaceReviewers([]);
readWorkspaceReviewers();
}
});

test("renders WhatsNewDialog with Slide Decks and Approvals tabs, where Slide Decks is built-in and only Approvals is Experimental", () => {
const slideDecksHtml = renderToStaticMarkup(
<WhatsNewDialog
Expand Down
36 changes: 5 additions & 31 deletions apps/web/src/workspace-approvals-setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,34 +35,8 @@ export function writeWorkspaceApprovalsEnabled(choice: OnOff): void {
notifyReviewers();
}

/** Seeded workspace reviewers available out of the box across all projects in the workspace. */
export const DEFAULT_WORKSPACE_REVIEWERS: readonly WorkspaceReviewer[] = [
{
id: "ws-rev-dana-chen",
name: "Dana Chen",
persona: "CEO",
email: "dana.chen@example.com",
goal: "Approve Aurora Weather Studio scope, timeline, and Q3 budget to begin the Coder phase",
},
{
id: "ws-rev-marcus-vance",
name: "Marcus Vance",
persona: "CTO",
email: "marcus.vance@example.com",
goal: "Sign off on radar tile payload budget, latency SLAs, and offline caching architecture",
},
{
id: "ws-rev-priya-patel",
name: "Priya Patel",
persona: "VP Product",
email: "priya.patel@example.com",
goal: "Confirm user journey, severe alert triage flow, and accessibility targets",
},
];

let cachedReviewersRaw: string | null = null;
let cachedReviewersList: readonly WorkspaceReviewer[] =
DEFAULT_WORKSPACE_REVIEWERS;
let cachedReviewersList: readonly WorkspaceReviewer[] = [];

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
Expand Down Expand Up @@ -94,13 +68,13 @@ function parseReviewers(raw: string | null): readonly WorkspaceReviewer[] {
if (raw === cachedReviewersRaw) return cachedReviewersList;
cachedReviewersRaw = raw;
if (raw === null) {
cachedReviewersList = DEFAULT_WORKSPACE_REVIEWERS;
cachedReviewersList = [];
return cachedReviewersList;
}
try {
const parsed: unknown = JSON.parse(raw);
if (!Array.isArray(parsed)) {
cachedReviewersList = DEFAULT_WORKSPACE_REVIEWERS;
cachedReviewersList = [];
return cachedReviewersList;
}
const valid: WorkspaceReviewer[] = [];
Expand All @@ -114,7 +88,7 @@ function parseReviewers(raw: string | null): readonly WorkspaceReviewer[] {
return cachedReviewersList;
} catch (error) {
console.warn("Invalid stored workspace reviewers JSON.", error);
cachedReviewersList = DEFAULT_WORKSPACE_REVIEWERS;
cachedReviewersList = [];
return cachedReviewersList;
}
}
Expand Down Expand Up @@ -226,7 +200,7 @@ export function useWorkspaceReviewers(): readonly WorkspaceReviewer[] {
return useSyncExternalStore(
subscribeReviewers,
readWorkspaceReviewers,
() => DEFAULT_WORKSPACE_REVIEWERS,
() => cachedReviewersList,
);
}

Expand Down
3 changes: 2 additions & 1 deletion docs/DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ it to one more than the previous entry's. Delete this paragraph after that date.
workspace tenant in Postgres, or creating a local development workspace if the
database is empty) with up to three realistic dummy projects across any stage
(`idea`, `brief`, `planning`, `design_options`, `designed`, `ready_to_build`,
`shipped`):
`shipped`), and gives a workspace with no saved reviewers three demo
reviewers:

```sh
bun run dev:seed # all 3 fixtures at their default stages
Expand Down
32 changes: 32 additions & 0 deletions scripts/seed-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ApprovalRound,
type ApprovalRound as ApprovalRoundType,
type ExpiryPreset,
type WorkspaceReviewer,
} from "@corbits-builder/schemas/approval-round";
import {
AppBundle,
Expand Down Expand Up @@ -58,6 +59,7 @@ import {
SEED_FIXTURE_IDS,
SEED_PROJECT_FIXTURES,
SEED_STAGES,
SEED_WORKSPACE_REVIEWERS,
getSeedProjectFixture,
loadSeedProjectFixture,
loadSeedProjectFixtures,
Expand Down Expand Up @@ -168,6 +170,16 @@ const approvalRoundTable = pgTable("sb_approval_round", {
.defaultNow(),
});

const settingsTable = pgTable(
"sb_settings",
{
tenantId: text("tenant_id").notNull(),
scopeKey: text("scope_key").notNull(),
fields: jsonb("fields").notNull(),
},
(table) => [primaryKey({ columns: [table.tenantId, table.scopeKey] })],
);

export interface ResolvedWorkspaceTenant {
tenantId: string;
principalId: string;
Expand Down Expand Up @@ -196,6 +208,10 @@ export interface SeedApprovalRoundInput {

export interface SeedDeps {
resolveWorkspaceTenant(tenantId?: string): Promise<ResolvedWorkspaceTenant>;
seedWorkspaceReviewers(
workspace: ResolvedWorkspaceTenant,
reviewers: readonly WorkspaceReviewer[],
): Promise<void>;
createProject(
workspace: ResolvedWorkspaceTenant,
input: {
Expand Down Expand Up @@ -283,6 +299,7 @@ export async function seedDevelopmentProjects(
const approve: boolean = options.approve ?? true;
const fixtures = await loadSeedProjectFixtures(selector);
const workspace = await deps.resolveWorkspaceTenant(options.tenantId);
await deps.seedWorkspaceReviewers(workspace, SEED_WORKSPACE_REVIEWERS);

const projects: SeededProjectSummary[] = [];

Expand Down Expand Up @@ -670,6 +687,21 @@ export function createDatabaseSeedDeps(
return resolveOrCreateWorkspaceTenant(db, principalKeyStore, tenantId);
},

async seedWorkspaceReviewers(workspace, reviewers) {
await db
.insert(settingsTable)
.values({
tenantId: workspace.tenantId,
scopeKey: "workspace:reviewers",
fields: {
providerNames: { value: null, version: 0 },
title: { value: null, version: 0 },
reviewers,
},
})
Comment thread
TheGreatAxios marked this conversation as resolved.
Resolved
.onConflictDoNothing();
},

async createProject(workspace, input) {
const projectId = crypto.randomUUID();
const child = await provisionChildTenant(
Expand Down
2 changes: 2 additions & 0 deletions test/integration/seed-projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ function createInMemorySeedDeps(): SeedDeps {
};
},

async seedWorkspaceReviewers() {},

async createProject(workspace, input) {
const projectId = `project-${input.fixtureId}`;
projects.set(projectId, {
Expand Down
25 changes: 25 additions & 0 deletions test/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ExpiryPreset,
ReviewerDecision,
TriageItemStatus,
WorkspaceReviewer,
} from "@corbits-builder/schemas/approval-round";
import {
ApprovalPackageBundle,
Expand Down Expand Up @@ -44,6 +45,30 @@ export type SeedFixtureId = (typeof SEED_FIXTURE_IDS)[number];

export type SeedFixtureSelector = SeedFixtureId | "all";

export const SEED_WORKSPACE_REVIEWERS: readonly WorkspaceReviewer[] = [
{
id: "ws-rev-dana-chen",
name: "Dana Chen",
persona: "CEO",
email: "dana.chen@example.com",
goal: "Approve Aurora Weather Studio scope, timeline, and Q3 budget to begin the Coder phase",
},
{
id: "ws-rev-marcus-vance",
name: "Marcus Vance",
persona: "CTO",
email: "marcus.vance@example.com",
goal: "Sign off on radar tile payload budget, latency SLAs, and offline caching architecture",
},
{
id: "ws-rev-priya-patel",
name: "Priya Patel",
persona: "VP Product",
email: "priya.patel@example.com",
goal: "Confirm user journey, severe alert triage flow, and accessibility targets",
},
];

export type SeedDesignOptionMetadata = Readonly<{
conceptId: string;
title: string;
Expand Down