Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
app.locations.new.tsx's "Location name"/"Address" fields and app.blackouts._index.tsx's "Date"/"Reason" fields were missing value/onChange — Polaris TextField is fully controlled, so without that wiring every keystroke gets overwritten back to an empty string on re-render, making the field appear frozen. Every other form in the app already followed the value/onChange + useState pattern; these four fields were the only ones missed, and went uncaught until live browser testing was actually possible tonight (shopify.web.toml was missing until now). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import { useState } from "react";
|
|
import { redirect, type ActionFunctionArgs } from "@remix-run/node";
|
|
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
|
import { Page, Card, BlockStack, Banner, FormLayout, TextField, Button } from "@shopify/polaris";
|
|
import { TitleBar } from "@shopify/app-bridge-react";
|
|
import { authenticate } from "../shopify.server";
|
|
import db from "../db.server";
|
|
import { canAddLocation, getShopTier, locationLimitFor } from "../services/billing.server";
|
|
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
const { session } = await authenticate.admin(request);
|
|
const formData = await request.formData();
|
|
|
|
const name = String(formData.get("name") || "").trim();
|
|
const address = String(formData.get("address") || "").trim();
|
|
const timezone = String(formData.get("timezone") || "").trim();
|
|
|
|
const errors: Record<string, string> = {};
|
|
if (!name) errors.name = "Name is required";
|
|
if (!timezone) errors.timezone = "Timezone is required";
|
|
if (Object.keys(errors).length > 0) {
|
|
return { errors };
|
|
}
|
|
|
|
if (!(await canAddLocation(session.shop))) {
|
|
const tier = await getShopTier(session.shop);
|
|
return {
|
|
errors: {
|
|
plan: `Your ${tier} plan allows up to ${locationLimitFor(tier)} location(s). Upgrade to add more.`,
|
|
},
|
|
};
|
|
}
|
|
|
|
const location = await db.location.create({
|
|
data: { shopDomain: session.shop, name, address, timezone },
|
|
});
|
|
|
|
return redirect(`/app/locations/${location.id}`);
|
|
};
|
|
|
|
export default function NewLocation() {
|
|
const actionData = useActionData<typeof action>();
|
|
const navigation = useNavigation();
|
|
const [name, setName] = useState("");
|
|
const [address, setAddress] = useState("");
|
|
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
|
|
return (
|
|
<Page>
|
|
<TitleBar title="Add location" />
|
|
<Card>
|
|
<Form method="post">
|
|
<FormLayout>
|
|
{actionData?.errors?.plan && (
|
|
<Banner tone="warning" title="Location limit reached">
|
|
<p>
|
|
{actionData.errors.plan}{" "}
|
|
<a href="/app/billing">View plans</a>
|
|
</p>
|
|
</Banner>
|
|
)}
|
|
<TextField
|
|
label="Location name"
|
|
name="name"
|
|
autoComplete="off"
|
|
value={name}
|
|
onChange={setName}
|
|
error={actionData?.errors?.name}
|
|
requiredIndicator
|
|
/>
|
|
<TextField
|
|
label="Address"
|
|
name="address"
|
|
autoComplete="off"
|
|
multiline={2}
|
|
value={address}
|
|
onChange={setAddress}
|
|
/>
|
|
<TextField
|
|
label="Timezone (IANA, e.g. America/Toronto)"
|
|
name="timezone"
|
|
autoComplete="off"
|
|
value={timezone}
|
|
onChange={setTimezone}
|
|
error={actionData?.errors?.timezone}
|
|
requiredIndicator
|
|
helpText="All slot math for this location is computed in this timezone."
|
|
/>
|
|
<BlockStack>
|
|
<Button submit variant="primary" loading={navigation.state === "submitting"}>
|
|
Create location
|
|
</Button>
|
|
</BlockStack>
|
|
</FormLayout>
|
|
</Form>
|
|
</Card>
|
|
</Page>
|
|
);
|
|
}
|