Skip to content
Open
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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default defineConfig({
"**/reaction-names.spec.ts",
"**/inbox-reactions.spec.ts",
"**/send-channel-binding.spec.ts",
"**/projects-empty.spec.ts",
"**/project-commit-detail.spec.ts",
"**/project-inbox.spec.ts",
"**/project-pr-review.spec.ts",
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/features/projects/ui/ProjectCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ function StatusPill({ status }: { status: string }) {
);
}

export function EmptyState() {
export function EmptyState({ onCreate }: { onCreate: () => void }) {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-4 py-16 text-center">
<FolderGit2 className="h-10 w-10 text-muted-foreground/40" />
Expand All @@ -273,6 +273,9 @@ export function EmptyState() {
Projects published to this relay will appear here.
</p>
</div>
<Button onClick={onCreate} type="button">
Create repository
</Button>
</div>
);
}
Expand Down
8 changes: 3 additions & 5 deletions desktop/src/features/projects/ui/ProjectsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,6 @@ export function ProjectsView() {
);
}

if (projects.length === 0) {
return <EmptyState />;
}

const repositoryItems =
visibleProjects.length === 0 ? (
<EmptyFilteredState />
Expand Down Expand Up @@ -646,7 +642,9 @@ export function ProjectsView() {
</div>
<div className="mx-auto w-full max-w-6xl">
<div className="w-full min-w-0 pb-4 pt-4">
{filter === "all" ? (
{projects.length === 0 ? (
<EmptyState onCreate={() => setCreateProjectOpen(true)} />
) : filter === "all" ? (
<ProjectsOverviewPanel
localRepositoryCount={localProjectCount}
metadata={
Expand Down
16 changes: 12 additions & 4 deletions desktop/src/testing/e2eBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ type MockSearchProfileSeed = {
type E2eConfig = {
mode?: "mock" | "relay";
mock?: {
/** Start the mock relay without seeded NIP-34 repositories. */
emptyProjects?: boolean;
/** Advertised HEAD for the first mock project without adding that branch. */
projectHeadBranch?: string;
/** Builderlab account returned by hosted-community onboarding. Null/omitted = signed out. */
Expand Down Expand Up @@ -4992,8 +4994,11 @@ function buildMockProjectEvents(): RelayEvent[] {
const daySeconds = 86_400;
const now = Math.floor(Date.now() / 1000);
const historyDays = 26 * 7;
const projectSeeds = getConfig()?.mock?.emptyProjects
? []
: MOCK_PROJECT_SEEDS;

for (const [projectIndex, seed] of MOCK_PROJECT_SEEDS.entries()) {
for (const [projectIndex, seed] of projectSeeds.entries()) {
const owner =
projectIndex === 0
? (window.__BUZZ_E2E_PROJECT_OWNER_OVERRIDE__ ?? seed.owner)
Expand Down Expand Up @@ -5096,10 +5101,13 @@ function getMockProjectEventStore(): RelayEvent[] {
return mockProjectEventStore;
}

/** Project-scoped publishes (PR/issue comments, NIP-34 status events) carry
* a repo-address `a` tag instead of a channel `h` tag — store them with the
* seeded project events so refetches see them. */
/** Repository announcements and project-scoped publishes belong in the mock
* project store so refetches see newly-created repositories and work items. */
function isMockProjectScopedEvent(event: RelayEvent): boolean {
if (event.kind === KIND_REPO_ANNOUNCEMENT) {
return true;
}

const hasRepoAddressTag = event.tags.some(
(tag) => tag[0] === "a" && (tag[1] ?? "").startsWith("30617:"),
);
Expand Down
31 changes: 31 additions & 0 deletions desktop/tests/e2e/projects-empty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from "@playwright/test";

import { installMockBridge } from "../helpers/bridge";

test("first repository can be created from the empty projects shell", async ({
page,
}) => {
await installMockBridge(page, { emptyProjects: true });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();

await expect(
page.getByRole("heading", { level: 1, name: "Projects" }),
).toBeVisible();
await expect(page.getByTestId("projects-create-menu")).toBeVisible();
await expect(page.getByText("No projects yet")).toBeVisible();

await page.getByRole("button", { name: "Create repository" }).click();
await expect(page.getByTestId("create-project-dialog")).toBeVisible();

await page.getByTestId("create-project-name").fill("first-repository");
await page.getByTestId("create-project-submit").click();

await expect(page.getByTestId("create-project-dialog")).toBeHidden();
await expect(
page.getByText('Project "first-repository" created.'),
).toBeVisible();
await expect(
page.getByText("first-repository", { exact: true }),
).toBeVisible();
});
2 changes: 2 additions & 0 deletions desktop/tests/helpers/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export type MockAgentMemoryListing = {
};

type MockBridgeOptions = {
/** Start the mock relay without seeded NIP-34 repositories. */
emptyProjects?: boolean;
/** Advertised HEAD for the first mock project without adding that branch. */
projectHeadBranch?: string;
/** Relay NIP-11 identity used to sign authoritative repository state. */
Expand Down