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
4 changes: 3 additions & 1 deletion templates/design/actions/list-designs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const PREVIEW_MAX_BYTES = 50_000;
export default defineAction({
description:
"List all design projects accessible to the current user. " +
"Returns title, id, project type, and timestamps.",
"Returns title, id, project type, owner email, and timestamps.",
schema: z.object({
compact: z
.enum(["true", "false"])
Expand Down Expand Up @@ -43,6 +43,7 @@ export default defineAction({
projectType: schema.designs.projectType,
designSystemId: schema.designs.designSystemId,
visibility: schema.designs.visibility,
ownerEmail: schema.designs.ownerEmail,
createdAt: schema.designs.createdAt,
updatedAt: schema.designs.updatedAt,
})
Expand Down Expand Up @@ -106,6 +107,7 @@ export default defineAction({
projectType: row.projectType,
designSystemId: row.designSystemId,
visibility: row.visibility,
ownerEmail: row.ownerEmail,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
Expand Down
78 changes: 78 additions & 0 deletions templates/design/app/i18n-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,9 @@ const enUS = {
newDesign: "New Design",
newDesignLower: "New design",
createDesignProject: "Create a design project",
createdBy: "Created by",
allAuthors: "All authors",
me: "Me",
openingDesign: "Opening design...",
skipToEditor: "Skip to editor",
failedToCreateDesign: "Failed to create design",
Expand Down Expand Up @@ -15375,6 +15378,72 @@ const designComponentSourceOverrides = {
},
} satisfies Record<Exclude<LocaleCode, "en-US" | "zh-TW">, PartialMessages>;

const designAuthorFilterOverrides = {
"zh-CN": {
home: {
createdBy: "创建者",
allAuthors: "所有作者",
me: "我",
},
},
"es-ES": {
home: {
createdBy: "Creado por",
allAuthors: "Todos los autores",
me: "Yo",
},
},
"fr-FR": {
home: {
createdBy: "Créé par",
allAuthors: "Tous les auteurs",
me: "Moi",
},
},
"de-DE": {
home: {
createdBy: "Erstellt von",
allAuthors: "Alle Autoren",
me: "Ich",
},
},
"ja-JP": {
home: {
createdBy: "作成者",
allAuthors: "すべての作成者",
me: "自分",
},
},
"ko-KR": {
home: {
createdBy: "만든 사람",
allAuthors: "모든 작성자",
me: "나",
},
},
"pt-BR": {
home: {
createdBy: "Criado por",
allAuthors: "Todos os autores",
me: "Eu",
},
},
"hi-IN": {
home: {
createdBy: "द्वारा बनाया गया",
allAuthors: "सभी लेखक",
me: "मैं",
},
},
"ar-SA": {
home: {
createdBy: "أنشأه",
allAuthors: "جميع المؤلفين",
me: "أنا",
},
},
} satisfies Record<Exclude<LocaleCode, "en-US" | "zh-TW">, PartialMessages>;

export const messagesByLocale = {
"en-US": enUS,
"zh-TW": mergeLocalizedMessages(
Expand Down Expand Up @@ -15429,6 +15498,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["zh-CN"],
designComponentInstanceOverrides["zh-CN"],
designComponentSourceOverrides["zh-CN"],
designAuthorFilterOverrides["zh-CN"],
{
root: {
commandActions: "操作",
Expand Down Expand Up @@ -15508,6 +15578,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["es-ES"],
designComponentInstanceOverrides["es-ES"],
designComponentSourceOverrides["es-ES"],
designAuthorFilterOverrides["es-ES"],
{
root: {
commandActions: "Acciones",
Expand Down Expand Up @@ -15589,6 +15660,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["fr-FR"],
designComponentInstanceOverrides["fr-FR"],
designComponentSourceOverrides["fr-FR"],
designAuthorFilterOverrides["fr-FR"],
{
root: {
commandActions: "Actions",
Expand Down Expand Up @@ -15670,6 +15742,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["de-DE"],
designComponentInstanceOverrides["de-DE"],
designComponentSourceOverrides["de-DE"],
designAuthorFilterOverrides["de-DE"],
{
root: {
commandActions: "Aktionen",
Expand Down Expand Up @@ -15751,6 +15824,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["ja-JP"],
designComponentInstanceOverrides["ja-JP"],
designComponentSourceOverrides["ja-JP"],
designAuthorFilterOverrides["ja-JP"],
{
root: {
commandActions: "操作",
Expand Down Expand Up @@ -15833,6 +15907,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["ko-KR"],
designComponentInstanceOverrides["ko-KR"],
designComponentSourceOverrides["ko-KR"],
designAuthorFilterOverrides["ko-KR"],
{
root: {
commandActions: "작업",
Expand Down Expand Up @@ -15913,6 +15988,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["pt-BR"],
designComponentInstanceOverrides["pt-BR"],
designComponentSourceOverrides["pt-BR"],
designAuthorFilterOverrides["pt-BR"],
{
root: {
commandActions: "Ações",
Expand Down Expand Up @@ -15994,6 +16070,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["hi-IN"],
designComponentInstanceOverrides["hi-IN"],
designComponentSourceOverrides["hi-IN"],
designAuthorFilterOverrides["hi-IN"],
{
root: {
commandActions: "क्रियाएं",
Expand Down Expand Up @@ -16075,6 +16152,7 @@ export const messagesByLocale = {
designRuntimeIdentityAndBridgeOverrides["ar-SA"],
designComponentInstanceOverrides["ar-SA"],
designComponentSourceOverrides["ar-SA"],
designAuthorFilterOverrides["ar-SA"],
{
root: {
commandActions: "الإجراءات",
Expand Down
3 changes: 3 additions & 0 deletions templates/design/app/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,9 @@ const messages = {
newDesign: "新Design",
newDesignLower: "新設計",
createDesignProject: "建立一個設計專案",
createdBy: "建立者",
allAuthors: "所有作者",
me: "我",
openingDesign: "正在開啟設計...",
skipToEditor: "直接進入編輯器",
failedToCreateDesign: "無法建立設計",
Expand Down
84 changes: 84 additions & 0 deletions templates/design/app/lib/design-authors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";

import {
ALL_AUTHORS,
collectAuthorEmails,
filterDesignsByAuthor,
MY_DESIGNS,
shouldShowAuthors,
} from "./design-authors";

const designs = [
{ id: "a", ownerEmail: "zoe@example.com" },
{ id: "b", ownerEmail: "adam@example.com" },
{ id: "c", ownerEmail: "Zoe@Example.com" },
{ id: "d", ownerEmail: null },
];

describe("collectAuthorEmails", () => {
it("dedupes case variants of one author and orders by display name", () => {
expect(collectAuthorEmails(designs)).toEqual([
"adam@example.com",
"zoe@example.com",
]);
});

it("returns no authors when nothing records an owner", () => {
expect(collectAuthorEmails([{ ownerEmail: null }, {}])).toEqual([]);
});
});

describe("shouldShowAuthors", () => {
it("stays hidden for a solo workspace with one author", () => {
expect(
shouldShowAuthors({
orgMemberCount: 1,
authorEmails: ["solo@example.com"],
}),
).toBe(false);
});

it("shows for a team whose designs all belong to one member", () => {
expect(
shouldShowAuthors({
orgMemberCount: 4,
authorEmails: ["solo@example.com"],
}),
).toBe(true);
});

it("shows when a second author appears without any org", () => {
expect(
shouldShowAuthors({
orgMemberCount: undefined,
authorEmails: ["a@example.com", "b@example.com"],
}),
).toBe(true);
});
});

describe("filterDesignsByAuthor", () => {
it("keeps every design for the all-authors value", () => {
expect(
filterDesignsByAuthor(designs, ALL_AUTHORS, "zoe@example.com"),
).toEqual(designs);
});

it("matches one author regardless of stored email casing", () => {
expect(
filterDesignsByAuthor(designs, "zoe@example.com", null).map((d) => d.id),
).toEqual(["a", "c"]);
});

it("resolves the mine value against the viewer email", () => {
expect(
filterDesignsByAuthor(designs, MY_DESIGNS, " Adam@example.com ").map(
(d) => d.id,
),
).toEqual(["b"]);
});

it("returns nothing for mine when the viewer is unknown", () => {
expect(filterDesignsByAuthor(designs, MY_DESIGNS, null)).toEqual([]);
});
});
64 changes: 64 additions & 0 deletions templates/design/app/lib/design-authors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { emailToName } from "@agent-native/core/client/collab";

/** Author-filter values that cannot collide with a real owner email. */
export const ALL_AUTHORS = "all";
export const MY_DESIGNS = "me";

export interface AuthoredDesign {
ownerEmail?: string | null;
}

export function normalizeAuthorEmail(
email: string | null | undefined,
): string | null {
const trimmed = email?.trim();
return trimmed ? trimmed.toLowerCase() : null;
}

/** Distinct owner emails across the whole library, ordered by display name. */
export function collectAuthorEmails(designs: AuthoredDesign[]): string[] {
const byNormalized = new Map<string, string>();
for (const design of designs) {
const email = design.ownerEmail?.trim();
if (!email) continue;
const normalized = email.toLowerCase();
if (!byNormalized.has(normalized)) byNormalized.set(normalized, email);
}
return Array.from(byNormalized.values()).sort((a, b) =>
emailToName(a).localeCompare(emailToName(b)),
);
}

/**
* Whether author bylines are worth showing on cards. A solo workspace would
* only ever repeat the viewer's own name, so bylines stay hidden until a
* second person is involved — either as an org member or as the owner of a
* design the viewer can see.
*/
export function shouldShowAuthors(input: {
orgMemberCount: number | undefined;
authorEmails: string[];
}): boolean {
return (input.orgMemberCount ?? 0) > 1 || input.authorEmails.length > 1;
}

export function filterDesignsByAuthor<T extends AuthoredDesign>(
designs: T[],
author: string,
viewerEmail: string | null | undefined,
): T[] {
if (author === ALL_AUTHORS) return designs;
const normalizedViewer = normalizeAuthorEmail(viewerEmail);
// An unknown viewer cannot own anything, so "mine" is empty rather than
// silently falling back to every design.
if (author === MY_DESIGNS) {
if (!normalizedViewer) return [];
return designs.filter(
(design) => normalizeAuthorEmail(design.ownerEmail) === normalizedViewer,
);
}
const normalizedAuthor = normalizeAuthorEmail(author);
return designs.filter(
(design) => normalizeAuthorEmail(design.ownerEmail) === normalizedAuthor,
);
}
11 changes: 11 additions & 0 deletions templates/design/app/pages/Index.skip-to-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ vi.mock("@agent-native/core/client/feature-flags", () => ({
useFeatureFlag: () => false,
}));

vi.mock("@agent-native/core/client/collab", () => ({
emailToColor: () => "#000000",
emailToName: (email: string) => email,
}));

vi.mock("@agent-native/core/client/org", () => ({
useOrgMembers: () => ({ data: undefined }),
}));

vi.mock("@agent-native/core/client/hooks", () => ({
useActionQuery: (name: string) => {
if (name === "list-designs") {
Expand Down Expand Up @@ -65,6 +74,8 @@ vi.mock("@agent-native/core/client/hooks", () => ({
: vi.fn().mockResolvedValue(undefined),
mutate: vi.fn(),
}),
useSession: () => ({ session: null, isLoading: false }),
useAvatarUrl: () => null,
}));

vi.mock("@agent-native/core/client/i18n", () => ({
Expand Down
Loading
Loading