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
40 changes: 36 additions & 4 deletions app/api/[owner]/[repo]/[branch]/files/[path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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] } });

Expand All @@ -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,
Expand Down Expand Up @@ -305,7 +323,7 @@ const githubSaveFile = async (
owner,
repo,
path: newPath,
message: `Create ${newPath} (via Pages CMS)`,
message: `Create ${newPath} (${attribution})`,
content: contentBase64,
branch,
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
24 changes: 18 additions & 6 deletions components/collaborators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function Collaborators({
const [collaborators, setCollaborators] = useState<any[]>([]);
const [addCollaboratorState, addCollaboratorAction] = useFormState(handleAddCollaborator, { message: "", data: [] });
const [email, setEmail] = useState<string>("");
const [nickname, setNickname] = useState<string>("");
const [removing, setRemoving] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(true);
// TODO: remove this, we can probably let error.tsx catch that
Expand Down Expand Up @@ -112,6 +113,7 @@ export function Collaborators({

toast.success(addCollaboratorState.message, { duration: 10000});
setEmail("");
setNickname("");
}
}, [addCollaboratorState, addNewCollaborator]);

Expand Down Expand Up @@ -153,7 +155,8 @@ export function Collaborators({
</AvatarFallback>
</Avatar>
<div className="font-medium text-left truncate">
{collaborator.email}
{collaborator.nickname || collaborator.email}
{collaborator.nickname && <span className="text-muted-foreground font-normal ml-1">({collaborator.email})</span>}
</div>
<AlertDialog>
<Tooltip>
Expand Down Expand Up @@ -193,7 +196,7 @@ export function Collaborators({
</div>
}
<form action={addCollaboratorAction} className="flex gap-x-2">
<div className="w-full">
<div className="flex-1">
<input type="hidden" name="owner" value={owner} />
<input type="hidden" name="repo" value={repo} />
<Input
Expand All @@ -204,14 +207,23 @@ export function Collaborators({
onChange={(e) => setEmail(e.target.value)}
required
/>
{addCollaboratorState?.error &&
<div className="text-sm font-medium text-red-500 mt-2 ">{addCollaboratorState.error}</div>
}
</div>
<div className="flex-1">
<Input
type="text"
name="nickname"
placeholder="Nickname (optional)"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
/>
</div>
<SubmitButton type="submit" disabled={isEmailInList}>
Invite by email
Invite
</SubmitButton>
</form>
{addCollaboratorState?.error &&
<div className="text-sm font-medium text-red-500">{addCollaboratorState.error}</div>
}
</div>
)
}
1 change: 1 addition & 0 deletions db/migrations/0003_add_collaborator_nickname.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "collaborator" ADD COLUMN IF NOT EXISTS "nickname" text;
7 changes: 7 additions & 0 deletions db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
1 change: 1 addition & 0 deletions db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions lib/actions/collaborator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down