Add Cancel import button to dashboard

action: handles _action=cancel by calling POST /pipeline/cancel/:jobId
UI:
- Start button disabled while a job is running for that source
- Cancel import button appears only when the active source has a
  running job (status not in done/error/cancelled)
- Toast shown on cancel request confirming stop after current step
- runningSources excludes cancelled status so tab resets correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MOHAN 2026-06-04 16:57:57 +05:30
parent eae9f19e9d
commit 9f942b4205

View File

@ -90,6 +90,25 @@ export const action = async ({ request }) => {
const formData = await request.formData();
const backendApiUrl = getBackendApiUrl();
const shop = session.shop;
const _action = String(formData.get("_action") || "run");
// Cancel a running job
if (_action === "cancel") {
const jobId = String(formData.get("jobId") || "").trim();
if (!jobId) return { ok: false, error: "Missing jobId." };
try {
const response = await fetch(
`${backendApiUrl}/pipeline/cancel/${encodeURIComponent(jobId)}`,
{ method: "POST" }
);
const payload = await readJsonSafe(response);
return { ok: response.ok, cancelled: true, jobId, ...payload };
} catch (error) {
return { ok: false, error: error.message };
}
}
// Start a new import job
const limitValue = String(formData.get("limit") || "").trim();
const source = String(formData.get("source") || "kyt").trim() || "kyt";
const limit = limitValue ? Number(limitValue) : null;
@ -97,9 +116,7 @@ export const action = async ({ request }) => {
try {
const response = await fetch(`${backendApiUrl}/pipeline/run`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
shop,
source,
@ -109,10 +126,7 @@ export const action = async ({ request }) => {
const payload = await readJsonSafe(response);
if (!response.ok) {
return {
ok: false,
error: payload?.error || "Failed to start import job.",
};
return { ok: false, error: payload?.error || "Failed to start import job." };
}
return {
@ -124,10 +138,7 @@ export const action = async ({ request }) => {
backendApiUrl,
};
} catch (error) {
return {
ok: false,
error: error.message,
};
return { ok: false, error: error.message };
}
};
@ -741,7 +752,7 @@ export default function Index() {
const runningSources = useMemo(() => {
return new Set(
Object.entries(sourceJobs || {})
.filter(([, sourceJob]) => sourceJob && !["done", "error"].includes(sourceJob.status))
.filter(([, sourceJob]) => sourceJob && !["done", "error", "cancelled"].includes(sourceJob.status))
.map(([sourceKey]) => sourceKey),
);
}, [sourceJobs]);
@ -780,6 +791,11 @@ export default function Index() {
return;
}
if (fetcher.data.ok && fetcher.data.cancelled) {
shopify.toast.show("Import cancellation requested — stopping after current step.");
return;
}
if (fetcher.data.ok && fetcher.data.jobId) {
const sourceKey = fetcher.data.source || activeSourceConfig.sourceKey;
setActiveSource(sourceKey);
@ -955,6 +971,7 @@ export default function Index() {
Launch this source independently and track its live sync below.
</p>
<fetcher.Form method="post">
<input type="hidden" name="_action" value="run" />
<input type="hidden" name="source" value={activeSourceConfig.sourceKey} />
<div className={styles.controlForm}>
<div className={styles.actionRow}>
@ -962,6 +979,7 @@ export default function Index() {
type="submit"
variant="primary"
{...(connection?.status !== 1 ? { disabled: true } : {})}
{...(isSubmitting || runningSources.has(activeSourceConfig.sourceKey) ? { disabled: true } : {})}
{...(isSubmitting ? { loading: true } : {})}
>
Start {activeSourceConfig.label}
@ -969,6 +987,23 @@ export default function Index() {
</div>
</div>
</fetcher.Form>
{runningSources.has(activeSourceConfig.sourceKey) && activeJobId && (
<fetcher.Form method="post" style={{ marginTop: "8px" }}>
<input type="hidden" name="_action" value="cancel" />
<input type="hidden" name="jobId" value={activeJobId} />
<div className={styles.actionRow}>
<s-button
type="submit"
variant="secondary"
tone="critical"
{...(fetcher.state !== "idle" ? { loading: true } : {})}
>
Cancel import
</s-button>
</div>
</fetcher.Form>
)}
</div>
</div>