feat: update board form layout and improve input handling for better user experience
This commit is contained in:
@@ -333,7 +333,6 @@ export default function EditBoardPage() {
|
|||||||
Update board settings and gateway.
|
Update board settings and gateway.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
@@ -347,179 +346,181 @@ export default function EditBoardPage() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
|
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
|
||||||
>
|
>
|
||||||
{resolvedBoardType !== "general" &&
|
{resolvedBoardType !== "general" &&
|
||||||
baseBoard &&
|
baseBoard &&
|
||||||
!(baseBoard.goal_confirmed ?? false) ? (
|
!(baseBoard.goal_confirmed ?? false) ? (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
|
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-semibold text-amber-900">
|
<p className="text-sm font-semibold text-amber-900">
|
||||||
Goal needs confirmation
|
Goal needs confirmation
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-amber-800/80">
|
<p className="mt-1 text-xs text-amber-800/80">
|
||||||
Start onboarding to draft an objective and success
|
Start onboarding to draft an objective and success
|
||||||
metrics.
|
metrics.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setIsOnboardingOpen(true)}
|
||||||
|
disabled={isLoading || !baseBoard}
|
||||||
|
>
|
||||||
|
Start onboarding
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Board name <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={resolvedName}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
placeholder="Board name"
|
||||||
|
disabled={isLoading || !baseBoard}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Gateway <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<SearchableSelect
|
||||||
|
ariaLabel="Select gateway"
|
||||||
|
value={displayGatewayId}
|
||||||
|
onValueChange={setGatewayId}
|
||||||
|
options={gatewayOptions}
|
||||||
|
placeholder="Select gateway"
|
||||||
|
searchPlaceholder="Search gateways..."
|
||||||
|
emptyMessage="No gateways found."
|
||||||
|
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
||||||
|
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
||||||
|
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Board type
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={resolvedBoardType}
|
||||||
|
onValueChange={setBoardType}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select board type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="goal">Goal</SelectItem>
|
||||||
|
<SelectItem value="general">General</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Board group
|
||||||
|
</label>
|
||||||
|
<SearchableSelect
|
||||||
|
ariaLabel="Select board group"
|
||||||
|
value={resolvedBoardGroupId}
|
||||||
|
onValueChange={setBoardGroupId}
|
||||||
|
options={groupOptions}
|
||||||
|
placeholder="No group"
|
||||||
|
searchPlaceholder="Search groups..."
|
||||||
|
emptyMessage="No groups found."
|
||||||
|
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
||||||
|
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
||||||
|
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Boards in the same group can share cross-board context
|
||||||
|
for agents.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
type="button"
|
<label className="text-sm font-medium text-slate-900">
|
||||||
variant="secondary"
|
Target date
|
||||||
onClick={() => setIsOnboardingOpen(true)}
|
</label>
|
||||||
disabled={isLoading || !baseBoard}
|
<Input
|
||||||
>
|
type="date"
|
||||||
Start onboarding
|
value={resolvedTargetDate}
|
||||||
</Button>
|
onChange={(event) =>
|
||||||
|
setTargetDate(event.target.value)
|
||||||
|
}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-900">
|
|
||||||
Board name <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={resolvedName}
|
|
||||||
onChange={(event) => setName(event.target.value)}
|
|
||||||
placeholder="Board name"
|
|
||||||
disabled={isLoading || !baseBoard}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-900">
|
|
||||||
Gateway <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<SearchableSelect
|
|
||||||
ariaLabel="Select gateway"
|
|
||||||
value={displayGatewayId}
|
|
||||||
onValueChange={setGatewayId}
|
|
||||||
options={gatewayOptions}
|
|
||||||
placeholder="Select gateway"
|
|
||||||
searchPlaceholder="Search gateways..."
|
|
||||||
emptyMessage="No gateways found."
|
|
||||||
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
|
||||||
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
|
||||||
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Board type
|
Objective
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Textarea
|
||||||
value={resolvedBoardType}
|
value={resolvedObjective}
|
||||||
onValueChange={setBoardType}
|
onChange={(event) => setObjective(event.target.value)}
|
||||||
>
|
placeholder="What should this board achieve?"
|
||||||
<SelectTrigger>
|
className="min-h-[120px]"
|
||||||
<SelectValue placeholder="Select board type" />
|
disabled={isLoading}
|
||||||
</SelectTrigger>
|
/>
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="goal">Goal</SelectItem>
|
|
||||||
<SelectItem value="general">General</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Board group
|
Success metrics (JSON)
|
||||||
</label>
|
</label>
|
||||||
<SearchableSelect
|
<Textarea
|
||||||
ariaLabel="Select board group"
|
value={resolvedSuccessMetrics}
|
||||||
value={resolvedBoardGroupId}
|
onChange={(event) =>
|
||||||
onValueChange={setBoardGroupId}
|
setSuccessMetrics(event.target.value)
|
||||||
options={groupOptions}
|
}
|
||||||
placeholder="No group"
|
placeholder='e.g. { "target": "Launch by week 2" }'
|
||||||
searchPlaceholder="Search groups..."
|
className="min-h-[140px] font-mono text-xs"
|
||||||
emptyMessage="No groups found."
|
|
||||||
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
|
||||||
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
|
||||||
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Boards in the same group can share cross-board context
|
Add key outcomes so the lead agent can measure progress.
|
||||||
for agents.
|
|
||||||
</p>
|
</p>
|
||||||
|
{metricsError ? (
|
||||||
|
<p className="text-xs text-red-500">{metricsError}</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-900">
|
|
||||||
Target date
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={resolvedTargetDate}
|
|
||||||
onChange={(event) => setTargetDate(event.target.value)}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
{gateways.length === 0 ? (
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<div className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
||||||
Objective
|
<p>
|
||||||
</label>
|
No gateways available. Create one in Gateways to
|
||||||
<Textarea
|
continue.
|
||||||
value={resolvedObjective}
|
</p>
|
||||||
onChange={(event) => setObjective(event.target.value)}
|
</div>
|
||||||
placeholder="What should this board achieve?"
|
|
||||||
className="min-h-[120px]"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-900">
|
|
||||||
Success metrics (JSON)
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
value={resolvedSuccessMetrics}
|
|
||||||
onChange={(event) =>
|
|
||||||
setSuccessMetrics(event.target.value)
|
|
||||||
}
|
|
||||||
placeholder='e.g. { "target": "Launch by week 2" }'
|
|
||||||
className="min-h-[140px] font-mono text-xs"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-slate-500">
|
|
||||||
Add key outcomes so the lead agent can measure progress.
|
|
||||||
</p>
|
|
||||||
{metricsError ? (
|
|
||||||
<p className="text-xs text-red-500">{metricsError}</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
|
||||||
|
|
||||||
{gateways.length === 0 ? (
|
{errorMessage ? (
|
||||||
<div className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
<p>
|
) : null}
|
||||||
No gateways available. Create one in Gateways to
|
|
||||||
continue.
|
<div className="flex justify-end gap-3">
|
||||||
</p>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => router.push(`/boards/${boardId}`)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !baseBoard || !isFormReady}
|
||||||
|
>
|
||||||
|
{isLoading ? "Saving…" : "Save changes"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</form>
|
||||||
|
</div>
|
||||||
{errorMessage ? (
|
)}
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => router.push(`/boards/${boardId}`)}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || !baseBoard || !isFormReady}
|
|
||||||
>
|
|
||||||
{isLoading ? "Saving…" : "Save changes"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</SignedIn>
|
</SignedIn>
|
||||||
|
|||||||
Reference in New Issue
Block a user