feat(projects): enable editing project info from project cards
This commit is contained in:
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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user