TaskDetailPage.tsx 4 KB
// Workflow task detail page.
//
// Fetches a single user task by ID. If the task carries a formKey
// starting with "vibe:", strips the prefix and renders the matching
// MetadataFormRenderer so the user sees a rich, metadata-driven
// form. Otherwise falls back to a read-only JSON dump of the task
// variables with a simple "Complete" button.

import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { workflow } from '@/api/client'
import type { UserTaskDetail } from '@/types/api'
import { PageHeader } from '@/components/PageHeader'
import { Loading } from '@/components/Loading'
import { ErrorBox } from '@/components/ErrorBox'
import { MetadataFormRenderer } from '@/components/MetadataFormRenderer'
import { useT } from '@/i18n/LocaleContext'

export function TaskDetailPage() {
  const t = useT()
  const navigate = useNavigate()
  const { taskId = '' } = useParams<{ taskId: string }>()
  const [task, setTask] = useState<UserTaskDetail | null>(null)
  const [loading, setLoading] = useState(true)
  const [acting, setActing] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    setLoading(true)
    setError(null)
    workflow
      .getTask(taskId)
      .then(setTask)
      .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
      .finally(() => setLoading(false))
  }, [taskId])

  const handleComplete = async (formData?: Record<string, unknown>) => {
    setActing(true)
    setError(null)
    try {
      await workflow.completeTask(taskId, formData ?? {})
      navigate('/workflow/tasks')
    } catch (e: unknown) {
      setError(e instanceof Error ? e : new Error(String(e)))
    } finally {
      setActing(false)
    }
  }

  if (loading) return <Loading />
  if (error && !task) return <ErrorBox error={error} />
  if (!task) return <ErrorBox error={new Error('Task not found')} />

  // Determine whether we have a vibe: form key
  const vibeFormSlug =
    task.formKey && task.formKey.startsWith('vibe:')
      ? task.formKey.slice('vibe:'.length)
      : null

  return (
    <div>
      <PageHeader
        title={t('page.taskDetail.title')}
        subtitle={task.taskName}
        actions={
          <button
            className="btn-secondary"
            onClick={() => navigate('/workflow/tasks')}
          >
            {t('action.back')}
          </button>
        }
      />

      {error && <ErrorBox error={error} />}

      <div className="card p-4 space-y-4">
        <dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm max-w-lg">
          <dt className="font-medium text-slate-500">Task ID</dt>
          <dd className="font-mono">{task.taskId}</dd>
          <dt className="font-medium text-slate-500">Process</dt>
          <dd>{task.processDefinitionKey}</dd>
          <dt className="font-medium text-slate-500">Created</dt>
          <dd>{task.createTime}</dd>
          <dt className="font-medium text-slate-500">Assignee</dt>
          <dd>{task.assignee ?? '\u2014'}</dd>
          <dt className="font-medium text-slate-500">Form Key</dt>
          <dd className="font-mono">{task.formKey ?? '\u2014'}</dd>
        </dl>

        <hr className="border-slate-200" />

        {vibeFormSlug ? (
          <MetadataFormRenderer
            slug={vibeFormSlug}
            initialValues={task.variables}
            onSubmit={handleComplete}
          />
        ) : (
          <div className="space-y-4">
            <div>
              <h3 className="text-sm font-medium text-slate-700 mb-2">Variables</h3>
              <pre className="rounded-md bg-slate-50 border border-slate-200 p-3 text-xs overflow-x-auto max-h-64">
                {JSON.stringify(task.variables, null, 2)}
              </pre>
            </div>
            <button
              className="btn-primary"
              disabled={acting}
              onClick={() => handleComplete()}
            >
              {acting ? '...' : t('action.complete')}
            </button>
          </div>
        )}
      </div>
    </div>
  )
}