Skip to content

Commit 9046499

Browse files
es-ce-que la on peut faire le syst de file Management ?
es-ce-que gen
1 parent 08f27c5 commit 9046499

4 files changed

Lines changed: 538 additions & 205 deletions

File tree

src/app/(app)/projects/[id]/actions.ts

Lines changed: 132 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
'use server';
33

4-
import type { Project, ProjectMember, ProjectMemberRole, Task, TaskStatus, Tag, Document as ProjectDocumentType, Announcement as ProjectAnnouncement, UserGithubOAuthToken } from '@/types';
4+
import type { Project, ProjectMember, ProjectMemberRole, Task, TaskStatus, Tag, Document as ProjectDocumentType, Announcement as ProjectAnnouncement, UserGithubOAuthToken, GithubRepoContentItem } from '@/types';
55
import {
66
getProjectByUuid as dbGetProjectByUuid,
77
getUserByUuid as dbGetUserByUuid,
@@ -13,7 +13,6 @@ import {
1313
getProjectMemberRole as dbGetProjectMemberRole,
1414
createTask as dbCreateTask,
1515
getTasksForProject as dbGetTasksForProject,
16-
updateTaskStatus as dbUpdateTaskStatus,
1716
updateTask as dbUpdateTask,
1817
deleteTask as dbDeleteTask,
1918
getProjectTags as dbGetProjectTags,
@@ -37,7 +36,7 @@ import { z } from 'zod';
3736
import { auth } from '@/lib/authEdge';
3837
import { Octokit } from 'octokit';
3938
import { Buffer } from 'buffer';
40-
import { getInstallationOctokit, getAppAuthOctokit } from '@/lib/githubAppClient';
39+
import { getAppAuthOctokit, getInstallationOctokit } from '@/lib/githubAppClient';
4140

4241

4342
export async function fetchProjectAction(uuid: string | undefined): Promise<Project | null> {
@@ -321,65 +320,6 @@ export async function fetchTasksAction(projectUuid: string | undefined): Promise
321320
}
322321
}
323322

324-
const UpdateTaskStatusSchema = z.object({
325-
taskUuid: z.string().uuid("Invalid task UUID."),
326-
projectUuid: z.string().uuid("Invalid project UUID."),
327-
status: z.enum(['To Do', 'In Progress', 'Done', 'Archived'] as [TaskStatus, ...TaskStatus[]]),
328-
});
329-
330-
export interface UpdateTaskStatusFormState {
331-
message?: string;
332-
error?: string;
333-
updatedTask?: Task;
334-
}
335-
336-
export async function updateTaskStatusAction(prevState: UpdateTaskStatusFormState, formData: FormData): Promise<UpdateTaskStatusFormState> {
337-
const session = await auth();
338-
if (!session?.user?.uuid) {
339-
console.error("[updateTaskStatusAction] Authentication required. No session user UUID.");
340-
return { error: "Authentication required." };
341-
}
342-
console.log("[updateTaskStatusAction] Authenticated user for permission check:", session.user.uuid);
343-
344-
const validatedFields = UpdateTaskStatusSchema.safeParse({
345-
taskUuid: formData.get('taskUuid'),
346-
projectUuid: formData.get('projectUuid'),
347-
status: formData.get('status'),
348-
});
349-
350-
if (!validatedFields.success) {
351-
return { error: "Invalid input: " + JSON.stringify(validatedFields.error.flatten().fieldErrors) };
352-
}
353-
const { taskUuid, projectUuid, status } = validatedFields.data;
354-
355-
try {
356-
const userRole = await dbGetProjectMemberRole(projectUuid, session.user.uuid);
357-
console.log(`[updateTaskStatusAction] User role check for project ${projectUuid} (user ${session.user.uuid}): ${userRole}`);
358-
if (!userRole) {
359-
return { error: `You are not a member of this project. Your role: ${userRole || 'not a member'}. UUID: ${session.user.uuid}` };
360-
}
361-
362-
const updatedTask = await dbUpdateTaskStatus(taskUuid, status);
363-
if (!updatedTask) {
364-
return { error: "Failed to update task status." };
365-
}
366-
return { message: "Task status updated successfully!", updatedTask };
367-
} catch (error: any) {
368-
console.error("Error updating task status:", error);
369-
return { error: error.message || "An unexpected error occurred." };
370-
}
371-
}
372-
373-
const UpdateTaskSchema = z.object({
374-
taskUuid: z.string().uuid("Invalid task UUID."),
375-
projectUuid: z.string().uuid("Invalid project UUID."),
376-
title: z.string().min(1, "Title is required.").max(255),
377-
description: z.string().optional(),
378-
todoListMarkdown: z.string().optional().default(''),
379-
status: z.enum(['To Do', 'In Progress', 'Done', 'Archived'] as [TaskStatus, ...TaskStatus[]]),
380-
assigneeUuid: z.string().uuid("Invalid Assignee UUID format.").optional().or(z.literal('')),
381-
tagsString: z.string().optional(),
382-
});
383323

384324
export interface UpdateTaskFormState {
385325
message?: string;
@@ -396,48 +336,74 @@ export async function updateTaskAction(prevState: UpdateTaskFormState, formData:
396336
}
397337
console.log("[updateTaskAction] Authenticated user for permission check:", session.user.uuid);
398338

339+
const UpdateTaskSchema = z.object({
340+
taskUuid: z.string().uuid("Invalid task UUID."),
341+
projectUuid: z.string().uuid("Invalid project UUID."),
342+
title: z.string().min(1, "Title is required.").max(255).optional(),
343+
description: z.string().optional(),
344+
todoListMarkdown: z.string().optional().default(''),
345+
status: z.enum(['To Do', 'In Progress', 'Done', 'Archived'] as [TaskStatus, ...TaskStatus[]]).optional(),
346+
assigneeUuid: z.string().uuid("Invalid Assignee UUID format.").optional().or(z.literal('')),
347+
tagsString: z.string().optional(),
348+
});
349+
399350

400351
const validatedFields = UpdateTaskSchema.safeParse({
401352
taskUuid: formData.get('taskUuid'),
402353
projectUuid: formData.get('projectUuid'),
403-
title: formData.get('title'),
404-
description: formData.get('description') || '',
405-
todoListMarkdown: formData.get('todoListMarkdown') || '',
406-
status: formData.get('status'),
407-
assigneeUuid: formData.get('assigneeUuid') || '',
408-
tagsString: formData.get('tagsString'),
354+
title: formData.has('title') ? formData.get('title') : undefined,
355+
description: formData.has('description') ? (formData.get('description') || '') : undefined,
356+
todoListMarkdown: formData.has('todoListMarkdown') ? (formData.get('todoListMarkdown') || '') : undefined,
357+
status: formData.has('status') ? formData.get('status') : undefined,
358+
assigneeUuid: formData.has('assigneeUuid') ? (formData.get('assigneeUuid') || '') : undefined,
359+
tagsString: formData.has('tagsString') ? formData.get('tagsString') : undefined,
409360
});
410361

411362
if (!validatedFields.success) {
412363
console.error("[updateTaskAction] Validation failed:", validatedFields.error.flatten().fieldErrors);
413364
return { error: "Invalid input.", fieldErrors: validatedFields.error.flatten().fieldErrors };
414365
}
415366

416-
const { taskUuid, projectUuid, title, description, todoListMarkdown, status, assigneeUuid: rawAssigneeUuid, tagsString } = validatedFields.data;
367+
const { taskUuid, projectUuid, ...taskUpdateData } = validatedFields.data;
417368

418-
let finalAssigneeUuid: string | null = null;
419-
if (rawAssigneeUuid && rawAssigneeUuid !== '' && rawAssigneeUuid !== '__UNASSIGNED__') {
420-
finalAssigneeUuid = rawAssigneeUuid;
369+
let finalAssigneeUuid: string | null | undefined = taskUpdateData.assigneeUuid;
370+
if (taskUpdateData.assigneeUuid === '__UNASSIGNED__') {
371+
finalAssigneeUuid = null;
372+
} else if (taskUpdateData.assigneeUuid === '') {
373+
finalAssigneeUuid = undefined; // To not update if not provided
421374
}
422375

376+
423377
try {
424378
const userRole = await dbGetProjectMemberRole(projectUuid, session.user.uuid);
425379
console.log(`[updateTaskAction] User role check for project ${projectUuid} (user ${session.user.uuid}): ${userRole}`);
426-
if (!userRole || !['owner', 'co-owner', 'editor'].includes(userRole)) {
427-
return { error: `You do not have permission to update tasks in this project. Your role: ${userRole || 'not a member'}. UUID: ${session.user.uuid}` };
380+
381+
const canUpdateStatusOnly = userRole && !['owner', 'co-owner', 'editor'].includes(userRole);
382+
const canUpdateFull = userRole && ['owner', 'co-owner', 'editor'].includes(userRole);
383+
384+
if (!canUpdateFull && !canUpdateStatusOnly) {
385+
return { error: `You do not have permission to update tasks in this project. Your role: ${userRole || 'not a member'}. UUID: ${session.user.uuid}` };
386+
}
387+
388+
const dataToUpdate: Partial<Omit<Task, 'uuid' | 'projectUuid' | 'createdAt' | 'updatedAt' | 'tags' | 'assigneeName'>> & { tagsString?: string } = {};
389+
390+
if (canUpdateFull) {
391+
if (taskUpdateData.title !== undefined) dataToUpdate.title = taskUpdateData.title;
392+
if (taskUpdateData.description !== undefined) dataToUpdate.description = taskUpdateData.description || undefined;
393+
if (taskUpdateData.todoListMarkdown !== undefined) dataToUpdate.todoListMarkdown = taskUpdateData.todoListMarkdown || undefined;
394+
if (finalAssigneeUuid !== undefined) dataToUpdate.assigneeUuid = finalAssigneeUuid; // Handles null for unassign
395+
if (taskUpdateData.tagsString !== undefined) dataToUpdate.tagsString = taskUpdateData.tagsString || undefined;
428396
}
429397

430-
const taskData: Partial<Omit<Task, 'uuid' | 'projectUuid' | 'createdAt' | 'updatedAt' | 'tags' | 'assigneeName'>> & { tagsString?: string } = {};
398+
// Status can always be updated by any member
399+
if (taskUpdateData.status !== undefined) dataToUpdate.status = taskUpdateData.status;
431400

432-
if (formData.has('title')) taskData.title = title;
433-
if (formData.has('description')) taskData.description = description || undefined;
434-
if (formData.has('todoListMarkdown')) taskData.todoListMarkdown = todoListMarkdown || undefined;
435-
if (formData.has('status')) taskData.status = status;
436-
if (formData.has('assigneeUuid')) taskData.assigneeUuid = finalAssigneeUuid;
437-
if (formData.has('tagsString')) taskData.tagsString = tagsString || undefined;
438401

402+
if (Object.keys(dataToUpdate).length === 0) {
403+
return { message: "No changes to update.", updatedTask: await dbGetTaskByUuid(taskUuid) || undefined };
404+
}
439405

440-
const updatedTask = await dbUpdateTask(taskUuid, taskData);
406+
const updatedTask = await dbUpdateTask(taskUuid, dataToUpdate);
441407
if (!updatedTask) {
442408
return { error: "Failed to update task."};
443409
}
@@ -564,8 +530,8 @@ async function updateReadmeOnGithub(octokit: Octokit, owner: string, repo: strin
564530
message: 'Update README.md from FlowUp',
565531
content: Buffer.from(content).toString('base64'),
566532
committer: {
567-
name: 'FlowUp Bot',
568-
email: 'bot@flowup.app',
533+
name: 'FlowUp Bot', // Consider making this configurable or using the user's GitHub name
534+
email: 'bot@flowup.app', // Or a no-reply / user's email
569535
},
570536
};
571537
if (existingSha) {
@@ -642,7 +608,9 @@ export async function saveProjectReadmeAction(prevState: SaveProjectReadmeFormSt
642608
repo,
643609
path: 'README.md',
644610
});
611+
// @ts-ignore // Octokit types can be tricky with union types for content
645612
if ('sha' in readmeData && readmeData.type === 'file') {
613+
// @ts-ignore
646614
existingSha = readmeData.sha;
647615
}
648616
} catch (error: any) {
@@ -1155,20 +1123,20 @@ export async function linkProjectToGithubAction(
11551123
}
11561124

11571125
const projectUuid = formData.get('projectUuid') as string;
1158-
const flowUpProjectNameValue = formData.get('flowUpProjectName');
1159-
const githubRepoNameValue = formData.get('githubRepoName');
1126+
const flowUpProjectNameValue = formData.get('flowUpProjectName') as string | null;
1127+
const githubRepoNameValue = formData.get('githubRepoName') as string | null;
11601128
const useDefaultRepoName = formData.get('useDefaultRepoName') === 'true';
11611129

11621130
let nameForRepoCreation: string;
11631131

11641132
if (useDefaultRepoName) {
1165-
if (typeof flowUpProjectNameValue === 'string' && flowUpProjectNameValue.trim() !== '') {
1133+
if (flowUpProjectNameValue && flowUpProjectNameValue.trim() !== '') {
11661134
nameForRepoCreation = `FlowUp - ${flowUpProjectNameValue}`;
11671135
} else {
11681136
return { error: "FlowUp Project Name is required to generate the default repository name." };
11691137
}
11701138
} else {
1171-
if (typeof githubRepoNameValue === 'string' && githubRepoNameValue.trim() !== '') {
1139+
if (githubRepoNameValue && githubRepoNameValue.trim() !== '') {
11721140
nameForRepoCreation = githubRepoNameValue;
11731141
} else {
11741142
return { error: "Custom repository name cannot be empty." };
@@ -1208,10 +1176,11 @@ export async function linkProjectToGithubAction(
12081176
name: repoSlug,
12091177
private: repoIsPrivate,
12101178
description: `Repository for FlowUp project: ${projectFromDb.name}`,
1211-
auto_init: true,
1179+
auto_init: true, // Initialize with a README to avoid empty repo issues
12121180
});
12131181
console.log(`Successfully created repository: ${createdRepo.data.html_url}`);
12141182

1183+
// Sync FlowUp README to GitHub if content exists
12151184
if (projectFromDb.readmeContent && projectFromDb.readmeContent.trim() !== '') {
12161185
let existingReadmeSha: string | undefined = undefined;
12171186
try {
@@ -1220,7 +1189,9 @@ export async function linkProjectToGithubAction(
12201189
repo: createdRepo.data.name,
12211190
path: 'README.md',
12221191
});
1192+
// @ts-ignore
12231193
if ('sha' in readmeData && readmeData.type === 'file') {
1194+
// @ts-ignore
12241195
existingReadmeSha = readmeData.sha;
12251196
}
12261197
} catch (getContentError: any) {
@@ -1244,7 +1215,7 @@ export async function linkProjectToGithubAction(
12441215
}
12451216

12461217
const repoUrl = createdRepo.data.html_url;
1247-
const actualRepoName = createdRepo.data.full_name;
1218+
const actualRepoName = createdRepo.data.full_name; // e.g., "username/repo-name"
12481219

12491220
const updatedProject = await dbUpdateProjectGithubRepo(projectUuid, repoUrl, actualRepoName);
12501221

@@ -1260,22 +1231,79 @@ export async function linkProjectToGithubAction(
12601231
}
12611232
}
12621233

1263-
async function fetchUserGithubAccessDetailsAction(userUuid: string): Promise<{
1264-
oauthToken: UserGithubOAuthToken | null;
1265-
installation: UserGithubInstallation | null;
1266-
}> {
1234+
// GitHub File Management Actions
1235+
export async function getRepoContentsAction(projectUuid: string, path: string = ''): Promise<GithubRepoContentItem[] | { error: string }> {
1236+
const session = await auth();
1237+
if (!session?.user?.uuid) return { error: "Authentication required." };
1238+
1239+
const project = await dbGetProjectByUuid(projectUuid);
1240+
if (!project || !project.githubRepoName) {
1241+
return { error: "Project not found or not linked to GitHub." };
1242+
}
1243+
1244+
const oauthToken = await dbGetUserGithubOAuthToken(session.user.uuid);
1245+
if (!oauthToken || !oauthToken.accessToken) {
1246+
return { error: "GitHub account not linked or token missing." };
1247+
}
1248+
1249+
const [owner, repo] = project.githubRepoName.split('/');
1250+
if (!owner || !repo) return { error: "Invalid GitHub repository name format." };
1251+
1252+
const octokit = new Octokit({ auth: oauthToken.accessToken });
1253+
1254+
try {
1255+
const { data } = await octokit.rest.repos.getContent({
1256+
owner,
1257+
repo,
1258+
path,
1259+
});
1260+
if (Array.isArray(data)) {
1261+
return data as GithubRepoContentItem[];
1262+
}
1263+
// If data is a single file object (when path points to a file)
1264+
return [data as GithubRepoContentItem];
1265+
} catch (error: any) {
1266+
console.error(`[getRepoContentsAction] Error fetching content for ${owner}/${repo}/${path}:`, error);
1267+
return { error: error.message || "Failed to fetch repository contents." };
1268+
}
1269+
}
1270+
1271+
export async function getFileContentAction(projectUuid: string, filePath: string): Promise<{ content: string; sha: string } | { error: string }> {
12671272
const session = await auth();
1268-
if (!session?.user?.uuid || session.user.uuid !== userUuid) {
1269-
console.error("[fetchUserGithubAccessDetailsAction] Authentication mismatch or missing session.");
1270-
return { oauthToken: null, installation: null };
1273+
if (!session?.user?.uuid) return { error: "Authentication required." };
1274+
1275+
const project = await dbGetProjectByUuid(projectUuid);
1276+
if (!project || !project.githubRepoName) {
1277+
return { error: "Project not found or not linked to GitHub." };
1278+
}
1279+
1280+
const oauthToken = await dbGetUserGithubOAuthToken(session.user.uuid);
1281+
if (!oauthToken || !oauthToken.accessToken) {
1282+
return { error: "GitHub account not linked or token missing." };
12711283
}
1284+
1285+
const [owner, repo] = project.githubRepoName.split('/');
1286+
if (!owner || !repo) return { error: "Invalid GitHub repository name format." };
1287+
1288+
const octokit = new Octokit({ auth: oauthToken.accessToken });
1289+
12721290
try {
1273-
const oauthToken = await dbGetUserGithubOAuthToken(userUuid);
1274-
return { oauthToken, installation: null };
1275-
} catch (error) {
1276-
console.error("[fetchUserGithubAccessDetailsAction] Error fetching GitHub access details:", error);
1277-
return { oauthToken: null, installation: null };
1291+
const { data } = await octokit.rest.repos.getContent({
1292+
owner,
1293+
repo,
1294+
path: filePath,
1295+
});
1296+
1297+
// @ts-ignore - Octokit's type for data can be an array or object
1298+
if (data.type !== 'file' || typeof data.content !== 'string' || typeof data.sha !== 'string') {
1299+
return { error: "Path does not point to a valid file or content is missing." };
1300+
}
1301+
// @ts-ignore
1302+
const content = Buffer.from(data.content, data.encoding as BufferEncoding || 'base64').toString('utf8');
1303+
// @ts-ignore
1304+
return { content, sha: data.sha };
1305+
} catch (error: any) {
1306+
console.error(`[getFileContentAction] Error fetching file content for ${owner}/${repo}/${filePath}:`, error);
1307+
return { error: error.message || "Failed to fetch file content." };
12781308
}
12791309
}
1280-
1281-

0 commit comments

Comments
 (0)