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:
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
+19
@@ -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
|
||||
+37
@@ -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+
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user