first version
This commit is contained in:
+68
-321
@@ -1,345 +1,92 @@
|
||||
import { useEffect } from "react";
|
||||
import type {
|
||||
ActionFunctionArgs,
|
||||
HeadersFunction,
|
||||
LoaderFunctionArgs,
|
||||
} from "react-router";
|
||||
import { useFetcher } from "react-router";
|
||||
import { useAppBridge } from "@shopify/app-bridge-react";
|
||||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { Link, useLoaderData } from "react-router";
|
||||
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { boundary } from "@shopify/shopify-app-react-router/server";
|
||||
import db from "../db.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
await authenticate.admin(request);
|
||||
const { session } = await authenticate.admin(request);
|
||||
|
||||
return null;
|
||||
};
|
||||
const [settings, recent] = await Promise.all([
|
||||
db.shopSettings.findUnique({ where: { shopDomain: session.shop } }),
|
||||
db.invoice.findMany({
|
||||
where: { shopDomain: session.shop },
|
||||
orderBy: [{ issuedAt: "desc" }],
|
||||
take: 10,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const color = ["Red", "Orange", "Yellow", "Green"][
|
||||
Math.floor(Math.random() * 4)
|
||||
];
|
||||
const response = await admin.graphql(
|
||||
`#graphql
|
||||
mutation populateProduct($product: ProductCreateInput!) {
|
||||
productCreate(product: $product) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
handle
|
||||
status
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
price
|
||||
barcode
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
demoInfo: metafield(namespace: "$app", key: "demo_info") {
|
||||
jsonValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
variables: {
|
||||
product: {
|
||||
title: `${color} Snowboard`,
|
||||
metafields: [
|
||||
{
|
||||
namespace: "$app",
|
||||
key: "demo_info",
|
||||
value: "Created by React Router Template",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
const settingsConfigured = !!(
|
||||
settings &&
|
||||
settings.companyName &&
|
||||
settings.addressLine1 &&
|
||||
settings.iban
|
||||
);
|
||||
const responseJson = await response.json();
|
||||
|
||||
const product = responseJson.data!.productCreate!.product!;
|
||||
const variantId = product.variants.edges[0]!.node!.id!;
|
||||
|
||||
const variantResponse = await admin.graphql(
|
||||
`#graphql
|
||||
mutation shopifyReactRouterTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
|
||||
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
|
||||
productVariants {
|
||||
id
|
||||
price
|
||||
barcode
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
variables: {
|
||||
productId: product.id,
|
||||
variants: [{ id: variantId, price: "100.00" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const variantResponseJson = await variantResponse.json();
|
||||
|
||||
const metaobjectResponse = await admin.graphql(
|
||||
`#graphql
|
||||
mutation shopifyReactRouterTemplateUpsertMetaobject($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) {
|
||||
metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
|
||||
metaobject {
|
||||
id
|
||||
handle
|
||||
title: field(key: "title") {
|
||||
jsonValue
|
||||
}
|
||||
description: field(key: "description") {
|
||||
jsonValue
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
variables: {
|
||||
handle: {
|
||||
type: "$app:example",
|
||||
handle: "demo-entry",
|
||||
},
|
||||
metaobject: {
|
||||
fields: [
|
||||
{ key: "title", value: "Demo Entry" },
|
||||
{
|
||||
key: "description",
|
||||
value:
|
||||
"This metaobject was created by the Shopify app template to demonstrate the metaobject API.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const metaobjectResponseJson = await metaobjectResponse.json();
|
||||
|
||||
return {
|
||||
product: responseJson!.data!.productCreate!.product,
|
||||
variant:
|
||||
variantResponseJson!.data!.productVariantsBulkUpdate!.productVariants,
|
||||
metaobject:
|
||||
metaobjectResponseJson!.data!.metaobjectUpsert!.metaobject,
|
||||
settingsConfigured,
|
||||
recent: recent.map((i) => ({
|
||||
id: i.id,
|
||||
number: i.invoiceNumber,
|
||||
kind: i.kind,
|
||||
orderName: i.orderName,
|
||||
version: i.version,
|
||||
sentAt: i.sentAt?.toISOString() ?? null,
|
||||
cancelledAt: i.cancelledAt?.toISOString() ?? null,
|
||||
issuedAt: i.issuedAt.toISOString(),
|
||||
pdfUrl: i.pdfUrl,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const fetcher = useFetcher<typeof action>();
|
||||
|
||||
const shopify = useAppBridge();
|
||||
const isLoading =
|
||||
["loading", "submitting"].includes(fetcher.state) &&
|
||||
fetcher.formMethod === "POST";
|
||||
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.product?.id) {
|
||||
shopify.toast.show("Product created");
|
||||
}
|
||||
}, [fetcher.data?.product?.id, shopify]);
|
||||
|
||||
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
||||
const { settingsConfigured, recent } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<s-page heading="Shopify app template">
|
||||
<s-button slot="primary-action" onClick={generateProduct}>
|
||||
Generate a product
|
||||
</s-button>
|
||||
<s-page heading="LinumIQ Invoice">
|
||||
{!settingsConfigured && (
|
||||
<s-banner tone="warning" heading="Configure your invoice settings">
|
||||
Complete your company, bank and numbering details so generated
|
||||
invoices are legally compliant.{" "}
|
||||
<Link to="/app/settings">Open settings</Link>
|
||||
</s-banner>
|
||||
)}
|
||||
|
||||
<s-section heading="Congrats on creating a new Shopify app 🎉">
|
||||
<s-section heading="What this app does">
|
||||
<s-paragraph>
|
||||
This embedded app template uses{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/apps/tools/app-bridge"
|
||||
target="_blank"
|
||||
>
|
||||
App Bridge
|
||||
</s-link>{" "}
|
||||
interface examples like an{" "}
|
||||
<s-link href="/app/additional">additional page in the app nav</s-link>
|
||||
, as well as an{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/api/admin-graphql"
|
||||
target="_blank"
|
||||
>
|
||||
Admin GraphQL
|
||||
</s-link>{" "}
|
||||
mutation demo, to provide a starting point for app development.
|
||||
Generates Austrian-compliant PDF invoices for your Shopify orders.
|
||||
Trigger from the order page (Generate invoice action), via Shopify
|
||||
Flow, or in bulk from the Invoices page. PDFs are stored on
|
||||
Shopify Files and linked to each order via metafields.
|
||||
</s-paragraph>
|
||||
</s-section>
|
||||
<s-section heading="Get started with products">
|
||||
<s-paragraph>
|
||||
Generate a product with GraphQL and get the JSON output for that
|
||||
product. Learn more about the{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/api/admin-graphql/latest/mutations/productCreate"
|
||||
target="_blank"
|
||||
>
|
||||
productCreate
|
||||
</s-link>{" "}
|
||||
mutation in our API references. Includes a product{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/apps/build/custom-data/metafields"
|
||||
target="_blank"
|
||||
>
|
||||
metafield
|
||||
</s-link>{" "}
|
||||
and{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/apps/build/custom-data/metaobjects"
|
||||
target="_blank"
|
||||
>
|
||||
metaobject
|
||||
</s-link>
|
||||
.
|
||||
</s-paragraph>
|
||||
<s-stack direction="inline" gap="base">
|
||||
<s-button
|
||||
onClick={generateProduct}
|
||||
{...(isLoading ? { loading: true } : {})}
|
||||
>
|
||||
Generate a product
|
||||
</s-button>
|
||||
{fetcher.data?.product && (
|
||||
<s-button
|
||||
onClick={() => {
|
||||
shopify.intents.invoke?.("edit:shopify/Product", {
|
||||
value: fetcher.data?.product?.id,
|
||||
});
|
||||
}}
|
||||
target="_blank"
|
||||
variant="tertiary"
|
||||
>
|
||||
Edit product
|
||||
</s-button>
|
||||
)}
|
||||
</s-stack>
|
||||
{fetcher.data?.product && (
|
||||
<s-section heading="productCreate mutation">
|
||||
<s-stack direction="block" gap="base">
|
||||
<s-box
|
||||
padding="base"
|
||||
borderWidth="base"
|
||||
borderRadius="base"
|
||||
background="subdued"
|
||||
>
|
||||
<pre style={{ margin: 0 }}>
|
||||
<code>{JSON.stringify(fetcher.data.product, null, 2)}</code>
|
||||
</pre>
|
||||
</s-box>
|
||||
|
||||
<s-heading>productVariantsBulkUpdate mutation</s-heading>
|
||||
<s-box
|
||||
padding="base"
|
||||
borderWidth="base"
|
||||
borderRadius="base"
|
||||
background="subdued"
|
||||
>
|
||||
<pre style={{ margin: 0 }}>
|
||||
<code>{JSON.stringify(fetcher.data.variant, null, 2)}</code>
|
||||
</pre>
|
||||
</s-box>
|
||||
|
||||
<s-heading>metaobjectUpsert mutation</s-heading>
|
||||
<s-box
|
||||
padding="base"
|
||||
borderWidth="base"
|
||||
borderRadius="base"
|
||||
background="subdued"
|
||||
>
|
||||
<pre style={{ margin: 0 }}>
|
||||
<code>
|
||||
{JSON.stringify(fetcher.data.metaobject, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
</s-box>
|
||||
</s-stack>
|
||||
</s-section>
|
||||
<s-section heading="Recent invoices">
|
||||
{recent.length === 0 ? (
|
||||
<s-paragraph>No invoices generated yet.</s-paragraph>
|
||||
) : (
|
||||
<s-unordered-list>
|
||||
{recent.map((i) => (
|
||||
<s-list-item key={i.id}>
|
||||
{i.kind === "storno" ? "Storno " : ""}
|
||||
{i.number} — order {i.orderName} (v{i.version})
|
||||
{i.cancelledAt
|
||||
? " — cancelled"
|
||||
: i.sentAt
|
||||
? " — sent"
|
||||
: ""}
|
||||
{i.pdfUrl ? (
|
||||
<>
|
||||
{" "}
|
||||
[<a href={i.pdfUrl} target="_blank" rel="noreferrer">PDF</a>]
|
||||
</>
|
||||
) : null}
|
||||
</s-list-item>
|
||||
))}
|
||||
</s-unordered-list>
|
||||
)}
|
||||
</s-section>
|
||||
|
||||
<s-section slot="aside" heading="App template specs">
|
||||
<s-paragraph>
|
||||
<s-text>Framework: </s-text>
|
||||
<s-link href="https://reactrouter.com/" target="_blank">
|
||||
React Router
|
||||
</s-link>
|
||||
</s-paragraph>
|
||||
<s-paragraph>
|
||||
<s-text>Interface: </s-text>
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/api/app-home/using-polaris-components"
|
||||
target="_blank"
|
||||
>
|
||||
Polaris web components
|
||||
</s-link>
|
||||
</s-paragraph>
|
||||
<s-paragraph>
|
||||
<s-text>API: </s-text>
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/api/admin-graphql"
|
||||
target="_blank"
|
||||
>
|
||||
GraphQL
|
||||
</s-link>
|
||||
</s-paragraph>
|
||||
<s-paragraph>
|
||||
<s-text>Custom data: </s-text>
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/apps/build/custom-data"
|
||||
target="_blank"
|
||||
>
|
||||
Metafields & metaobjects
|
||||
</s-link>
|
||||
</s-paragraph>
|
||||
<s-paragraph>
|
||||
<s-text>Database: </s-text>
|
||||
<s-link href="https://www.prisma.io/" target="_blank">
|
||||
Prisma
|
||||
</s-link>
|
||||
</s-paragraph>
|
||||
</s-section>
|
||||
|
||||
<s-section slot="aside" heading="Next steps">
|
||||
<s-unordered-list>
|
||||
<s-list-item>
|
||||
Build an{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/apps/getting-started/build-app-example"
|
||||
target="_blank"
|
||||
>
|
||||
example app
|
||||
</s-link>
|
||||
</s-list-item>
|
||||
<s-list-item>
|
||||
Explore Shopify's API with{" "}
|
||||
<s-link
|
||||
href="https://shopify.dev/docs/apps/tools/graphiql-admin-api"
|
||||
target="_blank"
|
||||
>
|
||||
GraphiQL
|
||||
</s-link>
|
||||
</s-list-item>
|
||||
</s-unordered-list>
|
||||
<Link to="/app/invoices">Open invoices page</Link>
|
||||
</s-section>
|
||||
</s-page>
|
||||
);
|
||||
}
|
||||
|
||||
export const headers: HeadersFunction = (headersArgs) => {
|
||||
return boundary.headers(headersArgs);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user