mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 06:16:10 +00:00
feat(cron): delivery UI with Select dropdowns + targets fetching
- Replace free-text Input with Select for Channel + To fields - Fetch delivery targets via HEARTBEAT_TARGETS RPC - Populate To options based on selected Channel - Fallback to Input when no targets available
This commit is contained in:
1 parent
4a0d938452
commit
cac3974e7c
1 file changed
+97
-22
@@ -1,17 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save, Settings, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { ConfigGroupHeader } from "@/components/shared/config-group-header";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { getAllIanaTimezones, isValidIanaTimezone } from "@/lib/constants";
|
||||
import { toast } from "@/stores/use-toast-store";
|
||||
import { useChannels } from "@/pages/channels/hooks/use-channels";
|
||||
import { useWs } from "@/hooks/use-ws";
|
||||
import { Methods } from "@/api/protocol";
|
||||
import type { CronJob, CronJobPatch } from "../hooks/use-cron";
|
||||
|
||||
interface DeliveryTarget {
|
||||
channel: string;
|
||||
chatId: string;
|
||||
title?: string;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface CronAdvancedDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -34,6 +45,9 @@ function deriveState(job: CronJob) {
|
||||
export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAdvancedDialogProps) {
|
||||
const { t } = useTranslation("cron");
|
||||
const { t: tc } = useTranslation("common");
|
||||
const ws = useWs();
|
||||
const { channels: availableChannels } = useChannels();
|
||||
const channelNames = Object.keys(availableChannels);
|
||||
|
||||
const init = deriveState(job);
|
||||
const [timezone, setTimezone] = useState(init.timezone);
|
||||
@@ -44,6 +58,17 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
const [deleteAfterRun, setDeleteAfterRun] = useState(init.deleteAfterRun);
|
||||
const [stateless, setStateless] = useState(init.stateless);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [targets, setTargets] = useState<DeliveryTarget[]>([]);
|
||||
|
||||
const fetchTargets = useCallback(async () => {
|
||||
if (!job.agentId || !ws.isConnected) return;
|
||||
try {
|
||||
const res = await ws.call<{ targets: DeliveryTarget[] }>(
|
||||
Methods.HEARTBEAT_TARGETS, { agentId: job.agentId },
|
||||
);
|
||||
setTargets(res.targets ?? []);
|
||||
} catch { /* ignore — fallback to Input */ }
|
||||
}, [ws, job.agentId]);
|
||||
|
||||
// Re-sync when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -56,6 +81,7 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
setWakeHeartbeat(s.wakeHeartbeat);
|
||||
setDeleteAfterRun(s.deleteAfterRun);
|
||||
setStateless(s.stateless);
|
||||
fetchTargets();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
@@ -73,7 +99,7 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
await onUpdate(job.id, {
|
||||
schedule: {
|
||||
...job.schedule,
|
||||
tz: timezone !== "UTC" ? timezone : undefined,
|
||||
tz: timezone !== "UTC" ? timezone : "",
|
||||
},
|
||||
deliver,
|
||||
deliverChannel: deliver ? channel.trim() || undefined : undefined,
|
||||
@@ -131,28 +157,77 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
</div>
|
||||
|
||||
{deliver && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="adv-channel">{t("detail.channelLabel")}</Label>
|
||||
<Input
|
||||
id="adv-channel"
|
||||
value={channel}
|
||||
onChange={(e) => setChannel(e.target.value)}
|
||||
placeholder={t("detail.channelPlaceholder")}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Label>{t("detail.channelLabel")}</Label>
|
||||
{channelNames.length > 0 ? (
|
||||
<Select
|
||||
value={channel || "__none__"}
|
||||
onValueChange={(v) => { setChannel(v === "__none__" ? "" : v); setTo(""); }}
|
||||
>
|
||||
<SelectTrigger className="text-base md:text-sm">
|
||||
<SelectValue placeholder={t("detail.channelPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">{t("detail.channelPlaceholder")}</SelectItem>
|
||||
{channelNames.map((ch) => (
|
||||
<SelectItem key={ch} value={ch}>{ch}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={channel}
|
||||
onChange={(e) => setChannel(e.target.value)}
|
||||
placeholder={t("detail.channelPlaceholder")}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="adv-to">{t("detail.toLabel")}</Label>
|
||||
<Input
|
||||
id="adv-to"
|
||||
value={to}
|
||||
onChange={(e) => setTo(e.target.value)}
|
||||
placeholder={t("detail.toPlaceholder")}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Label>{t("detail.toLabel")}</Label>
|
||||
{(() => {
|
||||
if (!channel) {
|
||||
return (
|
||||
<Input
|
||||
placeholder={t("detail.channelPlaceholder")}
|
||||
disabled
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const filtered = targets.filter((tgt) => tgt.channel === channel);
|
||||
if (filtered.length > 0) {
|
||||
return (
|
||||
<Select
|
||||
value={to || "__none__"}
|
||||
onValueChange={(v) => setTo(v === "__none__" ? "" : v)}
|
||||
>
|
||||
<SelectTrigger className="text-base md:text-sm">
|
||||
<SelectValue placeholder={t("detail.toPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">{t("detail.toPlaceholder")}</SelectItem>
|
||||
{filtered.map((tgt) => (
|
||||
<SelectItem key={tgt.chatId} value={tgt.chatId} title={tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}>
|
||||
<span className="truncate">{tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
value={to}
|
||||
onChange={(e) => setTo(e.target.value)}
|
||||
placeholder={t("detail.toPlaceholder")}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border px-3 py-2.5">
|
||||
|
||||
Reference in new issue
Block a user