mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
Adds avatar_url column to workspace, threads it through the API +
WorkspaceAvatar component, and adds a click-to-upload editor in the
workspace settings tab. Mirrors the squad avatar pattern (migration 086);
UI strings use "logo" while the schema/code uses avatar_url for codebase
consistency with user.avatar_url and squad.avatar_url.
- migration 093: ALTER TABLE workspace ADD COLUMN avatar_url TEXT
- UpdateWorkspace SQL + handler accept avatar_url (auth gated to
owner/admin at the router via RequireWorkspaceRoleFromURL)
- WorkspaceAvatar renders <img> when avatar_url is set, falls back to
the initial-letter span otherwise
- workspace-tab.tsx adds a 16x16 click-to-upload logo editor at the
top of the general settings card, using useFileUpload + accept=
image/png,image/jpeg,image/webp (server stores under workspaces/{id}/)
- en + zh-Hans settings i18n strings added
Co-authored-by: Matt Voska <voska@users.noreply.github.com>
94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import {
|
|
WorkspaceSlugProvider,
|
|
useWorkspaceSlug,
|
|
useCurrentWorkspace,
|
|
} from "@multica/core/paths";
|
|
import { workspaceKeys } from "@multica/core/workspace/queries";
|
|
import type { Workspace } from "@multica/core/types";
|
|
|
|
// Hook tests for @multica/core/paths live here because packages/core/ runs
|
|
// Vitest in node environment (no jsdom). packages/views/ already has jsdom +
|
|
// @testing-library/react configured, so it's the correct home per CLAUDE.md
|
|
// testing rules ("shared UI components live in packages/views/*.test.tsx").
|
|
|
|
function makeWorkspace(over: Partial<Workspace>): Workspace {
|
|
return {
|
|
id: "id-default",
|
|
name: "Default",
|
|
slug: "default",
|
|
description: null,
|
|
context: null,
|
|
settings: {},
|
|
repos: [],
|
|
issue_prefix: "DEF",
|
|
avatar_url: null,
|
|
created_at: "",
|
|
updated_at: "",
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function setup(slug: string | null, wsList: Workspace[] = []) {
|
|
const qc = new QueryClient();
|
|
qc.setQueryData(workspaceKeys.list(), wsList);
|
|
return function Wrapper({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<QueryClientProvider client={qc}>
|
|
<WorkspaceSlugProvider slug={slug}>{children}</WorkspaceSlugProvider>
|
|
</QueryClientProvider>
|
|
);
|
|
};
|
|
}
|
|
|
|
describe("useWorkspaceSlug", () => {
|
|
it("returns the provided slug", () => {
|
|
function Probe() {
|
|
return <div data-testid="slug">{useWorkspaceSlug() ?? "null"}</div>;
|
|
}
|
|
render(<Probe />, { wrapper: setup("acme") });
|
|
expect(screen.getByTestId("slug").textContent).toBe("acme");
|
|
});
|
|
|
|
it("returns null when no slug is provided", () => {
|
|
function Probe() {
|
|
return <div data-testid="slug">{useWorkspaceSlug() ?? "null"}</div>;
|
|
}
|
|
render(<Probe />, { wrapper: setup(null) });
|
|
expect(screen.getByTestId("slug").textContent).toBe("null");
|
|
});
|
|
});
|
|
|
|
describe("useCurrentWorkspace", () => {
|
|
const acme = makeWorkspace({ id: "id-1", slug: "acme", name: "Acme" });
|
|
|
|
it("resolves workspace from slug and list", () => {
|
|
function Probe() {
|
|
const ws = useCurrentWorkspace();
|
|
return <div data-testid="name">{ws?.name ?? "none"}</div>;
|
|
}
|
|
render(<Probe />, { wrapper: setup("acme", [acme]) });
|
|
expect(screen.getByTestId("name").textContent).toBe("Acme");
|
|
});
|
|
|
|
it("returns null when slug does not match any workspace", () => {
|
|
function Probe() {
|
|
const ws = useCurrentWorkspace();
|
|
return <div data-testid="name">{ws?.name ?? "none"}</div>;
|
|
}
|
|
render(<Probe />, { wrapper: setup("bogus", [acme]) });
|
|
expect(screen.getByTestId("name").textContent).toBe("none");
|
|
});
|
|
|
|
it("returns null when no slug is provided", () => {
|
|
function Probe() {
|
|
const ws = useCurrentWorkspace();
|
|
return <div data-testid="name">{ws?.name ?? "none"}</div>;
|
|
}
|
|
render(<Probe />, { wrapper: setup(null, [acme]) });
|
|
expect(screen.getByTestId("name").textContent).toBe("none");
|
|
});
|
|
});
|