Merge dev-2026-03-22 into main #10

Merged
hzhang merged 7 commits from dev-2026-03-22 into main 2026-03-22 14:06:54 +00:00
7 changed files with 58 additions and 7 deletions
Showing only changes of commit 4fc120f595 - Show all commits

View File

@@ -0,0 +1,45 @@
import { useState } from 'react'
interface Props {
code: string
prefix?: string
}
export default function CopyableCode({ code, prefix }: Props) {
const [copied, setCopied] = useState(false)
const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation()
try {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
// fallback: select text
}
}
return (
<span
className="copyable-code"
title="Click to copy code"
onClick={handleCopy}
style={{
cursor: 'pointer',
fontFamily: 'monospace',
padding: '2px 8px',
borderRadius: '4px',
background: 'var(--bg-hover, rgba(255,255,255,.06))',
border: '1px solid var(--border, rgba(255,255,255,.1))',
fontSize: '0.95em',
userSelect: 'all',
transition: 'background .15s',
}}
>
{prefix}{code}
{copied && (
<span style={{ marginLeft: 6, fontSize: '0.8em', color: 'var(--success, #10b981)' }}></span>
)}
</span>
)
}

View File

@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
import api from '@/services/api'
import { useAuth } from '@/hooks/useAuth'
import dayjs from 'dayjs'
import CopyableCode from '@/components/CopyableCode'
interface MeetingItem {
id: number
@@ -153,7 +154,7 @@ export default function MeetingDetailPage() {
<button className="btn-back" onClick={() => navigate(-1)}> Back</button>
<div className="task-header">
<h2>📅 {meeting.meeting_code || `#${meeting.id}`}</h2>
<h2>📅 {meeting.meeting_code ? <CopyableCode code={meeting.meeting_code} /> : `#${meeting.id}`}</h2>
<div className="task-meta">
<span className={`badge status-${meeting.status}`}>{meeting.status}</span>
{meeting.project_code && <span className="text-dim">Project: {meeting.project_code}</span>}

View File

@@ -6,6 +6,7 @@ import dayjs from 'dayjs'
import CreateTaskModal from '@/components/CreateTaskModal'
import MilestoneFormModal from '@/components/MilestoneFormModal'
import { useAuth } from '@/hooks/useAuth'
import CopyableCode from '@/components/CopyableCode'
interface MilestoneTask {
id: number
@@ -155,7 +156,7 @@ export default function MilestoneDetailPage() {
<button className="btn-back" onClick={() => navigate('/milestones')}> Back to Milestones</button>
<div className="task-header">
<h2>🏁 {milestone.title}</h2>
<h2>🏁 {milestone.milestone_code && <><CopyableCode code={milestone.milestone_code} /> </>}{milestone.title}</h2>
<div className="task-meta">
<span className={`badge status-${milestone.status}`}>{milestone.status}</span>
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}

View File

@@ -6,6 +6,7 @@ import dayjs from 'dayjs'
import { useAuth } from '@/hooks/useAuth'
import ProjectFormModal from '@/components/ProjectFormModal'
import MilestoneFormModal from '@/components/MilestoneFormModal'
import CopyableCode from '@/components/CopyableCode'
export default function ProjectDetailPage() {
const { id } = useParams()
@@ -74,7 +75,7 @@ export default function ProjectDetailPage() {
<button className="btn-back" onClick={() => navigate('/projects')}> Back to projects</button>
<div className="task-header">
<h2>📁 {project.name} {project.project_code && <span className="badge">{project.project_code}</span>}</h2>
<h2>📁 {project.name} {project.project_code && <CopyableCode code={project.project_code} />}</h2>
<p style={{ color: 'var(--text-dim)', marginTop: 4 }}>{project.description || 'No description'}</p>
{project.repo && <p style={{ color: 'var(--text-dim)', marginTop: 4 }}>📦 {project.repo}</p>}
<div className="text-dim">Owner: {project.owner_name || 'Unknown'}</div>

View File

@@ -3,6 +3,7 @@ import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
import api from '@/services/api'
import type { Propose, Milestone } from '@/types'
import dayjs from 'dayjs'
import CopyableCode from '@/components/CopyableCode'
export default function ProposeDetailPage() {
const { id } = useParams()
@@ -124,7 +125,7 @@ export default function ProposeDetailPage() {
<div className="task-header">
<h2>
💡 {propose.title}
{propose.propose_code && <span className="badge" style={{ marginLeft: 8 }}>{propose.propose_code}</span>}
{propose.propose_code && <span style={{ marginLeft: 8 }}><CopyableCode code={propose.propose_code} /></span>}
</h2>
<span className={`badge ${statusBadgeClass(propose.status)}`} style={{ fontSize: '1rem' }}>
{propose.status}
@@ -136,7 +137,7 @@ export default function ProposeDetailPage() {
<div className="section">
<h3>Details</h3>
<div className="detail-grid">
<div><strong>Propose Code:</strong> {propose.propose_code || '—'}</div>
<div><strong>Propose Code:</strong> {propose.propose_code ? <CopyableCode code={propose.propose_code} /> : '—'}</div>
<div><strong>Status:</strong> {propose.status}</div>
<div><strong>Created By:</strong> {propose.created_by_username || (propose.created_by_id ? `User #${propose.created_by_id}` : '—')}</div>
<div><strong>Created:</strong> {dayjs(propose.created_at).format('YYYY-MM-DD HH:mm')}</div>

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import api from '@/services/api'
import { useAuth } from '@/hooks/useAuth'
import CopyableCode from '@/components/CopyableCode'
interface SupportItem {
id: number
@@ -141,7 +142,7 @@ export default function SupportDetailPage() {
<button className="btn-back" onClick={() => navigate(-1)}> Back</button>
<div className="task-header">
<h2>🎫 {support.support_code || `#${support.id}`}</h2>
<h2>🎫 {support.support_code ? <CopyableCode code={support.support_code} /> : `#${support.id}`}</h2>
<div className="task-meta">
<span className={`badge status-${support.status}`}>{support.status}</span>
<span className={`badge priority-${support.priority}`}>{support.priority}</span>

View File

@@ -5,6 +5,7 @@ import type { Task, Comment, Project, ProjectMember, Milestone } from '@/types'
import dayjs from 'dayjs'
import { useAuth } from '@/hooks/useAuth'
import CreateTaskModal from '@/components/CreateTaskModal'
import CopyableCode from '@/components/CopyableCode'
export default function TaskDetailPage() {
const { id } = useParams()
@@ -120,7 +121,7 @@ export default function TaskDetailPage() {
<button className="btn-back" onClick={() => navigate('/tasks')}> Back</button>
<div className="task-header">
<h2>{task.task_code ? `[${task.task_code}]` : `#${task.id}`} {task.title}</h2>
<h2>{task.task_code ? <><CopyableCode code={task.task_code} /> </> : `#${task.id} `}{task.title}</h2>
<div className="task-meta">
<span className={`badge status-${task.status}`}>{task.status}</span>
<span className={`badge priority-${task.priority}`}>{task.priority}</span>