From a08644dde3117eee0b1931119603eb2b9dfc27ca Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 1 Apr 2026 04:46:46 +0000 Subject: [PATCH 01/13] FE-PR-001: Rename Propose -> Proposal across frontend - Rename ProposesPage -> ProposalsPage, ProposeDetailPage -> ProposalDetailPage - Update Propose type to Proposal (keep Propose as deprecated alias) - Add GeneratedTask type for accept results - Switch API calls from /proposes to /proposals (canonical) - Update sidebar label: Proposes -> Proposals - Update routes: /proposals (+ legacy /proposes compat) - Update all UI text: Propose -> Proposal - Remove feat_task_id display, add generated_tasks section - Clean up propose references in comments --- src/App.tsx | 11 ++- src/components/CreateTaskModal.tsx | 2 +- src/components/Sidebar.tsx | 2 +- src/pages/CreateTaskPage.tsx | 2 +- ...eDetailPage.tsx => ProposalDetailPage.tsx} | 99 +++++++++++-------- .../{ProposesPage.tsx => ProposalsPage.tsx} | 37 ++++--- src/types/index.ts | 16 ++- 7 files changed, 100 insertions(+), 69 deletions(-) rename src/pages/{ProposeDetailPage.tsx => ProposalDetailPage.tsx} (68%) rename src/pages/{ProposesPage.tsx => ProposalsPage.tsx} (76%) diff --git a/src/App.tsx b/src/App.tsx index 39e8886..0e15d9b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,8 +14,8 @@ import MilestoneDetailPage from '@/pages/MilestoneDetailPage' import NotificationsPage from '@/pages/NotificationsPage' import RoleEditorPage from '@/pages/RoleEditorPage' import MonitorPage from '@/pages/MonitorPage' -import ProposesPage from '@/pages/ProposesPage' -import ProposeDetailPage from '@/pages/ProposeDetailPage' +import ProposalsPage from '@/pages/ProposalsPage' +import ProposalDetailPage from '@/pages/ProposalDetailPage' import UsersPage from '@/pages/UsersPage' import SupportDetailPage from '@/pages/SupportDetailPage' import MeetingDetailPage from '@/pages/MeetingDetailPage' @@ -116,8 +116,11 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> + } /> + {/* Legacy routes for backward compatibility */} + } /> + } /> } /> } /> } /> diff --git a/src/components/CreateTaskModal.tsx b/src/components/CreateTaskModal.tsx index 61ed735..35bfd14 100644 --- a/src/components/CreateTaskModal.tsx +++ b/src/components/CreateTaskModal.tsx @@ -3,7 +3,7 @@ import api from '@/services/api' import type { Milestone, Project, Task } from '@/types' const TASK_TYPES = [ - { value: 'story', label: 'Story', subtypes: ['improvement', 'refactor'] }, // P9.6: 'feature' removed โ€” must come from propose accept + { value: 'story', label: 'Story', subtypes: ['improvement', 'refactor'] }, // P9.6: 'feature' removed โ€” must come from proposal accept { value: 'issue', label: 'Issue', subtypes: ['infrastructure', 'performance', 'regression', 'security', 'user_experience', 'defect'] }, // P7.1: 'task' type removed โ€” defect subtype migrated to issue/defect { value: 'test', label: 'Test', subtypes: ['regression', 'security', 'smoke', 'stress'] }, diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 2afdbd2..a07e3f7 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -32,7 +32,7 @@ export default function Sidebar({ user, onLogout }: Props) { const links = user ? [ { to: '/', icon: '๐Ÿ“Š', label: 'Dashboard' }, { to: '/projects', icon: '๐Ÿ“', label: 'Projects' }, - { to: '/proposes', icon: '๐Ÿ’ก', label: 'Proposes' }, + { to: '/proposals', icon: '๐Ÿ’ก', label: 'Proposals' }, { to: '/notifications', icon: '๐Ÿ””', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') }, { to: '/monitor', icon: '๐Ÿ“ก', label: 'Monitor' }, ...(user.is_admin ? [ diff --git a/src/pages/CreateTaskPage.tsx b/src/pages/CreateTaskPage.tsx index 8a1c82e..dad58d5 100644 --- a/src/pages/CreateTaskPage.tsx +++ b/src/pages/CreateTaskPage.tsx @@ -4,7 +4,7 @@ import api from '@/services/api' import type { Project, Milestone } from '@/types' const TASK_TYPES = [ - { value: 'story', label: 'Story', subtypes: ['improvement', 'refactor'] }, // P9.6: 'feature' removed โ€” must come from propose accept + { value: 'story', label: 'Story', subtypes: ['improvement', 'refactor'] }, // P9.6: 'feature' removed โ€” must come from proposal accept { value: 'issue', label: 'Issue', subtypes: ['infrastructure', 'performance', 'regression', 'security', 'user_experience', 'defect'] }, // P7.1: 'task' type removed โ€” defect subtype migrated to issue/defect { value: 'test', label: 'Test', subtypes: ['regression', 'security', 'smoke', 'stress'] }, diff --git a/src/pages/ProposeDetailPage.tsx b/src/pages/ProposalDetailPage.tsx similarity index 68% rename from src/pages/ProposeDetailPage.tsx rename to src/pages/ProposalDetailPage.tsx index 045bc56..bc3c3d2 100644 --- a/src/pages/ProposeDetailPage.tsx +++ b/src/pages/ProposalDetailPage.tsx @@ -1,36 +1,36 @@ import { useState, useEffect } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' import api from '@/services/api' -import type { Propose, Milestone } from '@/types' +import type { Proposal, Milestone } from '@/types' import dayjs from 'dayjs' import CopyableCode from '@/components/CopyableCode' -export default function ProposeDetailPage() { +export default function ProposalDetailPage() { const { id } = useParams() const [searchParams] = useSearchParams() const projectId = searchParams.get('project_id') const navigate = useNavigate() - const [propose, setPropose] = useState(null) + const [proposal, setProposal] = useState(null) const [milestones, setMilestones] = useState([]) const [showAccept, setShowAccept] = useState(false) const [selectedMilestone, setSelectedMilestone] = useState('') const [actionLoading, setActionLoading] = useState(false) const [error, setError] = useState('') - // Edit state (P10.7) + // Edit state const [showEdit, setShowEdit] = useState(false) const [editTitle, setEditTitle] = useState('') const [editDescription, setEditDescription] = useState('') const [editLoading, setEditLoading] = useState(false) - const fetchPropose = () => { + const fetchProposal = () => { if (!projectId) return - api.get(`/projects/${projectId}/proposes/${id}`).then(({ data }) => setPropose(data)) + api.get(`/projects/${projectId}/proposals/${id}`).then(({ data }) => setProposal(data)) } useEffect(() => { - fetchPropose() + fetchProposal() }, [id, projectId]) const loadMilestones = () => { @@ -44,9 +44,9 @@ export default function ProposeDetailPage() { setActionLoading(true) setError('') try { - await api.post(`/projects/${projectId}/proposes/${id}/accept`, { milestone_id: selectedMilestone }) + await api.post(`/projects/${projectId}/proposals/${id}/accept`, { milestone_id: selectedMilestone }) setShowAccept(false) - fetchPropose() + fetchProposal() } catch (err: any) { setError(err.response?.data?.detail || 'Accept failed') } finally { @@ -60,8 +60,8 @@ export default function ProposeDetailPage() { setActionLoading(true) setError('') try { - await api.post(`/projects/${projectId}/proposes/${id}/reject`, { reason: reason || undefined }) - fetchPropose() + await api.post(`/projects/${projectId}/proposals/${id}/reject`, { reason: reason || undefined }) + fetchProposal() } catch (err: any) { setError(err.response?.data?.detail || 'Reject failed') } finally { @@ -70,9 +70,9 @@ export default function ProposeDetailPage() { } const openEditModal = () => { - if (!propose) return - setEditTitle(propose.title) - setEditDescription(propose.description || '') + if (!proposal) return + setEditTitle(proposal.title) + setEditDescription(proposal.description || '') setError('') setShowEdit(true) } @@ -82,12 +82,12 @@ export default function ProposeDetailPage() { setEditLoading(true) setError('') try { - await api.patch(`/projects/${projectId}/proposes/${id}`, { + await api.patch(`/projects/${projectId}/proposals/${id}`, { title: editTitle, description: editDescription, }) setShowEdit(false) - fetchPropose() + fetchProposal() } catch (err: any) { setError(err.response?.data?.detail || 'Update failed') } finally { @@ -100,8 +100,8 @@ export default function ProposeDetailPage() { setActionLoading(true) setError('') try { - await api.post(`/projects/${projectId}/proposes/${id}/reopen`) - fetchPropose() + await api.post(`/projects/${projectId}/proposals/${id}/reopen`) + fetchProposal() } catch (err: any) { setError(err.response?.data?.detail || 'Reopen failed') } finally { @@ -109,7 +109,7 @@ export default function ProposeDetailPage() { } } - if (!propose) return
Loading...
+ if (!proposal) return
Loading...
const statusBadgeClass = (s: string) => { if (s === 'open') return 'status-open' @@ -124,11 +124,11 @@ export default function ProposeDetailPage() {

- ๐Ÿ’ก {propose.title} - {propose.propose_code && } + ๐Ÿ’ก {proposal.title} + {proposal.proposal_code && }

- - {propose.status} + + {proposal.status}
@@ -137,23 +137,45 @@ export default function ProposeDetailPage() {

Details

-
Propose Code: {propose.propose_code ? : 'โ€”'}
-
Status: {propose.status}
-
Created By: {propose.created_by_username || (propose.created_by_id ? `User #${propose.created_by_id}` : 'โ€”')}
-
Created: {dayjs(propose.created_at).format('YYYY-MM-DD HH:mm')}
-
Updated: {propose.updated_at ? dayjs(propose.updated_at).format('YYYY-MM-DD HH:mm') : 'โ€”'}
-
Feature Task: {propose.feat_task_id || 'โ€”'}
+
Proposal Code: {proposal.proposal_code ? : 'โ€”'}
+
Status: {proposal.status}
+
Created By: {proposal.created_by_username || (proposal.created_by_id ? `User #${proposal.created_by_id}` : 'โ€”')}
+
Created: {dayjs(proposal.created_at).format('YYYY-MM-DD HH:mm')}
+
Updated: {proposal.updated_at ? dayjs(proposal.updated_at).format('YYYY-MM-DD HH:mm') : 'โ€”'}

Description

-

{propose.description || 'No description'}

+

{proposal.description || 'No description'}

+ {/* Generated tasks (shown when accepted) */} + {proposal.status === 'accepted' && proposal.generated_tasks && proposal.generated_tasks.length > 0 && ( +
+

Generated Tasks

+
+ {proposal.generated_tasks.map((gt) => ( +
navigate(`/tasks/${gt.task_code || gt.task_id}`)} + style={{ cursor: 'pointer' }} + > +
+ {gt.task_type}/{gt.task_subtype} + {gt.task_code && {gt.task_code}} +

{gt.title}

+
+
+ ))} +
+
+ )} + {/* Action buttons */}
- {propose.status === 'open' && ( + {proposal.status === 'open' && ( <> - )} - {propose.status === 'rejected' && ( + {proposal.status === 'rejected' && (