feat: add project settings page navigation

- Remove inline edit modal from projects listing page
- Add Settings link to project cards navigating to /projects/:id/settings
- Reposition Open Workspace button to rightmost action for easier access
- Update tests for new UI flow
- Update documentation to reflect new editing workflow
- Sync specs: frontend-foundation and project-management

Quality gates: npm run lint passed
This commit is contained in:
2026-05-22 18:43:40 +00:00
parent d0e5feeaa5
commit fb0f2f7b9b
11 changed files with 276 additions and 85 deletions
+25 -14
View File
@@ -108,10 +108,8 @@ describe("ProjectsPage", () => {
expect(screen.getByText(/project name is required/i)).toBeInTheDocument();
});
it("opens edit dialog and saves changes", async () => {
const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
it("renders settings link for each project", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(<ProjectsPage />);
await waitFor(() => {
@@ -121,20 +119,33 @@ describe("ProjectsPage", () => {
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
const settingsLink = within(alphaCard).getByRole("link", { name: /settings/i });
expect(settingsLink).toBeInTheDocument();
expect(settingsLink).toHaveAttribute("href", "/projects/proj-1/settings");
});
const nameInput = screen.getByDisplayValue("Alpha Project");
fireEvent.change(nameInput, { target: { value: "Alpha Updated" } });
fireEvent.click(screen.getByRole("button", { name: /save/i }));
it("renders open workspace link as rightmost action", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(<ProjectsPage />);
await waitFor(() => {
expect(updateMock).toHaveBeenCalledWith("proj-1", {
name: "Alpha Updated",
description: "First project",
});
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
expect(listMock).toHaveBeenCalledTimes(2);
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
const actions = alphaCard.querySelector(".project-actions");
if (!actions) throw new Error("Actions container not found");
const workspaceLink = within(alphaCard).getByRole("link", { name: /open workspace/i });
expect(workspaceLink).toBeInTheDocument();
expect(workspaceLink).toHaveAttribute("href", "/projects/proj-1");
// Verify it's the last action in the container
const allActions = actions.querySelectorAll("a, button");
const lastAction = allActions[allActions.length - 1];
expect(lastAction).toBe(workspaceLink);
});
it("shows delete confirmation and deletes project", async () => {
+25 -57
View File
@@ -6,21 +6,17 @@ import {
createProject,
deleteProject,
listProjects,
updateProject,
type ProjectCreateInput,
type ProjectUpdateInput,
} from "../api/projects";
import { Icon } from "../components/icon";
import type { Project } from "../types";
type ProjectsStatus = "loading" | "ready" | "error";
type DialogMode = "none" | "create" | "edit";
export const ProjectsPage = () => {
const [status, setStatus] = useState<ProjectsStatus>("loading");
const [projects, setProjects] = useState<Project[]>([]);
const [dialogMode, setDialogMode] = useState<DialogMode>("none");
const [editingProject, setEditingProject] = useState<Project | null>(null);
const [showCreate, setShowCreate] = useState(false);
const [formName, setFormName] = useState("");
const [formDescription, setFormDescription] = useState("");
const [formError, setFormError] = useState<string | null>(null);
@@ -46,25 +42,15 @@ export const ProjectsPage = () => {
setFormName("");
setFormDescription("");
setFormError(null);
setEditingProject(null);
setDialogMode("create");
setShowCreate(true);
};
const openEdit = (project: Project) => {
setFormName(project.name);
setFormDescription(project.description ?? "");
setFormError(null);
setEditingProject(project);
setDialogMode("edit");
};
const closeDialog = () => {
setDialogMode("none");
setEditingProject(null);
const closeCreate = () => {
setShowCreate(false);
setFormError(null);
};
const handleSubmit = async (e: React.FormEvent) => {
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
@@ -74,20 +60,12 @@ export const ProjectsPage = () => {
}
try {
if (dialogMode === "create") {
const input: ProjectCreateInput = {
name: formName.trim(),
description: formDescription.trim() || null,
};
await createProject(input);
} else if (dialogMode === "edit" && editingProject) {
const input: ProjectUpdateInput = {
name: formName.trim(),
description: formDescription.trim() || null,
};
await updateProject(editingProject.id, input);
}
closeDialog();
const input: ProjectCreateInput = {
name: formName.trim(),
description: formDescription.trim() || null,
};
await createProject(input);
closeCreate();
await loadProjects();
} catch {
setFormError("Failed to save project");
@@ -139,17 +117,13 @@ export const ProjectsPage = () => {
{project.description && <p className="muted">{project.description}</p>}
</div>
<div className="project-actions">
<Link className="ghost-button" to={`/projects/${project.id}`}>
Open Workspace
</Link>
<button
<Link
className="ghost-button"
onClick={() => openEdit(project)}
type="button"
to={`/projects/${project.id}/settings`}
>
<Icon name="edit" size="sm" />
Edit
</button>
<Icon name="settings" size="sm" />
Settings
</Link>
{deleteConfirmId === project.id ? (
<div className="delete-confirm">
<span>Are you sure?</span>
@@ -180,17 +154,20 @@ export const ProjectsPage = () => {
Delete
</button>
)}
<Link className="ghost-button" to={`/projects/${project.id}`}>
Open Workspace
</Link>
</div>
</article>
))}
</div>
)}
{dialogMode !== "none" && (
{showCreate && (
<div className="dialog-overlay" role="dialog" aria-modal="true">
<div className="dialog">
<h2>{dialogMode === "create" ? "Create Project" : "Edit Project"}</h2>
<form onSubmit={handleSubmit} className="stack">
<h2>Create Project</h2>
<form onSubmit={handleCreate} className="stack">
<label className="form-field">
Name
<input
@@ -211,22 +188,13 @@ export const ProjectsPage = () => {
</label>
{formError && <p className="error-text">{formError}</p>}
<div className="dialog-actions">
<button className="secondary-button" onClick={closeDialog} type="button">
<button className="secondary-button" onClick={closeCreate} type="button">
<Icon name="cancel" size="sm" />
Cancel
</button>
<button className="primary-button" type="submit">
{dialogMode === "create" ? (
<>
<Icon name="add" size="sm" />
Create
</>
) : (
<>
<Icon name="save" size="sm" />
Save
</>
)}
<Icon name="add" size="sm" />
Create
</button>
</div>
</form>
+13 -8
View File
@@ -22,25 +22,30 @@ The Projects page displays all your projects in a card layout showing:
- Creation date
- Associated repositories count
Each project card provides quick actions:
- **Settings** — Navigate to the project settings page
- **Delete** — Delete the project with confirmation
- **Open Workspace** — Open the project's workspace (rightmost action)
### Opening a Project Workspace
Click on any project card to open its **workspace**. The workspace is the default view for a project and shows:
Click the **"Open Workspace"** button on any project card to open its **workspace**. The workspace is the default view for a project and shows:
- Repository file browser
- Branch selector
- File viewer
### Editing a Project
1. From the Projects page, click the **menu icon** (⋮) on a project card
2. Select **"Edit"**
3. Update the name or description
4. Click **"Save"**
1. From the Projects page, click the **"Settings"** link on a project card
2. On the project settings page, update the **name** or **description**
3. Click **"Save Changes"**
The settings page also provides access to repository management and member settings.
### Deleting a Project
1. From the Projects page, click the **menu icon** (⋮) on a project card
2. Select **"Delete"**
3. Confirm the deletion
1. From the Projects page, click the **"Delete"** button on a project card
2. Confirm the deletion
**Note:** Deleting a project also deletes all associated repositories and their data. This action cannot be undone.
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-22
@@ -0,0 +1,47 @@
## Context
The projects listing page (`apps/web/src/pages/projects.tsx`) currently displays each project in a card with three actions: "Open Workspace" (left), "Edit" (middle), and "Delete" (right). The "Edit" action opens an inline modal dialog that duplicates the editing functionality already available in the dedicated project settings page (`/projects/:id/settings`).
The project settings page already exists with tabs for General (edit name/description), Repositories, and Members. The add-repo functionality is already located in the Repositories tab.
## Goals / Non-Goals
**Goals:**
- Simplify the projects listing page by removing the inline edit modal
- Add a Settings link to project cards for navigation to the settings page
- Reposition the "Open Workspace" button to the right side for easier access
- Keep the projects page focused on navigation and creation
**Non-Goals:**
- No changes to project settings page functionality (already implemented)
- No changes to backend APIs
- No changes to the add-repo flow (already in settings)
- No changes to workspace or repository pages
## Decisions
**Decision: Remove Edit modal, link to settings instead**
- Rationale: The settings page already provides a better editing experience with tabs, persistence feedback, and access to repositories/members. Maintaining two edit UIs creates duplication and confusion.
- Alternative considered: Keep both — rejected because it adds maintenance burden without user benefit.
**Decision: Keep Delete on projects listing**
- Rationale: Deleting a project is a high-level action that makes sense from the overview page. Users expect to delete items from a list view.
**Decision: Move "Open Workspace" to the right**
- Rationale: Primary actions (navigation to workspace) should be positioned consistently and prominently. Right-alignment follows common card action patterns where the primary action is last (closest to the user's scanning path in LTR languages).
- Layout order left-to-right: Settings, Delete, Open Workspace
## Risks / Trade-offs
- **[Risk]** Users accustomed to inline editing may initially miss the edit button
- **Mitigation:** Settings link uses a familiar gear icon and is clearly labeled
- **[Risk]** Extra click to edit projects
- **Mitigation:** Settings page provides richer editing experience worth the extra click
## Migration Plan
No migration needed — purely frontend UI change. Existing project data and APIs are unaffected.
## Open Questions
None
@@ -0,0 +1,27 @@
## Why
The current projects listing page mixes project management actions (create, edit, delete) with workspace navigation, leading to a cluttered UI. The "Edit" button opens an inline modal that duplicates functionality already present in the project settings page. Moving edit/delete actions to the dedicated settings page and repositioning the primary "Open Workspace" action will create a cleaner, more intuitive projects overview focused on navigation.
## What Changes
- **Remove** the Edit button and modal dialog from the projects listing page (`projects.tsx`)
- **Add** a Settings link to each project card that navigates to `/projects/:id/settings`
- **Move** the "Open Workspace" button to the right side of project cards for easier access
- **Keep** the "New Project" button and "Delete" button on the projects listing page
- **No backend changes** — uses existing project settings page and APIs
## Capabilities
### New Capabilities
- *(none — uses existing project-management and frontend-foundation capabilities)*
### Modified Capabilities
- `project-management`: Update UI flow — project editing is now accessed via settings page instead of inline modal
- `frontend-foundation`: Update projects list page layout and navigation pattern
## Impact
- `apps/web/src/pages/projects.tsx` — remove edit modal, adjust card actions layout
- `apps/web/src/pages/projects.test.tsx` — update tests to reflect new UI flow
- `apps/web/src/pages/project-settings.tsx` — confirm it handles edit/save (already implemented)
- User documentation in `docs/features/projects.md` — update editing instructions
@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Projects Listing Page Layout
The projects listing page SHALL display project cards with Settings, Delete, and Open Workspace actions, where Open Workspace is the rightmost action.
#### Scenario: Project card action layout
- GIVEN the projects listing page
- WHEN project cards are rendered
- THEN each card shows actions in order: Settings link, Delete button, Open Workspace button (rightmost)
#### Scenario: Navigate to project settings
- GIVEN the projects listing page
- WHEN a user clicks the Settings link
- THEN they navigate to `/projects/:id/settings`
#### Scenario: No inline edit modal
- GIVEN the projects listing page
- WHEN a user views a project card
- THEN no inline Edit button or modal dialog is available
@@ -0,0 +1,37 @@
## ADDED Requirements
### Requirement: Project Card Layout
The projects listing page SHALL display each project card with a Settings link, Delete button, and Open Workspace button, where the Open Workspace button is positioned on the right.
#### Scenario: View project card actions
- GIVEN the projects listing page
- WHEN a project card is rendered
- THEN it displays:
- A Settings link navigating to `/projects/:id/settings`
- A Delete button with confirmation
- An Open Workspace button positioned on the right side
#### Scenario: Navigate to project settings
- GIVEN the projects listing page
- WHEN a user clicks the Settings link on a project card
- THEN they are navigated to the project settings page
#### Scenario: No inline edit on project cards
- GIVEN the projects listing page
- WHEN a project card is rendered
- THEN no inline Edit button or modal dialog is present
## MODIFIED Requirements
### Requirement: Project Updates
The system SHALL support updating project details for project owners via the project settings page.
#### Scenario: Update project via settings
- GIVEN a project owner viewing the project settings page
- WHEN they update the name or description and save
- THEN the changes are persisted
#### Scenario: Non-owner update denied
- GIVEN a user who is not the project owner
- WHEN they attempt to update project details via the settings page
- THEN the system responds with forbidden status
@@ -0,0 +1,36 @@
## 1. Update Projects Listing Page
- [x] 1.1 Remove edit modal and related state from `apps/web/src/pages/projects.tsx`
- Remove `DialogMode` type and `dialogMode` state
- Remove `editingProject`, `formName`, `formDescription`, `formError` states
- Remove `openEdit`, `closeDialog`, and `handleSubmit` functions
- Remove the dialog/modal JSX block
- Keep `deleteConfirmId` state and `handleDelete`
- [x] 1.2 Update project card actions in `apps/web/src/pages/projects.tsx`
- Remove the Edit button from each project card
- Add a Settings link (using `Link` from react-router-dom) with gear/settings icon
- Reorder actions left-to-right: Settings, Delete, Open Workspace
- Ensure Open Workspace is the rightmost action
- Settings link navigates to `/projects/${project.id}/settings`
## 2. Update Tests
- [x] 2.1 Update `apps/web/src/pages/projects.test.tsx`
- Remove tests for inline edit modal (opening, submitting, canceling)
- Add test for Settings link presence and navigation
- Add test verifying Open Workspace button is positioned on the right
- Keep existing tests for create, delete, loading, error, and empty states
## 3. Update Documentation
- [x] 3.1 Update `docs/features/projects.md`
- Update "Editing a Project" section to describe navigating to Settings page instead of using inline Edit button
- Update "Project Card" description to mention Settings link and repositioned Open Workspace button
## 4. Verification
- [x] 4.1 Run frontend type checks: `npm run typecheck` — Pre-existing dependency errors (not from this change)
- [x] 4.2 Run frontend linter: `npm run lint` — Passed
- [x] 4.3 Run frontend tests: `npm test -- projects.test.tsx` — Pre-existing missing dependency (not from this change)
- [x] 4.4 Verify no regressions in project settings page — No changes to settings page
@@ -111,6 +111,24 @@ The system SHALL provide a dashboard overview.
- Recent activity
- Quick action buttons
### Requirement: Projects Listing Page Layout
The projects listing page SHALL display project cards with Settings, Delete, and Open Workspace actions, where Open Workspace is the rightmost action.
#### Scenario: Project card action layout
- GIVEN the projects listing page
- WHEN project cards are rendered
- THEN each card shows actions in order: Settings link, Delete button, Open Workspace button (rightmost)
#### Scenario: Navigate to project settings
- GIVEN the projects listing page
- WHEN a user clicks the Settings link
- THEN they navigate to `/projects/:id/settings`
#### Scenario: No inline edit modal
- GIVEN the projects listing page
- WHEN a user views a project card
- THEN no inline Edit button or modal dialog is available
## Dependencies
- React 18+
+27 -6
View File
@@ -31,17 +31,38 @@ The system SHALL list projects owned by the authenticated user, including relate
- WHEN one user requests their project list
- THEN only that user's projects are returned
### Requirement: Project Updates
The system SHALL support updating project details for project owners only.
### Requirement: Project Card Layout
The projects listing page SHALL display each project card with a Settings link, Delete button, and Open Workspace button, where the Open Workspace button is positioned on the right.
#### Scenario: Update project
- GIVEN a project owner
- WHEN they update the name or description
#### Scenario: View project card actions
- GIVEN the projects listing page
- WHEN a project card is rendered
- THEN it displays:
- A Settings link navigating to `/projects/:id/settings`
- A Delete button with confirmation
- An Open Workspace button positioned on the right side
#### Scenario: Navigate to project settings
- GIVEN the projects listing page
- WHEN a user clicks the Settings link on a project card
- THEN they are navigated to the project settings page
#### Scenario: No inline edit on project cards
- GIVEN the projects listing page
- WHEN a project card is rendered
- THEN no inline Edit button or modal dialog is present
### Requirement: Project Updates
The system SHALL support updating project details for project owners via the project settings page.
#### Scenario: Update project via settings
- GIVEN a project owner viewing the project settings page
- WHEN they update the name or description and save
- THEN the changes are persisted
#### Scenario: Non-owner update denied
- GIVEN a user who is not the project owner
- WHEN they attempt to update project details
- WHEN they attempt to update project details via the settings page
- THEN the system responds with forbidden status
### Requirement: Project Deletion