Files
template-app/modules/activities/ui/controls.tsx
T
2026-11-14 09:56:45 +00:00

194 lines
5.7 KiB
TypeScript

"use client"
import type { ProjectSlugs } from "@conusai-kit/ui"
import { useDataClient, useWrite } from "@conusai-kit/ui/data"
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import {
FormLayout,
type FormValues,
PageSection,
ResourceList,
TextField,
usePageNavigation,
useResourceList,
} from "@conusai-kit/ui/patterns"
import { useState } from "react"
import type { CommentReceipt, CommentView, TaskReceipt, TaskView } from "../contract/index.js"
import { describeFailure, type Labels } from "./labels.js"
type Page<T> = { rows: T[]; after?: string | undefined }
export function Tasks({
slugs,
seed,
href,
labels,
timeline = false,
}: {
slugs: ProjectSlugs
seed: Page<TaskView>
href: string
labels: Labels
timeline?: boolean
}) {
const client = useDataClient()
const { list } = useResourceList<TaskView>({
seed,
rowId: (task) => task.taskId,
read: async ({ after }) =>
(
await client.call<Page<TaskView>>(timeline ? "activities.timeline.list" : "activities.task.list", {
...slugs,
...(after ? { after } : {}),
})
).value,
})
return (
<ResourceList
{...list}
label={timeline ? labels.timeline : labels.open}
columns={[
{ id: "subject", header: labels.subject, wrap: true },
{
id: "status",
header: labels.status,
cell: (task) =>
task.status === "open"
? labels.stateOpen
: task.status === "done"
? labels.stateDone
: labels.stateCancelled,
},
]}
rowHref={(task) => `${href}/${task.taskId}`}
rowLabel={(task) => task.subject}
empty={{ title: labels.empty }}
/>
)
}
export function TaskForm({
slugs,
task,
href,
labels,
}: {
slugs: ProjectSlugs
task?: TaskView
href: string
labels: Labels
}) {
const navigation = usePageNavigation()
const [requestId] = useState(() => crypto.randomUUID())
const mutation = useWrite<Record<string, unknown>, TaskReceipt>(
task ? "activities.task.update" : "activities.task.create",
)
const submit = async ({ subject, body = "" }: FormValues) => {
const result = await mutation.mutate({
...slugs,
subject,
body,
requestId,
...(task ? { taskId: task.taskId, expectedVersion: task.revision } : {}),
})
navigation.navigate(`${href}/${result.taskId}`)
}
return (
<PageSection title={task ? labels.update : labels.create} variant="card">
<FormLayout
key={requestId}
describeError={(error) => describeFailure(error, labels)}
submitLabel={task ? labels.update : labels.create}
busyLabel={labels.busy}
onSubmit={submit}
>
<TextField name="subject" label={labels.subject} required maxLength={200} defaultValue={task?.subject} />
<TextField name="body" label={labels.body} maxLength={10000} defaultValue={task?.body} />
</FormLayout>
</PageSection>
)
}
export function TaskState({ slugs, task, labels }: { slugs: ProjectSlugs; task: TaskView; labels: Labels }) {
const navigation = usePageNavigation()
const [requestId, setRequestId] = useState(() => crypto.randomUUID())
const change = useWrite<Record<string, unknown>, TaskReceipt>(
task.status === "done" ? "activities.task.reopen" : "activities.task.complete",
)
if (task.status === "cancelled") return null
const submit = async () => {
await change.mutate({ ...slugs, taskId: task.taskId, expectedVersion: task.revision, requestId })
await navigation.refresh()
setRequestId(crypto.randomUUID())
}
return (
<FormLayout
key={requestId}
describeError={(error) => describeFailure(error, labels)}
submitLabel={task.status === "done" ? labels.reopen : labels.complete}
busyLabel={labels.busy}
onSubmit={submit}
/>
)
}
export function Comments({
slugs,
task,
seed,
labels,
}: {
slugs: ProjectSlugs
task: TaskView
seed: Page<CommentView>
labels: Labels
}) {
const client = useDataClient()
const { list } = useResourceList<CommentView>({
seed,
rowId: (comment) => comment.commentId,
read: async ({ after }) =>
(
await client.call<Page<CommentView>>("activities.comment.list", {
...slugs,
taskId: task.taskId,
...(after ? { after } : {}),
})
).value,
})
return (
<ResourceList
{...list}
label={labels.comments}
columns={[
{
id: "body",
header: labels.comment,
wrap: true,
cell: (comment) => (comment.erased ? labels.erased : comment.body),
},
]}
rowLabel={(comment) => (comment.erased ? labels.erased : comment.body)}
empty={{ title: labels.emptyComments }}
/>
)
}
export function CommentForm({ slugs, task, labels }: { slugs: ProjectSlugs; task: TaskView; labels: Labels }) {
const navigation = usePageNavigation()
const [requestId, setRequestId] = useState(() => crypto.randomUUID())
const add = useWrite<Record<string, unknown>, CommentReceipt>("activities.comment.add")
const submit = async ({ body = "" }: FormValues) => {
await add.mutate({ ...slugs, taskId: task.taskId, expectedVersion: task.revision, requestId, body })
await navigation.refresh()
setRequestId(crypto.randomUUID())
}
return (
<PageSection title={labels.addComment}>
<FormLayout
key={requestId}
describeError={(error) => describeFailure(error, labels)}
submitLabel={labels.addComment}
busyLabel={labels.busy}
onSubmit={submit}
>
<TextField name="body" label={labels.comment} required maxLength={10000} />
</FormLayout>
</PageSection>
)
}