From 9f942b4205d89c2a92752d4a7540df1458f02030 Mon Sep 17 00:00:00 2001
From: MOHAN
Date: Thu, 4 Jun 2026 16:57:57 +0530
Subject: [PATCH] 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
---
app/routes/app._index.jsx | 59 +++++++++++++++++++++++++++++++--------
1 file changed, 47 insertions(+), 12 deletions(-)
diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx
index 0bda272..673c2fd 100644
--- a/app/routes/app._index.jsx
+++ b/app/routes/app._index.jsx
@@ -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.
+
@@ -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() {
+
+ {runningSources.has(activeSourceConfig.sourceKey) && activeJobId && (
+
+
+
+
+
+ Cancel import
+
+
+
+ )}