Let a conversation's menu show the routines that run in it
The header menu had Tasks but nothing for the schedules whose results land in that conversation. It now has Schedules, right under Tasks, opened in a modal that lists only what runs there: a bot's own chat holds the routines that run nowhere, a workspace only that bot's routines in it (`routineScopeSql`, tested against SQLite). The list loads when the modal opens, through `api/admin/bots/list-routines`, which checks the user chats in that conversation first, so the page props stay small. `grabCronJobs` gained an optional `workspace_id`: left out it still means everywhere. The routines page's row, form and edit modal moved to `src/components/general/`, and its subtitle to `src/utils/`, since both the page and the modal use them.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||
import type { CronJobWithNames } from "@/src/types";
|
||||
import RoutineForm from "./routine-form";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Open while set.
|
||||
*/
|
||||
cron_job?: CronJobWithNames;
|
||||
onClose: () => void;
|
||||
onSaved: (params: { cron_job: CronJobWithNames }) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits a routine in a modal: its name, schedule and brief.
|
||||
*/
|
||||
export default function RoutineEditModal({ cron_job, onClose, onSaved }: Props) {
|
||||
return (
|
||||
<AddModalShell
|
||||
open={Boolean(cron_job)}
|
||||
setOpen={(open) => !open && onClose()}
|
||||
title={`Edit ${cron_job?.title || "the routine"}`}
|
||||
description={`${cron_job?.bot_name || "The bot"} gets the brief as a task at every run.`}
|
||||
>
|
||||
{cron_job ? <RoutineForm key={cron_job.id} cron_job={cron_job} onCancel={onClose} onSaved={onSaved} /> : null}
|
||||
</AddModalShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import CodeEditor from "@/src/components/general/code-editor";
|
||||
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions";
|
||||
import type { CronJobWithNames } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
cron_job: CronJobWithNames;
|
||||
onCancel: () => void;
|
||||
onSaved: (params: { cron_job: CronJobWithNames }) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A routine's name, schedule (a cron expression in UTC) and brief. The
|
||||
* routines page and the conversation's routines modal both edit with it.
|
||||
*/
|
||||
export default function RoutineForm({ cron_job, onCancel, onSaved }: Props) {
|
||||
const [title, setTitle] = useState(cron_job.title || "");
|
||||
const [schedule, setSchedule] = useState(cron_job.schedule || "");
|
||||
const [instructions, setInstructions] = useState(cron_job.instructions || "");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const submit = async () => {
|
||||
if (loading) return;
|
||||
if (!window.confirm(`Save ${title || "this routine"}?`)) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await postAdminApi<CronJobWithNames>({
|
||||
path: "routines/update-routine",
|
||||
body: { cron_job_id: cron_job.id, title, schedule, instructions },
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res.success || !res.singleRes) {
|
||||
setError(res.msg || "Couldn't save the routine.");
|
||||
return;
|
||||
}
|
||||
|
||||
onSaved({ cron_job: res.singleRes });
|
||||
};
|
||||
|
||||
return (
|
||||
<Form className="w-full gap-4! items-stretch pt-4" onSubmit={submit}>
|
||||
<Input
|
||||
id="routine-title"
|
||||
title="Name"
|
||||
label="Name"
|
||||
showLabel
|
||||
autoComplete="off"
|
||||
defaultValue={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
id="routine-schedule"
|
||||
title="Schedule"
|
||||
label="Schedule"
|
||||
showLabel
|
||||
autoComplete="off"
|
||||
placeholder="0 9 * * MON-FRI"
|
||||
defaultValue={schedule}
|
||||
info="Cron, in UTC: minute hour day month weekday, or @hourly, @daily, @weekly."
|
||||
onChange={(e) => setSchedule(e.target.value)}
|
||||
/>
|
||||
<CodeEditor
|
||||
mode="markdown"
|
||||
className="h-40"
|
||||
title="Brief"
|
||||
content={instructions}
|
||||
info="What the bot does at every run, and how it knows it's done."
|
||||
onChange={(value) => setInstructions(value)}
|
||||
/>
|
||||
<AddFormActions submit_label="Save" loading={loading} error={error} onCancel={onCancel} />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
import botStoneProps from "@/src/utils/bot-stone-props";
|
||||
import type { CronJobWithNames } from "@/src/types";
|
||||
import SettingsListRow from "@/src/pages/admin/settings/(partials)/settings-list-row";
|
||||
import SettingsRowAction from "@/src/pages/admin/settings/(partials)/settings-row-action";
|
||||
import routineSubtitle from "@/src/utils/routine-subtitle";
|
||||
import BotAvatar from "./bot-avatar";
|
||||
|
||||
type Props = {
|
||||
cron_job: CronJobWithNames;
|
||||
busy?: boolean;
|
||||
onToggle: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
/**
|
||||
* In a conversation's own modal the Chat link and the bot's and
|
||||
* workspace's names are already the ones on screen.
|
||||
*/
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* One routine: its bot's avatar, its name (Paused or Done when it is),
|
||||
* whose it is and when it runs, then Chat, Pause or Resume, Edit, Delete.
|
||||
*/
|
||||
export default function RoutineRow({ cron_job, busy, onToggle, onEdit, onDelete, compact }: Props) {
|
||||
const title = cron_job.title || `Routine ${cron_job.id}`;
|
||||
const done = Boolean(cron_job.max_runs && Number(cron_job.run_count || 0) >= cron_job.max_runs);
|
||||
const chat_url = cron_job.workspace_id
|
||||
? `/admin/workspaces/${cron_job.workspace_id}/bots/${cron_job.bot_id}`
|
||||
: `/admin/bots/${cron_job.bot_id}`;
|
||||
|
||||
return (
|
||||
<SettingsListRow
|
||||
avatar={
|
||||
<BotAvatar
|
||||
image_media_id={cron_job.bot_image_media_id}
|
||||
{...botStoneProps({ color: cron_job.bot_color, stone: cron_job.bot_stone, hue: cron_job.bot_hue })}
|
||||
face={(cron_job.bot_face || "idle") as BotFace}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
badge={cron_job.enabled ? undefined : done ? "Done" : "Paused"}
|
||||
subtitle={routineSubtitle({
|
||||
cron_job,
|
||||
show_bot: !compact,
|
||||
show_workspace: !compact,
|
||||
})}
|
||||
>
|
||||
{compact ? null : (
|
||||
<SettingsRowAction title={`Chat with ${cron_job.bot_name || "the bot"}`} label="Chat" href={chat_url} />
|
||||
)}
|
||||
<SettingsRowAction
|
||||
title={cron_job.enabled ? `Pause ${title}` : `Resume ${title}`}
|
||||
label={cron_job.enabled ? "Pause" : "Resume"}
|
||||
disabled={busy}
|
||||
onClick={onToggle}
|
||||
/>
|
||||
<SettingsRowAction title={`Edit ${title}`} label="Edit" disabled={busy} onClick={onEdit} />
|
||||
<SettingsRowAction title={`Delete ${title}`} label="Delete" disabled={busy} onClick={onDelete} />
|
||||
</SettingsListRow>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user