feat(water-log): add Smartsheet setup instructions modal
Deploy to route.crispygoat.com / deploy (push) Successful in 3m54s
Deploy to route.crispygoat.com / deploy (push) Successful in 3m54s
Adds a 'Setup instructions' button to the Connection card that opens a modal with full onboarding guidance: - Prerequisites (Smartsheet account, admin access) - Step 1: Generate an API token (with link to Smartsheet docs) - Step 2: Sheet structure — full schema table mapping our 7 fields to Smartsheet column types (TEXT_NUMBER / DATETIME / DATE), with required vs optional flags and example values - Collapsible 'starter header row' CSV snippet users can paste into row 1 of a new sheet - Step 3: Connect flow (paste URL + token, test, map columns, save) - Common issues troubleshooting (401, 404 slug URLs, DATE column rejecting ISO timestamps) Modal UX: - role='dialog', aria-modal, aria-labelledby - Closes on X button, 'Got it' footer button, click-outside backdrop, or Escape key - Sticky header + footer so scrollable content stays framed - Locked to existing cream + forest palette (Field Almanac style) Implementation: - Added 'setupModalOpen' to state, OPEN_SETUP_MODAL/CLOSE_SETUP_MODAL actions to reducer - Extended Card subcomponent with optional 'action' slot (used here for the trigger button, reusable for future per-card actions) - Added useEffect that registers a window keydown listener only while the modal is open; cleans up on close or unmount - FIELD_SCHEMA const declared alongside the modal so future edits to the schema live next to their documentation
This commit is contained in:
@@ -95,6 +95,8 @@ type State = {
|
|||||||
// Recent activity
|
// Recent activity
|
||||||
recentLog: SmartsheetSyncLogEntry[];
|
recentLog: SmartsheetSyncLogEntry[];
|
||||||
loadingLog: boolean;
|
loadingLog: boolean;
|
||||||
|
/** Whether the setup-instructions modal is open. */
|
||||||
|
setupModalOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
@@ -114,7 +116,9 @@ type Action =
|
|||||||
| { type: "TEST_OK"; sheetName: string; columns: ColumnOption[] }
|
| { type: "TEST_OK"; sheetName: string; columns: ColumnOption[] }
|
||||||
| { type: "TEST_FAIL"; message: string }
|
| { type: "TEST_FAIL"; message: string }
|
||||||
| { type: "CLEAR_MESSAGE" }
|
| { type: "CLEAR_MESSAGE" }
|
||||||
| { type: "REFRESH_LOG"; log: SmartsheetSyncLogEntry[] };
|
| { type: "REFRESH_LOG"; log: SmartsheetSyncLogEntry[] }
|
||||||
|
| { type: "OPEN_SETUP_MODAL" }
|
||||||
|
| { type: "CLOSE_SETUP_MODAL" };
|
||||||
|
|
||||||
const EMPTY_MAPPING: SmartsheetColumnMapping = {
|
const EMPTY_MAPPING: SmartsheetColumnMapping = {
|
||||||
entry_id: "",
|
entry_id: "",
|
||||||
@@ -141,6 +145,7 @@ const initialState: State = {
|
|||||||
test: { state: "idle" },
|
test: { state: "idle" },
|
||||||
recentLog: [],
|
recentLog: [],
|
||||||
loadingLog: false,
|
loadingLog: false,
|
||||||
|
setupModalOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
@@ -218,6 +223,10 @@ function reducer(state: State, action: Action): State {
|
|||||||
return { ...state, message: null };
|
return { ...state, message: null };
|
||||||
case "REFRESH_LOG":
|
case "REFRESH_LOG":
|
||||||
return { ...state, recentLog: action.log };
|
return { ...state, recentLog: action.log };
|
||||||
|
case "OPEN_SETUP_MODAL":
|
||||||
|
return { ...state, setupModalOpen: true };
|
||||||
|
case "CLOSE_SETUP_MODAL":
|
||||||
|
return { ...state, setupModalOpen: false };
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -243,6 +252,18 @@ export default function SmartsheetIntegrationCard({
|
|||||||
void loadAll();
|
void loadAll();
|
||||||
}, [loadAll]);
|
}, [loadAll]);
|
||||||
|
|
||||||
|
// Close the setup modal on Escape. Listens only while the modal is open.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.setupModalOpen) return;
|
||||||
|
function onKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
dispatch({ type: "CLOSE_SETUP_MODAL" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, [state.setupModalOpen]);
|
||||||
|
|
||||||
async function handleSave(e: React.FormEvent) {
|
async function handleSave(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch({ type: "SAVE_START" });
|
dispatch({ type: "SAVE_START" });
|
||||||
@@ -331,6 +352,15 @@ export default function SmartsheetIntegrationCard({
|
|||||||
<Card
|
<Card
|
||||||
title="Connection"
|
title="Connection"
|
||||||
subtitle="Smartsheet API token + target sheet."
|
subtitle="Smartsheet API token + target sheet."
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => dispatch({ type: "OPEN_SETUP_MODAL" })}
|
||||||
|
className="rounded-md border border-[#d4d9d3] bg-white px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-[#5a5d5a] transition hover:border-[#1a4d2e] hover:text-[#1a4d2e]"
|
||||||
|
>
|
||||||
|
Setup instructions
|
||||||
|
</button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Sheet */}
|
{/* Sheet */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -603,6 +633,12 @@ export default function SmartsheetIntegrationCard({
|
|||||||
Paste an API token above to enable Save.
|
Paste an API token above to enable Save.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{state.setupModalOpen && (
|
||||||
|
<SetupInstructionsModal
|
||||||
|
onClose={() => dispatch({ type: "CLOSE_SETUP_MODAL" })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -612,17 +648,22 @@ export default function SmartsheetIntegrationCard({
|
|||||||
function Card({
|
function Card({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
action,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
action?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border border-[#d4d9d3] bg-white p-5 shadow-sm">
|
<section className="rounded-xl border border-[#d4d9d3] bg-white p-5 shadow-sm">
|
||||||
<header className="mb-4">
|
<header className="mb-4 flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
<h2 className="text-base font-semibold text-[#1d1d1f]">{title}</h2>
|
<h2 className="text-base font-semibold text-[#1d1d1f]">{title}</h2>
|
||||||
{subtitle && <p className="mt-0.5 text-xs text-[#5a5d5a]">{subtitle}</p>}
|
{subtitle && <p className="mt-0.5 text-xs text-[#5a5d5a]">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
{action && <div className="shrink-0">{action}</div>}
|
||||||
</header>
|
</header>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
@@ -688,3 +729,339 @@ function TestResultPanel({ test }: { test: TestState }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Setup instructions modal ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
const FIELD_SCHEMA: Array<{
|
||||||
|
key: keyof SmartsheetColumnMapping;
|
||||||
|
label: string;
|
||||||
|
required: boolean;
|
||||||
|
type: string;
|
||||||
|
example: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "entry_id",
|
||||||
|
label: "Entry ID",
|
||||||
|
required: true,
|
||||||
|
type: "TEXT_NUMBER",
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
description:
|
||||||
|
"Unique UUID for each water log row. We use this for dedup — if the same Entry ID is already in your sheet, we skip the insert instead of creating a duplicate row.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "logged_at",
|
||||||
|
label: "Logged At",
|
||||||
|
required: true,
|
||||||
|
type: "DATETIME (or DATE)",
|
||||||
|
example: "2026-07-03 14:50",
|
||||||
|
description:
|
||||||
|
"When the reading was taken. Use a DATETIME column for full timestamp, or DATE for day-only. We format automatically based on the column type.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "headgate",
|
||||||
|
label: "Headgate",
|
||||||
|
required: false,
|
||||||
|
type: "TEXT_NUMBER",
|
||||||
|
example: "Section 4 — North Field",
|
||||||
|
description: "Display name of the headgate the reading came from.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "measurement",
|
||||||
|
label: "Measurement",
|
||||||
|
required: false,
|
||||||
|
type: "TEXT_NUMBER",
|
||||||
|
example: "123.45",
|
||||||
|
description:
|
||||||
|
"The numeric reading. Sent as a string (e.g. \"123.45\"); Smartsheet's TEXT_NUMBER columns accept this and display it as a number.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "unit",
|
||||||
|
label: "Unit",
|
||||||
|
required: false,
|
||||||
|
type: "TEXT_NUMBER",
|
||||||
|
example: "CFS",
|
||||||
|
description: "Display unit, e.g. CFS, GPM, AF/Day, inches.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "irrigator",
|
||||||
|
label: "Irrigator",
|
||||||
|
required: false,
|
||||||
|
type: "TEXT_NUMBER",
|
||||||
|
example: "Jane Doe",
|
||||||
|
description: "Name of the irrigator who logged the reading.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "notes",
|
||||||
|
label: "Notes",
|
||||||
|
required: false,
|
||||||
|
type: "TEXT_NUMBER",
|
||||||
|
example: "Replaced meter seal",
|
||||||
|
description: "Free-text notes attached to the entry (may be empty).",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function SetupInstructionsModal({ onClose }: { onClose: () => void }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="smartsheet-setup-title"
|
||||||
|
onClick={(e) => {
|
||||||
|
// Close when clicking the backdrop (but not the panel itself).
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-xl border border-[#d4d9d3] bg-[#fdfaf2] shadow-xl">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="sticky top-0 z-10 flex items-start justify-between gap-4 border-b border-[#d4d9d3] bg-[#fdfaf2] px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-mono uppercase tracking-[0.3em] text-[#8a6b3b]">
|
||||||
|
§ 05.1 — Setup
|
||||||
|
</p>
|
||||||
|
<h2
|
||||||
|
id="smartsheet-setup-title"
|
||||||
|
className="mt-1 text-xl font-medium text-[#1a4d2e]"
|
||||||
|
style={{ fontFamily: "var(--font-display, Georgia, serif)" }}
|
||||||
|
>
|
||||||
|
Connect a Smartsheet
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close"
|
||||||
|
className="-mr-1 -mt-1 rounded-md p-2 text-[#5a5d5a] hover:bg-[#f4f1e8] hover:text-[#1d1d1f]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="h-5 w-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="space-y-6 px-6 py-5">
|
||||||
|
{/* Prerequisites */}
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-[#1d1d1f]">
|
||||||
|
Before you start
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-2 list-inside list-disc space-y-1 text-sm text-[#5a5d5a]">
|
||||||
|
<li>You need a Smartsheet account.</li>
|
||||||
|
<li>
|
||||||
|
The target sheet must exist in your account. We can add
|
||||||
|
columns to it for you (see Step 2), but we cannot create
|
||||||
|
the sheet itself.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
You need admin access to the Water Log settings page
|
||||||
|
(you're already here).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Step 1 */}
|
||||||
|
<section>
|
||||||
|
<h3 className="flex items-baseline gap-2 text-sm font-semibold text-[#1d1d1f]">
|
||||||
|
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#1a4d2e] text-xs font-bold text-white">
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
Generate an API token
|
||||||
|
</h3>
|
||||||
|
<ol className="mt-2 list-inside list-decimal space-y-1 text-sm text-[#5a5d5a]">
|
||||||
|
<li>
|
||||||
|
Go to{" "}
|
||||||
|
<a
|
||||||
|
href="https://app.smartsheet.com/b/home"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[#1a4d2e] underline underline-offset-2"
|
||||||
|
>
|
||||||
|
app.smartsheet.com
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click your avatar (top right) →{" "}
|
||||||
|
<span className="font-mono text-xs">Account</span> →{" "}
|
||||||
|
<span className="font-mono text-xs">Personal Settings</span>{" "}
|
||||||
|
→ <span className="font-mono text-xs">API Access</span>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click <strong>Generate new access token</strong>. Give it a
|
||||||
|
name like <span className="font-mono text-xs">RouteCommerce Water Log</span>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Copy the token <em>immediately</em> — Smartsheet only shows
|
||||||
|
it once.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Step 2 */}
|
||||||
|
<section>
|
||||||
|
<h3 className="flex items-baseline gap-2 text-sm font-semibold text-[#1d1d1f]">
|
||||||
|
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#1a4d2e] text-xs font-bold text-white">
|
||||||
|
2
|
||||||
|
</span>
|
||||||
|
Set up your sheet
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-[#5a5d5a]">
|
||||||
|
Your sheet needs at least the two <strong>required</strong>{" "}
|
||||||
|
columns below. The other five are optional but recommended —
|
||||||
|
you decide which of your sheet's existing columns
|
||||||
|
receives each field.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-3 overflow-hidden rounded-lg border border-[#d4d9d3]">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead className="bg-[#f4f1e8] text-[#5a5d5a]">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left font-semibold">
|
||||||
|
Our field
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left font-semibold">
|
||||||
|
Smartsheet column type
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left font-semibold">
|
||||||
|
Example
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-[#d4d9d3] bg-white">
|
||||||
|
{FIELD_SCHEMA.map((f) => (
|
||||||
|
<tr key={f.key}>
|
||||||
|
<td className="px-3 py-2 align-top">
|
||||||
|
<div className="font-semibold text-[#1d1d1f]">
|
||||||
|
{f.label}
|
||||||
|
{f.required && (
|
||||||
|
<span className="ml-1 text-[#a4452b]">*</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-[10px] text-[#8a8b88]">
|
||||||
|
{f.description}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 align-top font-mono text-[11px] text-[#5a5d5a]">
|
||||||
|
{f.type}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 align-top font-mono text-[11px] text-[#5a5d5a]">
|
||||||
|
{f.example}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-[#8a8b88]">
|
||||||
|
<span className="text-[#a4452b]">*</span> Required. The
|
||||||
|
Entry ID column is what we use for dedup — without it, a
|
||||||
|
re-sync would create duplicate rows.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details className="mt-3 rounded-lg border border-[#d4d9d3] bg-white">
|
||||||
|
<summary className="cursor-pointer px-4 py-2 text-sm font-medium text-[#1a4d2e] hover:bg-[#f4f1e8]">
|
||||||
|
Copy a starter header row
|
||||||
|
</summary>
|
||||||
|
<pre className="overflow-x-auto border-t border-[#d4d9d3] bg-[#fdfaf2] px-4 py-3 text-xs text-[#1d1d1f]">
|
||||||
|
<code>
|
||||||
|
Entry ID,Logged At,Headgate,Measurement,Unit,Irrigator,Notes
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<p className="border-t border-[#d4d9d3] px-4 py-2 text-xs text-[#5a5d5a]">
|
||||||
|
Paste this into row 1 of a new sheet, then click the
|
||||||
|
<span className="font-mono"> ▼ </span>
|
||||||
|
on the Entry ID cell and choose{" "}
|
||||||
|
<span className="font-mono">Column Type → Text/Number</span>.
|
||||||
|
Repeat for the others.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Step 3 */}
|
||||||
|
<section>
|
||||||
|
<h3 className="flex items-baseline gap-2 text-sm font-semibold text-[#1d1d1f]">
|
||||||
|
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#1a4d2e] text-xs font-bold text-white">
|
||||||
|
3
|
||||||
|
</span>
|
||||||
|
Connect
|
||||||
|
</h3>
|
||||||
|
<ol className="mt-2 list-inside list-decimal space-y-1 text-sm text-[#5a5d5a]">
|
||||||
|
<li>
|
||||||
|
Close this dialog and paste your <strong>sheet URL</strong>{" "}
|
||||||
|
or numeric sheet ID into the Sheet URL field. (File →
|
||||||
|
Properties in Smartsheet shows the numeric ID.)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Paste your API token into the API Access Token field.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click <strong>Test Connection</strong> — we'll load
|
||||||
|
your sheet's columns into the dropdowns.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Map each of our fields to one of your sheet's
|
||||||
|
columns. Unmapped fields are simply skipped.
|
||||||
|
</li>
|
||||||
|
<li>Click <strong>Save Settings</strong>.</li>
|
||||||
|
</ol>
|
||||||
|
<p className="mt-3 rounded-lg border border-[#1a4d2e]/30 bg-[#1a4d2e]/5 px-3 py-2 text-xs text-[#1a4d2e]">
|
||||||
|
<strong>Tip:</strong> the first sync runs immediately if you
|
||||||
|
set frequency to <em>Real-time</em>, or on the next cron
|
||||||
|
tick for 15-minute / hourly. Submit a test entry from{" "}
|
||||||
|
<code className="font-mono">/water</code> to see it land in
|
||||||
|
your sheet.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Troubleshooting */}
|
||||||
|
<section className="border-t border-[#d4d9d3] pt-4">
|
||||||
|
<h3 className="text-sm font-semibold text-[#1d1d1f]">
|
||||||
|
Common issues
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-2 space-y-2 text-xs text-[#5a5d5a]">
|
||||||
|
<li>
|
||||||
|
<strong>401 Unauthorized</strong> — the token is wrong or
|
||||||
|
expired. Re-generate it in Smartsheet.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>404 Sheet not found</strong> — if you pasted a
|
||||||
|
share URL with a slug (e.g. <code className="font-mono">/sheets/abc123</code>),
|
||||||
|
Smartsheet sometimes 404s on those. Use the numeric sheet
|
||||||
|
ID instead, found in{" "}
|
||||||
|
<span className="font-mono">File → Properties</span>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Test Connection succeeds but rows don't
|
||||||
|
appear</strong> — the column type probably rejects our
|
||||||
|
value (most common: a DATE column getting an ISO
|
||||||
|
timestamp). Check the Recent Activity panel for the exact
|
||||||
|
error.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="sticky bottom-0 flex justify-end border-t border-[#d4d9d3] bg-[#fdfaf2] px-6 py-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-lg bg-[#1a4d2e] px-5 py-2 text-sm font-semibold text-white transition hover:bg-[#143d24]"
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user