From 110cb189e3651b96f0e54af7a904416df37a52e6 Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sun, 18 Jan 2026 15:31:54 +0000 Subject: [PATCH] at moment of invite-new-collaborator, allow specifying of a nickname/displayname --- .../[repo]/[branch]/files/[path]/route.ts | 40 +++++++++++++++++-- components/collaborators.tsx | 24 ++++++++--- .../0003_add_collaborator_nickname.sql | 1 + db/migrations/meta/_journal.json | 7 ++++ db/schema.ts | 1 + lib/actions/collaborator.ts | 2 + 6 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 db/migrations/0003_add_collaborator_nickname.sql diff --git a/app/api/[owner]/[repo]/[branch]/files/[path]/route.ts b/app/api/[owner]/[repo]/[branch]/files/[path]/route.ts index 1a4b537a6..7eea63bf5 100644 --- a/app/api/[owner]/[repo]/[branch]/files/[path]/route.ts +++ b/app/api/[owner]/[repo]/[branch]/files/[path]/route.ts @@ -10,6 +10,9 @@ import { getAuth } from "@/lib/auth"; import { getToken } from "@/lib/token"; import { updateFileCache } from "@/lib/githubCache"; import mergeWith from "lodash.mergewith"; +import { db } from "@/db"; +import { collaboratorTable } from "@/db/schema"; +import { and, eq } from "drizzle-orm"; /** * Create, update and delete individual files in a GitHub repository. @@ -31,6 +34,19 @@ export async function POST( const token = await getToken(user, params.owner, params.repo); if (!token) throw new Error("Token not found"); + // Look up display name for commit message attribution + let displayName = user?.githubUsername || user?.githubName || ''; + if (user?.id) { + const collab = await db.query.collaboratorTable.findFirst({ + where: and( + eq(collaboratorTable.owner, params.owner), + eq(collaboratorTable.repo, params.repo), + eq(collaboratorTable.userId, user.id) + ) + }); + if (collab?.nickname) displayName = collab.nickname; + } + const normalizedPath = normalizePath(params.path); const config = await getConfig(params.owner, params.repo, params.branch); @@ -173,7 +189,7 @@ export async function POST( throw new Error(`Invalid type "${data.type}".`); } - const response = await githubSaveFile(token, params.owner, params.repo, params.branch, normalizedPath, contentBase64, data.sha); + const response = await githubSaveFile(token, params.owner, params.repo, params.branch, normalizedPath, contentBase64, data.sha, displayName); const savedPath = response?.data.content?.path; @@ -249,7 +265,9 @@ const githubSaveFile = async ( path: string, contentBase64: string, sha?: string, + displayName?: string, ) => { + const attribution = displayName ? `${displayName} via Pages CMS` : 'via Pages CMS'; // We disable retries for 409 errors as it means the file has changed (conflict on SHA) const octokit = createOctokitInstance(token, { retry: { doNotRetry: [409] } }); @@ -259,7 +277,7 @@ const githubSaveFile = async ( owner, repo, path, - message: sha ? `Update ${path} (via Pages CMS)` : `Create ${path} (via Pages CMS)`, + message: sha ? `Update ${path} (${attribution})` : `Create ${path} (${attribution})`, content: contentBase64, branch, sha: sha || undefined, @@ -305,7 +323,7 @@ const githubSaveFile = async ( owner, repo, path: newPath, - message: `Create ${newPath} (via Pages CMS)`, + message: `Create ${newPath} (${attribution})`, content: contentBase64, branch, }); @@ -334,6 +352,20 @@ export async function DELETE( const token = await getToken(user, params.owner, params.repo); if (!token) throw new Error("Token not found"); + // Look up display name for commit message attribution + let displayName = user?.githubUsername || user?.githubName || ''; + if (user?.id) { + const collab = await db.query.collaboratorTable.findFirst({ + where: and( + eq(collaboratorTable.owner, params.owner), + eq(collaboratorTable.repo, params.repo), + eq(collaboratorTable.userId, user.id) + ) + }); + if (collab?.nickname) displayName = collab.nickname; + } + const attribution = displayName ? `${displayName} via Pages CMS` : 'via Pages CMS'; + if (params.path === ".pages.yml") throw new Error(`Deleting the settings file isn't allowed.`); const searchParams = request.nextUrl.searchParams; @@ -388,7 +420,7 @@ export async function DELETE( branch: params.branch, path: params.path, sha: sha, - message: `Delete ${params.path} (via Pages CMS)`, + message: `Delete ${params.path} (${attribution})`, }); // Update cache after successful deletion diff --git a/components/collaborators.tsx b/components/collaborators.tsx index eaec28263..d16de928f 100644 --- a/components/collaborators.tsx +++ b/components/collaborators.tsx @@ -45,6 +45,7 @@ export function Collaborators({ const [collaborators, setCollaborators] = useState([]); const [addCollaboratorState, addCollaboratorAction] = useFormState(handleAddCollaborator, { message: "", data: [] }); const [email, setEmail] = useState(""); + const [nickname, setNickname] = useState(""); const [removing, setRemoving] = useState([]); const [isLoading, setIsLoading] = useState(true); // TODO: remove this, we can probably let error.tsx catch that @@ -112,6 +113,7 @@ export function Collaborators({ toast.success(addCollaboratorState.message, { duration: 10000}); setEmail(""); + setNickname(""); } }, [addCollaboratorState, addNewCollaborator]); @@ -153,7 +155,8 @@ export function Collaborators({
- {collaborator.email} + {collaborator.nickname || collaborator.email} + {collaborator.nickname && ({collaborator.email})}
@@ -193,7 +196,7 @@ export function Collaborators({ }
-
+
setEmail(e.target.value)} required /> - {addCollaboratorState?.error && -
{addCollaboratorState.error}
- } +
+
+ setNickname(e.target.value)} + />
- Invite by email + Invite + {addCollaboratorState?.error && +
{addCollaboratorState.error}
+ }
) } \ No newline at end of file diff --git a/db/migrations/0003_add_collaborator_nickname.sql b/db/migrations/0003_add_collaborator_nickname.sql new file mode 100644 index 000000000..a8b958ab5 --- /dev/null +++ b/db/migrations/0003_add_collaborator_nickname.sql @@ -0,0 +1 @@ +ALTER TABLE "collaborator" ADD COLUMN IF NOT EXISTS "nickname" text; diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 52eab6033..cfa50d488 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1752656062455, "tag": "0002_sweet_molly_hayes", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1737215000000, + "tag": "0003_add_collaborator_nickname", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/schema.ts b/db/schema.ts index 1de0fe07f..e84e3d79f 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -59,6 +59,7 @@ const collaboratorTable = pgTable("collaborator", { owner: text("owner").notNull(), repo: text("repo").notNull(), branch: text("branch"), + nickname: text("nickname"), email: text("email").notNull(), userId: text("user_id").references(() => userTable.id), invitedBy: text("invited_by").notNull().references(() => userTable.id) diff --git a/lib/actions/collaborator.ts b/lib/actions/collaborator.ts index 0d5a9843b..00b98f40b 100644 --- a/lib/actions/collaborator.ts +++ b/lib/actions/collaborator.ts @@ -35,6 +35,7 @@ const handleAddCollaborator = async (prevState: any, formData: FormData) => { if (!emailValidation.success) throw new Error ("Invalid email"); const email = emailValidation.data; + const nickname = formData.get("nickname")?.toString().trim() || null; const token = await getUserToken(); if (!token) throw new Error("Token not found"); @@ -89,6 +90,7 @@ const handleAddCollaborator = async (prevState: any, formData: FormData) => { repoId: installationRepos[0].id, owner: installationRepos[0].owner.login, repo: installationRepos[0].name, + nickname, email, invitedBy: user.id }).returning();