Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export function SessionsNone() {
panelClassName="max-w-full"
accessory={
<LinkButton
to={docsPath("/ai-chat/sessions")}
to={docsPath("ai-chat/sessions")}
variant="docs/small"
LeadingIcon={BookOpenIcon}
>
Expand Down
111 changes: 73 additions & 38 deletions apps/webapp/app/components/schedules/ScheduleInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
TrashIcon,
} from "@heroicons/react/20/solid";
import { DialogDescription } from "@radix-ui/react-dialog";
import { Form, useLocation } from "@remix-run/react";
import { type FetcherWithComponents, Form, useLocation } from "@remix-run/react";
import { type ReactNode } from "react";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
Expand Down Expand Up @@ -76,9 +76,22 @@ type Props = {
* is rendered somewhere else (e.g. in a sheet on a different page).
*/
actionPath?: string;
/** When set, Edit calls back instead of navigating to the standalone edit page. */
onEdit?: () => void;
/** Submits enable/disable via this fetcher with `_format=json` so the host stays put. */
activeToggleFetcher?: FetcherWithComponents<unknown>;
/** Submits delete via this fetcher with `_format=json` so the host stays put. */
deleteFetcher?: FetcherWithComponents<unknown>;
};

export function ScheduleInspector({ schedule, headerActions, actionPath }: Props) {
export function ScheduleInspector({
schedule,
headerActions,
actionPath,
onEdit,
activeToggleFetcher,
deleteFetcher,
}: Props) {
const location = useLocation();
const organization = useOrganization();
const project = useProject();
Expand All @@ -91,7 +104,7 @@ export function ScheduleInspector({ schedule, headerActions, actionPath }: Props
<div
className={cn(
"grid h-full max-h-full overflow-hidden bg-background-bright",
isImperative ? "grid-rows-[2.5rem_1fr_3.25rem]" : "grid-rows-[2.5rem_1fr]"
isImperative ? "grid-rows-[2.5rem_1fr_auto]" : "grid-rows-[2.5rem_1fr]"
)}
>
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
Expand Down Expand Up @@ -244,30 +257,38 @@ export function ScheduleInspector({ schedule, headerActions, actionPath }: Props
</div>
</div>
{isImperative && (
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2 py-2">
<div className="flex items-center gap-2">
<Form method="post" action={actionPath}>
<Button
type="submit"
variant="tertiary/medium"
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
name="action"
value={schedule.active ? "disable" : "enable"}
>
{schedule.active ? "Disable" : "Enable"}
</Button>
</Form>
{(() => {
const ToggleForm = activeToggleFetcher?.Form ?? Form;
const isSubmitting = activeToggleFetcher?.state === "submitting";
return (
<ToggleForm method="post" action={actionPath}>
{activeToggleFetcher ? <input type="hidden" name="_format" value="json" /> : null}
<Button
type="submit"
variant="secondary/small"
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
name="action"
value={schedule.active ? "disable" : "enable"}
disabled={isSubmitting}
>
{schedule.active ? "Disable" : "Enable"}
</Button>
</ToggleForm>
);
})()}
<Dialog>
<DialogTrigger asChild>
<Button
type="submit"
variant="danger/medium"
variant="danger/small"
LeadingIcon={TrashIcon}
name="action"
value="delete"
>
Delete
Delete
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-sm">
Expand All @@ -276,31 +297,45 @@ export function ScheduleInspector({ schedule, headerActions, actionPath }: Props
Are you sure you want to delete this schedule? This can't be reversed.
</DialogDescription>
<DialogFooter className="sm:justify-end">
<Form method="post" action={actionPath}>
<Button
type="submit"
variant="danger/medium"
LeadingIcon={TrashIcon}
name="action"
value="delete"
>
Delete
</Button>
</Form>
{(() => {
const DeleteForm = deleteFetcher?.Form ?? Form;
const isSubmitting = deleteFetcher?.state === "submitting";
return (
<DeleteForm method="post" action={actionPath}>
{deleteFetcher ? <input type="hidden" name="_format" value="json" /> : null}
<Button
type="submit"
variant="danger/medium"
LeadingIcon={TrashIcon}
name="action"
value="delete"
disabled={isSubmitting}
>
Delete
</Button>
</DeleteForm>
);
})()}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="flex items-center gap-4">
<LinkButton
variant="tertiary/medium"
to={`${v3EditSchedulePath(organization, project, environment, schedule)}${
location.search
}`}
LeadingIcon={PencilSquareIcon}
>
Edit schedule
</LinkButton>
{onEdit ? (
<Button variant="secondary/small" LeadingIcon={PencilSquareIcon} onClick={onEdit}>
Edit schedule…
</Button>
) : (
<LinkButton
variant="secondary/small"
to={`${v3EditSchedulePath(organization, project, environment, schedule)}${
location.search
}`}
LeadingIcon={PencilSquareIcon}
>
Edit schedule…
</LinkButton>
)}
</div>
</div>
)}
Expand Down
Comment thread
samejr marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server";
import { requireUserId } from "~/services/session.server";
import { v3EnvironmentPath, v3ScheduleParams, v3SchedulePath } from "~/utils/pathBuilder";
import { throwNotFound } from "~/utils/httpErrors";
import { DeleteTaskScheduleService } from "~/v3/services/deleteTaskSchedule.server";
import { SetActiveOnTaskScheduleService } from "~/v3/services/setActiveOnTaskSchedule.server";

Expand Down Expand Up @@ -45,11 +44,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
environmentId: environment.id,
});

if (!result) {
throwNotFound("Schedule not found");
}

return typedjson({ schedule: result.schedule });
// Return null (not a 404 throw) so fetcher-driven hosts (e.g. the sheet
// running this loader after a delete-in-flight) don't surface a
// page-level error boundary. The standalone Page below renders a
// not-found message when `schedule` is null.
return typedjson({ schedule: result?.schedule ?? null });
Comment thread
samejr marked this conversation as resolved.
};

const schema = z.discriminatedUnion("action", [
Expand All @@ -76,13 +75,20 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
return json(submission);
}

// `_format=json` → return JSON instead of redirecting; caller stays put.
const wantsJson = formData.get("_format") === "json";

const project = await prisma.project.findFirst({
where: {
slug: projectParam,
},
});

if (!project) {
const message = `No project found with slug ${projectParam}`;
if (wantsJson) {
return json({ ok: false as const, message }, { status: 404 });
}
return redirectWithErrorMessage(
v3SchedulePath(
{ slug: organizationSlug },
Expand All @@ -91,7 +97,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
{ friendlyId: scheduleParam }
),
request,
`No project found with slug ${projectParam}`
message
);
}

Expand All @@ -104,12 +110,21 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
userId,
friendlyId: scheduleParam,
});
if (wantsJson) {
return json({ ok: true as const, message: `${scheduleParam} deleted` });
}
return redirectWithSuccessMessage(
v3EnvironmentPath({ slug: organizationSlug }, { slug: projectParam }, { slug: envParam }),
request,
`${scheduleParam} deleted`
);
} catch (e) {
const message = `${scheduleParam} could not be deleted: ${
e instanceof Error ? e.message : JSON.stringify(e)
}`;
if (wantsJson) {
return json({ ok: false as const, message }, { status: 500 });
}
return redirectWithErrorMessage(
v3SchedulePath(
{ slug: organizationSlug },
Expand All @@ -118,9 +133,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
{ friendlyId: scheduleParam }
),
request,
`${scheduleParam} could not be deleted: ${
e instanceof Error ? e.message : JSON.stringify(e)
}`
message
);
}
}
Expand All @@ -135,6 +148,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
friendlyId: scheduleParam,
active,
});
if (wantsJson) {
return json({ ok: true as const, active });
}
return redirectWithSuccessMessage(
v3SchedulePath(
{ slug: organizationSlug },
Expand All @@ -146,6 +162,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
`${scheduleParam} ${active ? "enabled" : "disabled"}`
);
} catch (e) {
const message = e instanceof Error ? e.message : JSON.stringify(e);
if (wantsJson) {
return json({ ok: false as const, message }, { status: 500 });
}
return redirectWithErrorMessage(
v3SchedulePath(
{ slug: organizationSlug },
Expand All @@ -154,9 +174,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
{ friendlyId: scheduleParam }
),
request,
`${scheduleParam} could not be ${active ? "enabled" : "disabled"}: ${
e instanceof Error ? e.message : JSON.stringify(e)
}`
`${scheduleParam} could not be ${active ? "enabled" : "disabled"}: ${message}`
);
}
}
Expand All @@ -170,6 +188,20 @@ export default function Page() {
const project = useProject();
const environment = useEnvironment();

if (!schedule) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 bg-background-bright p-6">
<p className="text-sm text-text-bright">Schedule not found.</p>
<LinkButton
to={`${v3EnvironmentPath(organization, project, environment)}${location.search}`}
variant="secondary/small"
>
Back to tasks
</LinkButton>
</div>
);
}

return (
<ScheduleInspector
schedule={schedule}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function Page() {
<LinkButton
variant={"docs/small"}
LeadingIcon={BookOpenIcon}
to={docsPath("/ai-chat/sessions")}
to={docsPath("ai-chat/sessions")}
>
Sessions docs
</LinkButton>
Expand Down
Loading