feat(projects): enable editing project info from project cards

This commit is contained in:
KodekaTeam committed 2026-06-10 01:30:12 +07:00
1 parent 0f570955ef
commit 70fd3db19f
5 files changed
+232 -21

No files matched your search

@@ -2,21 +2,24 @@
import { useMemo, useState } from "react";
import { FolderArchive, Loader2, LucideRocket, X } from "lucide-react";
import type { ProjectCreateBody } from "@/lib/api";
import type { ProjectCreateBody, ProjectRecord } from "@/lib/api";
interface ProjectFormModalProps {
project?: ProjectRecord;
submitting: boolean;
onClose: () => void;
onSubmit: (payload: ProjectCreateBody) => Promise<void>;
}
export default function ProjectFormModal({
project,
submitting,
onClose,
onSubmit,
}: ProjectFormModalProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const isEditing = Boolean(project);
const [name, setName] = useState(project?.name ?? "");
const [description, setDescription] = useState(project?.description ?? "");
const [error, setError] = useState("");
const canSubmit = useMemo(() => name.trim().length >= 2, [name]);
@@ -30,11 +33,13 @@ export default function ProjectFormModal({
}
setError("");
await onSubmit({
const payload = {
name: name.trim(),
description: description.trim(),
environments: [],
});
...(isEditing ? {} : { environments: [] }),
};
await onSubmit(payload);
};
return (
@@ -69,12 +74,14 @@ export default function ProjectFormModal({
}}
>
<FolderArchive size={18} style={{ marginRight: 8 }} />
Create New Project
{isEditing ? "Project Detail/Ops" : "Create New Project"}
</h3>
<p
style={{ marginTop: 2, color: "var(--text-muted)", fontSize: 12 }}
>
Create a new project to manage your environments and deployments.
{isEditing
? "Update project information used across environments and deployments."
: "Create a new project to manage your environments and deployments."}
</p>
</div>
<button
@@ -178,7 +185,13 @@ export default function ProjectFormModal({
) : (
<LucideRocket size={18} style={{ marginRight: 0 }} />
)}
{submitting ? "Creating..." : "Create Project"}
{submitting
? isEditing
? "Saving..."
: "Creating..."
: isEditing
? "Save Changes"
: "Create Project"}
</button>
</div>
</form>
+20 -5
View File
@@ -1,16 +1,18 @@
import Link from "next/link";
import { FolderTree, Layers3, Trash2 } from "lucide-react";
import { FolderTree, Layers3, Trash2, Wrench } from "lucide-react";
import type { ProjectRecord } from "@/lib/api";
interface ProjectsGridProps {
projects: ProjectRecord[];
deletingProjectId: string | null;
onEditProject: (project: ProjectRecord) => void;
onDeleteProject: (project: ProjectRecord) => void;
}
export default function ProjectsGrid({
projects,
deletingProjectId,
onEditProject,
onDeleteProject,
}: ProjectsGridProps) {
if (projects.length === 0) {
@@ -202,12 +204,25 @@ export default function ProjectsGrid({
))}
</div>
<div style={{ display: "flex", gap: 6 }}>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
<button
type="button"
className="btn btn-success"
onClick={() => onEditProject(project)}
style={{
flex: "1 1 120px",
fontSize: 11,
padding: "5px",
}}
>
<Wrench size={11} />
Detail/Ops
</button>
<Link
href={`/projects/${project.id}`}
className="btn btn-success"
className="btn btn-primary"
style={{
flex: 1,
flex: "1 1 120px",
fontSize: 11,
padding: "5px",
textDecoration: "none",
@@ -221,7 +236,7 @@ export default function ProjectsGrid({
className="btn btn-ghost"
onClick={() => onDeleteProject(project)}
disabled={deletingProjectId === project.id}
style={{ flex: 1, fontSize: 11, padding: "5px" }}
style={{ flex: "1 1 120px", fontSize: 11, padding: "5px" }}
>
<Trash2 size={11} />
{deletingProjectId === project.id ? "Deleting..." : "Remove"}
+61 -6
View File
@@ -31,7 +31,11 @@ export default function ProjectsPage() {
const [refreshing, setRefreshing] = useState(false);
const [search, setSearch] = useState("");
const [showProjectModal, setShowProjectModal] = useState(false);
const [editingProject, setEditingProject] = useState<ProjectRecord | null>(
null,
);
const [creatingProject, setCreatingProject] = useState(false);
const [updatingProject, setUpdatingProject] = useState(false);
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(
null,
);
@@ -154,6 +158,46 @@ export default function ProjectsPage() {
[pushToast],
);
const handleUpdateProject = useCallback(
async (payload: ProjectCreateBody) => {
if (!editingProject) {
return;
}
setUpdatingProject(true);
try {
const response = await projectsApi.update(editingProject.id, {
name: payload.name,
description: payload.description,
});
const updatedProject = response.data;
if (updatedProject) {
setProjects((current) =>
current.map((project) =>
project.id === updatedProject.id ? updatedProject : project,
),
);
}
setEditingProject(null);
pushToast({
tone: "success",
title: "Project updated",
message: `Project ${payload.name} has been successfully updated.`,
});
} catch (error) {
pushToast({
tone: "error",
title: "Update failed",
message:
error instanceof Error ? error.message : "Project update failed.",
});
} finally {
setUpdatingProject(false);
}
},
[editingProject, pushToast],
);
const performDeleteProject = useCallback(
async (project: ProjectRecord) => {
setDeletingProjectId(project.id);
@@ -236,7 +280,10 @@ export default function ProjectsPage() {
addLabel="Create Project"
onSearchChange={setSearch}
onRefresh={() => load(true)}
onAdd={() => setShowProjectModal(true)}
onAdd={() => {
setEditingProject(null);
setShowProjectModal(true);
}}
/>
<ProjectsStatePanel
@@ -244,23 +291,31 @@ export default function ProjectsPage() {
isEmpty={filteredProjects.length === 0}
searchActive={Boolean(search.trim())}
canCreate={true}
onAdd={() => setShowProjectModal(true)}
onAdd={() => {
setEditingProject(null);
setShowProjectModal(true);
}}
/>
{!loading && filteredProjects.length > 0 ? (
<ProjectsGrid
projects={filteredProjects}
deletingProjectId={deletingProjectId}
onEditProject={setEditingProject}
onDeleteProject={handleDeleteProject}
/>
) : null}
</div>
{showProjectModal ? (
{showProjectModal || editingProject ? (
<ProjectFormModal
submitting={creatingProject}
onClose={() => setShowProjectModal(false)}
onSubmit={handleCreateProject}
project={editingProject ?? undefined}
submitting={editingProject ? updatingProject : creatingProject}
onClose={() => {
setShowProjectModal(false);
setEditingProject(null);
}}
onSubmit={editingProject ? handleUpdateProject : handleCreateProject}
/>
) : null}
</DashboardLayout>