diff --git a/admin-action/package.json.liquid b/admin-action/package.json.liquid index 92df9cd3..6d3ea4ea 100644 --- a/admin-action/package.json.liquid +++ b/admin-action/package.json.liquid @@ -11,8 +11,8 @@ {%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", - "@shopify/ui-extensions-react": "2025.4.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { @@ -20,7 +20,7 @@ } {%- else %} "dependencies": { - "@shopify/ui-extensions": "2025.4.x" + "@shopify/ui-extensions": "2025.7.x" } {%- endif %} } \ No newline at end of file diff --git a/admin-action/shopify.extension.toml.liquid b/admin-action/shopify.extension.toml.liquid index 2d62900e..910ff787 100644 --- a/admin-action/shopify.extension.toml.liquid +++ b/admin-action/shopify.extension.toml.liquid @@ -1,7 +1,7 @@ {%- if flavor contains "preact" -%} api_version = "2025-10" {% else %} -api_version = "2025-04" +api_version = "2025-07" {% endif -%} [[extensions]] diff --git a/admin-block/package.json.liquid b/admin-block/package.json.liquid index 92df9cd3..6d3ea4ea 100644 --- a/admin-block/package.json.liquid +++ b/admin-block/package.json.liquid @@ -11,8 +11,8 @@ {%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", - "@shopify/ui-extensions-react": "2025.4.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { @@ -20,7 +20,7 @@ } {%- else %} "dependencies": { - "@shopify/ui-extensions": "2025.4.x" + "@shopify/ui-extensions": "2025.7.x" } {%- endif %} } \ No newline at end of file diff --git a/admin-block/shopify.extension.toml.liquid b/admin-block/shopify.extension.toml.liquid index 99008e07..52269ef8 100644 --- a/admin-block/shopify.extension.toml.liquid +++ b/admin-block/shopify.extension.toml.liquid @@ -1,7 +1,7 @@ {%- if flavor contains "preact" -%} api_version = "2025-10" {% else %} -api_version = "2025-04" +api_version = "2025-07" {% endif -%} [[extensions]] diff --git a/admin-print-action/package.json.liquid b/admin-print-action/package.json.liquid index 92df9cd3..6d3ea4ea 100644 --- a/admin-print-action/package.json.liquid +++ b/admin-print-action/package.json.liquid @@ -11,8 +11,8 @@ {%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", - "@shopify/ui-extensions-react": "2025.4.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { @@ -20,7 +20,7 @@ } {%- else %} "dependencies": { - "@shopify/ui-extensions": "2025.4.x" + "@shopify/ui-extensions": "2025.7.x" } {%- endif %} } \ No newline at end of file diff --git a/admin-print-action/shopify.extension.toml.liquid b/admin-print-action/shopify.extension.toml.liquid index 98e23cbd..24781021 100644 --- a/admin-print-action/shopify.extension.toml.liquid +++ b/admin-print-action/shopify.extension.toml.liquid @@ -1,7 +1,7 @@ {%- if flavor contains "preact" -%} api_version = "2025-10" {% else %} -api_version = "2025-04" +api_version = "2025-07" {% endif -%} [[extensions]] diff --git a/admin-purchase-options-action/package.json.liquid b/admin-purchase-options-action/package.json.liquid index 92df9cd3..6d3ea4ea 100644 --- a/admin-purchase-options-action/package.json.liquid +++ b/admin-purchase-options-action/package.json.liquid @@ -11,8 +11,8 @@ {%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", - "@shopify/ui-extensions-react": "2025.4.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { @@ -20,7 +20,7 @@ } {%- else %} "dependencies": { - "@shopify/ui-extensions": "2025.4.x" + "@shopify/ui-extensions": "2025.7.x" } {%- endif %} } \ No newline at end of file diff --git a/admin-purchase-options-action/shopify.extension.toml.liquid b/admin-purchase-options-action/shopify.extension.toml.liquid index 1295ab2e..ee780c57 100644 --- a/admin-purchase-options-action/shopify.extension.toml.liquid +++ b/admin-purchase-options-action/shopify.extension.toml.liquid @@ -1,7 +1,7 @@ {%- if flavor contains "preact" -%} api_version = "2025-10" {% else %} -api_version = "2025-04" +api_version = "2025-07" {% endif %} [[extensions]] diff --git a/customer-segment-template-extension/package.json.liquid b/customer-segment-template-extension/package.json.liquid index a088f86e..6d3ea4ea 100644 --- a/customer-segment-template-extension/package.json.liquid +++ b/customer-segment-template-extension/package.json.liquid @@ -1,27 +1,26 @@ -{%- if flavor contains "react" -%} { "name": "{{ handle }}", "private": true, "version": "1.0.0", "license": "UNLICENSED", +{%- if flavor contains "preact" %} + "dependencies": { + "preact": "^10.10.x", + "@shopify/ui-extensions": "~2025.10.0-rc" + } +{%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2024.7.x", - "@shopify/ui-extensions-react": "2024.7.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { "@types/react": "^18.0.0" } -} -{%- else -%} -{ - "name": "{{ handle }}", - "private": true, - "version": "1.0.0", - "license": "UNLICENSED", +{%- else %} "dependencies": { - "@shopify/ui-extensions": "2024.7.x" + "@shopify/ui-extensions": "2025.7.x" } -} -{%- endif -%} +{%- endif %} +} \ No newline at end of file diff --git a/customer-segment-template-extension/shopify.extension.toml.liquid b/customer-segment-template-extension/shopify.extension.toml.liquid index 299954a9..c0f44805 100644 --- a/customer-segment-template-extension/shopify.extension.toml.liquid +++ b/customer-segment-template-extension/shopify.extension.toml.liquid @@ -1,4 +1,8 @@ -api_version = "2024-07" +{%- if flavor contains "preact" -%} +api_version = "2025-10" +{% else %} +api_version = "2025-07" +{% endif -%} [[extensions]] # Change the merchant-facing name of the extension in locales/en.default.json @@ -15,4 +19,4 @@ target = "admin.customers.segmentation-templates.render" # Valid extension points: -# - admin.customers.segmentation-templates.render +# - admin.customers.segmentation-templates.render \ No newline at end of file diff --git a/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid b/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid index c9239662..399855a0 100644 --- a/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid +++ b/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid @@ -1,4 +1,28 @@ -{%- if flavor contains "react" -%} +{%- if flavor contains "preact" -%} +import {render} from 'preact'; + +const TARGET = 'admin.customers.segmentation-templates.render'; + +export default function() { + render(, document.body); +} + +function App() { + const query = 'number_of_orders = 1 AND products_purchased(id: (product_id)) = true'; + const queryToInsert = 'number_of_orders = 1 AND products_purchased(id: ('; + + return ( + + ); +} + +{%- elsif flavor contains "react" -%} import { reactExtension, CustomerSegmentTemplate, @@ -49,4 +73,4 @@ export default extension("admin.customers.segmentation-templates.render", (root, ) ); }); -{%- endif -%} +{%- endif -%} \ No newline at end of file diff --git a/discount-details-function-settings-block/package.json.liquid b/discount-details-function-settings-block/package.json.liquid index 8beecd7f..6d3ea4ea 100644 --- a/discount-details-function-settings-block/package.json.liquid +++ b/discount-details-function-settings-block/package.json.liquid @@ -1,28 +1,26 @@ -{%- if flavor contains "react" -%} { "name": "{{ handle }}", "private": true, "version": "1.0.0", "license": "UNLICENSED", +{%- if flavor contains "preact" %} + "dependencies": { + "preact": "^10.10.x", + "@shopify/ui-extensions": "~2025.10.0-rc" + } +{%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2024.10.x", - "@shopify/ui-extensions-react": "2024.10.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { "@types/react": "^18.0.0" } -} -{%- else -%} -{ - "name": "{{ handle }}", - "private": true, - "version": "1.0.0", - "license": "UNLICENSED", +{%- else %} "dependencies": { - "@shopify/ui-extensions": "2024.10.x" + "@shopify/ui-extensions": "2025.7.x" } -} -{%- endif -%} - +{%- endif %} +} \ No newline at end of file diff --git a/discount-details-function-settings-block/shopify.extension.toml.liquid b/discount-details-function-settings-block/shopify.extension.toml.liquid index c1badf6e..a1ae82b7 100644 --- a/discount-details-function-settings-block/shopify.extension.toml.liquid +++ b/discount-details-function-settings-block/shopify.extension.toml.liquid @@ -1,4 +1,8 @@ -api_version = "2024-10" +{%- if flavor contains "preact" -%} +api_version = "2025-10" +{% else %} +api_version = "2025-07" +{% endif -%} [[extensions]] # Change the merchant-facing name of the extension in locales/en.default.json @@ -12,4 +16,4 @@ type = "ui_extension" [[extensions.targeting]] module = "./src/DiscountFunctionSettings.{{ srcFileExtension }}" # The target used here must match the target used in the module file (./src/DiscountFunctionSettings.{{ srcFileExtension }}) -target = "admin.discount-details.function-settings.render" +target = "admin.discount-details.function-settings.render" \ No newline at end of file diff --git a/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid b/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid index 01efc3a1..86c9ae65 100644 --- a/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid +++ b/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid @@ -1,4 +1,262 @@ -{% if flavor contains 'react' %} +{%- if flavor contains "preact" -%} +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +const TARGET = "admin.discount-details.function-settings.render"; + +export default function() { + render(, document.body); +} + +function App() { + const [loading, setLoading] = useState(true); + const [percentages, setPercentages] = useState({ product: 0, order: 0, shipping: 0 }); + const [collections, setCollections] = useState([]); + const [appliesTo, setAppliesTo] = useState("all"); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + const initialize = async () => { + const existingDefinition = await getMetafieldDefinition(); + if (!existingDefinition) { + await createMetafieldDefinition(); + } + + const metafieldConfig = parseMetafield( + shopify.data?.metafields?.find(m => m.key === "function-configuration")?.value + ); + + setPercentages(metafieldConfig.percentages); + + if (metafieldConfig.collectionIds.length > 0) { + const fetchedCollections = await getCollections(metafieldConfig.collectionIds); + setCollections(fetchedCollections); + setAppliesTo("collections"); + } + + setLoading(false); + setInitialized(true); + }; + + initialize(); + }, []); + + const onPercentageChange = (type, value) => { + setPercentages(prev => ({ + ...prev, + [type]: Number(value) + })); + }; + + const onAppliesToChange = (value) => { + setAppliesTo(value); + if (value === "all") { + setCollections([]); + } + }; + + const onAddCollections = async () => { + const selection = await shopify.resourcePicker({ + type: "collection", + selectionIds: collections.map(c => ({ id: c.id })), + action: "select", + filter: { + archived: true, + variants: true, + }, + }); + if (selection) { + setCollections(selection); + } + }; + + const removeCollection = (id) => { + setCollections(prev => prev.filter(c => c.id !== id)); + }; + + const saveSettings = async () => { + await shopify.applyMetafieldChange({ + type: "updateMetafield", + namespace: "$app:example-discounts--ui-extension", + key: "function-configuration", + value: JSON.stringify({ + cartLinePercentage: percentages.product, + orderPercentage: percentages.order, + deliveryPercentage: percentages.shipping, + collectionIds: collections.map(c => c.id), + }), + valueType: "json", + }); + }; + + if (loading) { + return {shopify.i18n.translate("loading") || "Loading..."}; + } + + return ( + + {shopify.i18n.translate("title") || "Discount Configuration"} + + + + onPercentageChange("product", value)} + suffix="%" + /> + + + + + {appliesTo === "collections" && ( + + + {shopify.i18n.translate("collections.buttonLabel") || "Add collections"} + + + )} + + + {appliesTo === "collections" && collections.length > 0 && ( + + {collections.map((collection) => ( + + + + {collection.title} + + removeCollection(collection.id)}> + ✕ + + + + + ))} + + )} + + + {(appliesTo === "all" || collections.length === 0) && } + + onPercentageChange("order", value)} + suffix="%" + /> + + onPercentageChange("shipping", value)} + suffix="%" + /> + + + + + ); +} + +const METAFIELD_NAMESPACE = "$app:example-discounts--ui-extension"; +const METAFIELD_KEY = "function-configuration"; + +async function getMetafieldDefinition() { + const query = `#graphql + query GetMetafieldDefinition { + metafieldDefinitions(first: 1, ownerType: DISCOUNT, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") { + nodes { + id + } + } + } + `; + + const result = await shopify.query(query); + return result?.data?.metafieldDefinitions?.nodes[0]; +} + +async function createMetafieldDefinition() { + const definition = { + access: { + admin: "MERCHANT_READ_WRITE", + }, + key: METAFIELD_KEY, + name: "Discount Configuration", + namespace: METAFIELD_NAMESPACE, + ownerType: "DISCOUNT", + type: "json", + }; + + const query = `#graphql + mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) { + metafieldDefinitionCreate(definition: $definition) { + createdDefinition { + id + } + } + } + `; + + const variables = { definition }; + await shopify.query(query, { variables }); +} + +function parseMetafield(value) { + try { + const parsed = JSON.parse(value || "{}"); + return { + percentages: { + product: Number(parsed.cartLinePercentage ?? 0), + order: Number(parsed.orderPercentage ?? 0), + shipping: Number(parsed.deliveryPercentage ?? 0), + }, + collectionIds: parsed.collectionIds ?? [], + }; + } catch { + return { + percentages: { product: 0, order: 0, shipping: 0 }, + collectionIds: [], + }; + } +} + +async function getCollections(collectionGids) { + const query = `#graphql + query GetCollections($ids: [ID!]!) { + collections: nodes(ids: $ids) { + ... on Collection { + id + title + } + } + } + `; + const result = await shopify.query(query, { + variables: { ids: collectionGids }, + }); + return result?.data?.collections ?? []; +} + +{%- elsif flavor contains "react" -%} import { reactExtension, useApi, @@ -384,7 +642,7 @@ } -{% else %} +{%- else -%} import { FunctionSettings, Text, @@ -783,5 +1041,4 @@ export default extension(TARGET, async (root, api) => { await api.query(query, { variables }); } }); -{% endif %} - +{%- endif -%} \ No newline at end of file diff --git a/order-routing-location-rule/package.json.liquid b/order-routing-location-rule/package.json.liquid index 92df9cd3..6d3ea4ea 100644 --- a/order-routing-location-rule/package.json.liquid +++ b/order-routing-location-rule/package.json.liquid @@ -11,8 +11,8 @@ {%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", - "@shopify/ui-extensions-react": "2025.4.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { @@ -20,7 +20,7 @@ } {%- else %} "dependencies": { - "@shopify/ui-extensions": "2025.4.x" + "@shopify/ui-extensions": "2025.7.x" } {%- endif %} } \ No newline at end of file diff --git a/order-routing-location-rule/shopify.extension.toml.liquid b/order-routing-location-rule/shopify.extension.toml.liquid index 4aaff920..d7b6c3ad 100644 --- a/order-routing-location-rule/shopify.extension.toml.liquid +++ b/order-routing-location-rule/shopify.extension.toml.liquid @@ -1,7 +1,7 @@ {%- if flavor contains "preact" -%} api_version = "2025-10" {% else %} -api_version = "2025-04" +api_version = "2025-07" {% endif -%} [[extensions]] diff --git a/product-configuration-extension/package.json.liquid b/product-configuration-extension/package.json.liquid index 1baabd21..c9038039 100644 --- a/product-configuration-extension/package.json.liquid +++ b/product-configuration-extension/package.json.liquid @@ -11,8 +11,8 @@ {%- elsif flavor contains "react" -%} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", - "@shopify/ui-extensions-react": "2025.4.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { @@ -20,7 +20,7 @@ } {%- else -%} "dependencies": { - "@shopify/ui-extensions": "2025.4.x" + "@shopify/ui-extensions": "2025.7.x" } {%- endif -%} } \ No newline at end of file diff --git a/product-configuration-extension/shopify.extension.toml.liquid b/product-configuration-extension/shopify.extension.toml.liquid index e38886f5..de5e991f 100644 --- a/product-configuration-extension/shopify.extension.toml.liquid +++ b/product-configuration-extension/shopify.extension.toml.liquid @@ -1,7 +1,7 @@ {%- if flavor contains "preact" -%} api_version = "2025-10" {% else %} -api_version = "2025-04" +api_version = "2025-07" {% endif -%} [[extensions]] diff --git a/validation-settings/package.json.liquid b/validation-settings/package.json.liquid index c3ebba68..6d3ea4ea 100644 --- a/validation-settings/package.json.liquid +++ b/validation-settings/package.json.liquid @@ -1,27 +1,26 @@ -{%- if flavor contains "react" -%} { "name": "{{ handle }}", "private": true, "version": "1.0.0", "license": "UNLICENSED", +{%- if flavor contains "preact" %} + "dependencies": { + "preact": "^10.10.x", + "@shopify/ui-extensions": "~2025.10.0-rc" + } +{%- elsif flavor contains "react" %} "dependencies": { "react": "^18.0.0", - "@shopify/ui-extensions": "2024.10.x", - "@shopify/ui-extensions-react": "2024.10.x", + "@shopify/ui-extensions": "2025.7.x", + "@shopify/ui-extensions-react": "2025.7.x", "react-reconciler": "0.29.0" }, "devDependencies": { "@types/react": "^18.0.0" } -} -{%- else -%} -{ - "name": "{{ handle }}", - "private": true, - "version": "1.0.0", - "license": "UNLICENSED", +{%- else %} "dependencies": { - "@shopify/ui-extensions": "2024.10.x" + "@shopify/ui-extensions": "2025.7.x" } -} -{%- endif -%} +{%- endif %} +} \ No newline at end of file diff --git a/validation-settings/shopify.extension.toml.liquid b/validation-settings/shopify.extension.toml.liquid index 4f87e28e..c2b71909 100644 --- a/validation-settings/shopify.extension.toml.liquid +++ b/validation-settings/shopify.extension.toml.liquid @@ -1,4 +1,8 @@ -api_version = "2024-10" +{%- if flavor contains "preact" -%} +api_version = "2025-10" +{% else %} +api_version = "2025-07" +{% endif -%} [[extensions]] # change the merchant-facing name of the extension in locales/en.default.json @@ -14,6 +18,4 @@ type = "ui_extension" [[extensions.targeting]] module = "./src/ValidationSettings.{{ srcFileExtension }}" -target = "admin.settings.validation.render" - - +target = "admin.settings.validation.render" \ No newline at end of file diff --git a/validation-settings/src/ValidationSettings.liquid b/validation-settings/src/ValidationSettings.liquid index e3067a53..5e9dacae 100644 --- a/validation-settings/src/ValidationSettings.liquid +++ b/validation-settings/src/ValidationSettings.liquid @@ -1,56 +1,44 @@ -{%- if flavor contains 'react' -%} -import React, { useState } from "react"; -import { - reactExtension, - useApi, - Text, - Box, - FunctionSettings, - Section, - NumberField, - BlockStack, - Banner, - InlineStack, - Image, -} from "@shopify/ui-extensions-react/admin"; +{%- if flavor contains "preact" -%} +import {render} from 'preact'; +import {useEffect, useState} from 'preact/hooks'; const TARGET = "admin.settings.validation.render"; -export default reactExtension(TARGET, async (api) => { - const existingDefinition = await getMetafieldDefinition(api.query); - if (!existingDefinition) { - // Create a metafield definition for persistence if no pre-existing definition exists - const metafieldDefinition = await createMetafieldDefinition(api.query); - - if (!metafieldDefinition) { - throw new Error("Failed to create metafield definition"); - } - } - - // Read existing persisted data about product limits from the associated metafield - const configuration = JSON.parse( - api.data.validation?.metafields?.[0]?.value ?? "{}", - ); +export default function() { + render(, document.body); +} - // Query product data needed to render the settings UI - const products = await getProducts(api.query); +function ValidationSettings() { + const [errors, setErrors] = useState([]); + const [settings, setSettings] = useState({}); + const [products, setProducts] = useState([]); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + const initializeSettings = async () => { + const existingDefinition = await getMetafieldDefinition(); + if (!existingDefinition) { + const metafieldDefinition = await createMetafieldDefinition(); + if (!metafieldDefinition) { + throw new Error("Failed to create metafield definition"); + } + } - return ( - - ); -}); + const configuration = JSON.parse( + shopify.data.validation?.metafields?.[0]?.value ?? "{}", + ); -function ValidationSettings({ configuration, products }) { - const [errors, setErrors] = useState([]); - // State to keep track of product limit settings, initialized to any persisted metafield value - const [settings, setSettings] = useState( - createSettings(products, configuration), - ); + const fetchedProducts = await getProducts(); + setProducts(fetchedProducts); + setSettings(createSettings(fetchedProducts, configuration)); + setInitialized(true); + }; - const { applyMetafieldChange } = useApi(TARGET); + initializeSettings(); + }, []); const onError = (error) => { - setErrors(errors.map((e) => e.message)); + setErrors([error.message]); }; const onChange = async (variant, value) => { @@ -61,9 +49,7 @@ function ValidationSettings({ configuration, products }) { }; setSettings(newSettings); - // On input change, commit updated product variant limits to memory. - // Caution: the changes are only persisted on save! - const result = await applyMetafieldChange({ + const result = await shopify.applyMetafieldChange({ type: "updateMetafield", namespace: "$app:product-limits", key: "product-limits-values", @@ -75,68 +61,70 @@ function ValidationSettings({ configuration, products }) { } }; + if (!initialized) { + return Loading...; + } + return ( - // Note: FunctionSettings must be rendered for the host to receive metafield updates - + - + ); } function ProductQuantitySettings({ products, settings, onChange }) { function Header() { return ( - - - - Variant Name - - - Limit - - + + + + Variant Name + + + Limit + + ); } - // Render table of product variants and inputs to assign limits return products.map(({ title, variants }) => ( -
- + +
{variants.map((variant) => { const limit = settings[variant.id]; return ( - - + + {variant.imageUrl ? ( - {variant.title} + ) : ( - No image + No image )} - - - {variant.title} - - - + + {variant.title} + + + onChange(variant, value)} - > - - + /> + + ); })} - -
+ + )); } @@ -144,17 +132,17 @@ function ErrorBanner({ errors }) { if (errors.length === 0) return null; return ( - + {errors.map((error, i) => ( - + {error} - + ))} - + ); } -async function getProducts(adminApiQuery) { +async function getProducts() { const query = `#graphql query FetchProducts { products(first: 5) { @@ -173,7 +161,7 @@ async function getProducts(adminApiQuery) { } }`; - const result = await adminApiQuery(query); + const result = await shopify.query(query); return result?.data?.products.nodes.map(({ title, variants }) => { return { @@ -190,7 +178,7 @@ async function getProducts(adminApiQuery) { const METAFIELD_NAMESPACE = "$app:product-limits"; const METAFIELD_KEY = "product-limits-values"; -async function getMetafieldDefinition(adminApiQuery) { +async function getMetafieldDefinition() { const query = `#graphql query GetMetafieldDefinition { metafieldDefinitions(first: 1, ownerType: VALIDATION, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") { @@ -201,12 +189,12 @@ async function getMetafieldDefinition(adminApiQuery) { } `; - const result = await adminApiQuery(query); + const result = await shopify.query(query); return result?.data?.metafieldDefinitions?.nodes[0]; } -async function createMetafieldDefinition(adminApiQuery) { +async function createMetafieldDefinition() { const definition = { access: { admin: "MERCHANT_READ_WRITE", @@ -229,7 +217,7 @@ async function createMetafieldDefinition(adminApiQuery) { `; const variables = { definition }; - const result = await adminApiQuery(query, { variables }); + const result = await shopify.query(query, { variables }); return result?.data?.metafieldDefinitionCreate?.createdDefinition; } @@ -239,7 +227,6 @@ function createSettings(products, configuration) { products.forEach(({ variants }) => { variants.forEach(({ id }) => { - // Read existing product limits from metafield const limit = configuration[id]; if (limit) { @@ -251,7 +238,7 @@ function createSettings(products, configuration) { return settings; } -{%- elsif flavor contains 'typescript-react' -%} +{%- elsif flavor contains "react" -%} import React, { useState } from "react"; import { reactExtension, @@ -265,59 +252,44 @@ import { Banner, InlineStack, Image, - type FunctionSettingsError, } from "@shopify/ui-extensions-react/admin"; -import { type ValidationSettingsApi } from "@shopify/ui-extensions/admin"; const TARGET = "admin.settings.validation.render"; -export default reactExtension( - TARGET, - async (api: ValidationSettingsApi) => { - const existingDefinition = await getMetafieldDefinition(api.query); - if (!existingDefinition) { - // Create a metafield definition for persistence if no pre-existing definition exists - const metafieldDefinition = await createMetafieldDefinition(api.query); +export default reactExtension(TARGET, async (api) => { + const existingDefinition = await getMetafieldDefinition(api.query); + if (!existingDefinition) { + const metafieldDefinition = await createMetafieldDefinition(api.query); - if (!metafieldDefinition) { - throw new Error("Failed to create metafield definition"); - } + if (!metafieldDefinition) { + throw new Error("Failed to create metafield definition"); } + } - // Read existing persisted data about product limits from the associated metafield - const configuration = JSON.parse( - api.data.validation?.metafields?.[0]?.value ?? "{}", - ); + const configuration = JSON.parse( + api.data.validation?.metafields?.[0]?.value ?? "{}", + ); - // Query product data needed to render the settings UI - const products = await getProducts(api.query); + const products = await getProducts(api.query); - return ( - - ); - }, -); - -function ValidationSettings({ - configuration, - products, -}: { - configuration: Object; - products: Product[]; -}) { - const [errors, setErrors] = useState([]); - // State to keep track of product limit settings, initialized to any persisted metafield value - const [settings, setSettings] = useState>( + return ( + + ); +}); + +function ValidationSettings({ configuration, products }) { + const [errors, setErrors] = useState([]); + const [settings, setSettings] = useState( createSettings(products, configuration), ); const { applyMetafieldChange } = useApi(TARGET); - const onError = (errors: FunctionSettingsError[]) => { + const onError = (error) => { setErrors(errors.map((e) => e.message)); }; - const onChange = async (variant: ProductVariant, value: number) => { + const onChange = async (variant, value) => { setErrors([]); const newSettings = { ...settings, @@ -325,8 +297,6 @@ function ValidationSettings({ }; setSettings(newSettings); - // On input change, commit updated product variant limits to memory. - // Caution: the changes are only persisted on save! const result = await applyMetafieldChange({ type: "updateMetafield", namespace: "$app:product-limits", @@ -340,7 +310,6 @@ function ValidationSettings({ }; return ( - // Note: FunctionSettings must be rendered for the host to receive metafield updates ; - onChange: (variant: ProductVariant, value: number) => Promise; -}) { +function ProductQuantitySettings({ products, settings, onChange }) { function Header() { return ( @@ -375,7 +336,6 @@ function ProductQuantitySettings({ ); } - // Render table of product variants and inputs to assign limits return products.map(({ title, variants }) => (
@@ -402,7 +362,7 @@ function ProductQuantitySettings({ label="Set a limit" defaultValue={String(limit)} onChange={(value) => onChange(variant, value)} - > + /> ); @@ -412,7 +372,7 @@ function ProductQuantitySettings({ )); } -function ErrorBanner({ errors }: { errors: string[] }) { +function ErrorBanner({ errors }) { if (errors.length === 0) return null; return ( @@ -426,20 +386,7 @@ function ErrorBanner({ errors }: { errors: string[] }) { ); } -type Product = { - title: string; - variants: ProductVariant[]; -}; - -type ProductVariant = { - id: string; - title: string; - imageUrl?: string; -}; - -async function getProducts( - adminApiQuery: ValidationSettingsApi["query"], -): Promise { +async function getProducts(adminApiQuery) { const query = `#graphql query FetchProducts { products(first: 5) { @@ -458,392 +405,24 @@ async function getProducts( } }`; - type ProductQueryData = { - products: { - nodes: Array<{ - title: string; - variants: { - nodes: Array<{ - id: string; - title: string; - image?: { - url: string; - }; - }>; - }; - }>; - }; - }; - - const results = await adminApiQuery(query); - - return ( - results?.data?.products.nodes.map(({ title, variants }) => { - return { - title, - variants: variants.nodes.map((variant) => ({ - title: variant.title, - id: variant.id, - imageUrl: variant?.image?.url, - })), - }; - }) ?? [] - ); -} - -const METAFIELD_NAMESPACE = "$app:product-limits"; -const METAFIELD_KEY = "product-limits-values"; - -async function getMetafieldDefinition( - adminApiQuery: ValidationSettingsApi["query"], -) { - const query = `#graphql - query GetMetafieldDefinition { - metafieldDefinitions(first: 1, ownerType: VALIDATION, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") { - nodes { - id - } - } - } - `; - - type MetafieldDefinitionsQueryData = { - metafieldDefinitions: { - nodes: Array<{ - id: string; - }>; - }; - }; - - const result = await adminApiQuery(query); - - return result?.data?.metafieldDefinitions?.nodes[0]; -} - -async function createMetafieldDefinition( - adminApiQuery: ValidationSettingsApi["query"], -) { - const definition = { - access: { - admin: "MERCHANT_READ_WRITE", - }, - key: METAFIELD_KEY, - name: "Validation Configuration", - namespace: METAFIELD_NAMESPACE, - ownerType: "VALIDATION", - type: "json", - }; - - const query = `#graphql - mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) { - metafieldDefinitionCreate(definition: $definition) { - createdDefinition { - id - } - } - } - `; + const result = await adminApiQuery(query); - type MetafieldDefinitionCreateData = { - metafieldDefinitionCreate: { - createdDefinition?: { - id: string; - }; + return result?.data?.products.nodes.map(({ title, variants }) => { + return { + title, + variants: variants.nodes.map((variant) => ({ + title: variant.title, + id: variant.id, + imageUrl: variant?.image?.url, + })), }; - }; - - const variables = { definition }; - const result = await adminApiQuery(query, { - variables, }); - - return result?.data?.metafieldDefinitionCreate?.createdDefinition; -} - -function createSettings( - products: Product[], - configuration: Object, -): Record { - const settings = {}; - - products.forEach(({ variants }) => { - variants.forEach(({ id }) => { - // Read existing product limits from metafield - const limit = configuration[id]; - - if (limit) { - settings[id] = limit; - } - }); - }); - - return settings; -} - -{%- elsif flavor contains 'typescript' -%} -import { type RemoteRoot } from "@remote-ui/core"; -import { - extend, - Text, - Box, - FunctionSettings, - Section, - NumberField, - BlockStack, - Banner, - InlineStack, - Image, - type ValidationSettingsApi, - type FunctionSettingsError, -} from "@shopify/ui-extensions/admin"; - -const TARGET = "admin.settings.validation.render"; - -export default extend( - TARGET, - async (root: RemoteRoot, api: ValidationSettingsApi) => { - const existingDefinition = await getMetafieldDefinition(api.query); - if (!existingDefinition) { - // Create a metafield definition for persistence if no pre-existing definition exists - const metafieldDefinition = await createMetafieldDefinition(api.query); - - if (!metafieldDefinition) { - throw new Error("Failed to create metafield definition"); - } - } - - // Read existing persisted data about product limits from the associated metafield - const configuration = JSON.parse( - api.data.validation?.metafields?.[0]?.value ?? "{}", - ); - - // Query product data needed to render the settings UI - const products = await getProducts(api.query); - - renderValidationSettings(root, configuration, products, api); - }, -); - -function renderValidationSettings( - root: RemoteRoot, - configuration: Object, - products: Product[], - api: ValidationSettingsApi, -) { - let errors: string[] = []; - // State to keep track of product limit settings, initialized to any persisted metafield value - let settings = createSettings(products, configuration); - - const onError = (newErrors: FunctionSettingsError[]) => { - errors = newErrors.map((e) => e.message); - renderContent(); - }; - - const onChange = async (variant: ProductVariant, value: number) => { - errors = []; - const newSettings = { - ...settings, - [variant.id]: Number(value), - }; - settings = newSettings; - - // On input change, commit updated product variant limits to memory. - // Caution: the changes are only persisted on save! - const result = await api.applyMetafieldChange({ - type: "updateMetafield", - namespace: "$app:product-limits", - key: "product-limits-values", - value: JSON.stringify(newSettings), - }); - - if (result.type === "error") { - errors = [result.message]; - renderContent(); - } - }; - - const renderErrors = (errors: string[], root: RemoteRoot) => { - if (!errors.length) { - return []; - } - - return errors.map((error, i) => - root.createComponent( - Banner, - { - title: "Errors were encountered", - tone: "critical", - }, - root.createComponent(Text, {}, error), - ), - ); - }; - - const renderContent = () => { - return root.append( - root.createComponent( - // Note: FunctionSettings must be rendered for the host to receive metafield updates - FunctionSettings, - { onError }, - ...renderErrors(errors, root), - ...products.map((product) => - renderProductQuantitySettings(root, product, settings, onChange), - ), - ), - ); - }; - - renderContent(); -} - -function renderProductQuantitySettings( - root: RemoteRoot, - product: Product, - settings: Record, - onChange: (variant: ProductVariant, value: number) => Promise, -) { - const heading = root.createComponent( - InlineStack, - {}, - root.createComponent(Box, { minInlineSize: "5%" }), - root.createComponent( - Box, - { minInlineSize: "5%" }, - root.createComponent(Text, { fontWeight: "bold" }, "Variant Name"), - ), - root.createComponent( - Box, - { minInlineSize: "50%" }, - root.createComponent(Text, { fontWeight: "bold" }, "Limit"), - ), - ); - - const renderVariant = ( - variant: ProductVariant, - settings: Record, - root: RemoteRoot, - ) => { - const limit = settings[variant.id]; - - return root.createComponent( - InlineStack, - { columnGap: "none" }, - root.createComponent( - Box, - { minInlineSize: "5%" }, - variant.imageUrl - ? root.createComponent(Image, { - source: variant.imageUrl, - alt: variant.title, - }) - : null, - ), - root.createComponent( - Box, - { minInlineSize: "5%" }, - root.createComponent(Text, {}, variant.title), - ), - root.createComponent( - Box, - { minInlineSize: "50%" }, - root.createComponent(NumberField, { - label: "Set a limit", - value: limit, - min: 0, - max: 99, - defaultValue: String(limit), - onChange: (value: number) => onChange(variant, value), - }), - ), - ); - }; - - // Render table of product variants and inputs to assign limits - return root.createComponent( - Section, - { heading: product.title }, - root.createComponent( - BlockStack, - { paddingBlock: "large" }, - heading, - ...product.variants.map((variant) => - renderVariant(variant, settings, root), - ), - ), - ); -} - -type Product = { - title: string; - variants: ProductVariant[]; -}; - -type ProductVariant = { - id: string; - title: string; - imageUrl?: string; -}; - -async function getProducts( - adminApiQuery: ValidationSettingsApi["query"], -): Promise { - const query = `#graphql - query FetchProducts { - products(first: 5) { - nodes { - title - variants(first: 5) { - nodes { - id - title - image { - url - } - } - } - } - } - }`; - - type ProductQueryData = { - products: { - nodes: Array<{ - title: string; - variants: { - nodes: Array<{ - id: string; - title: string; - image?: { - url: string; - }; - }>; - }; - }>; - }; - }; - - const result = await adminApiQuery(query); - - return ( - result?.data?.products.nodes.map(({ title, variants }) => { - return { - title, - variants: variants.nodes.map((variant) => ({ - title: variant.title, - id: variant.id, - imageUrl: variant?.image?.url, - })), - }; - }) ?? [] - ); } const METAFIELD_NAMESPACE = "$app:product-limits"; const METAFIELD_KEY = "product-limits-values"; -async function getMetafieldDefinition( - adminApiQuery: ValidationSettingsApi["query"], -) { +async function getMetafieldDefinition(adminApiQuery) { const query = `#graphql query GetMetafieldDefinition { metafieldDefinitions(first: 1, ownerType: VALIDATION, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") { @@ -854,22 +433,12 @@ async function getMetafieldDefinition( } `; - type MetafieldDefinitionsQueryData = { - metafieldDefinitions: { - nodes: Array<{ - id: string; - }>; - }; - }; - - const result = await adminApiQuery(query); + const result = await adminApiQuery(query); return result?.data?.metafieldDefinitions?.nodes[0]; } -async function createMetafieldDefinition( - adminApiQuery: ValidationSettingsApi["query"], -) { +async function createMetafieldDefinition(adminApiQuery) { const definition = { access: { admin: "MERCHANT_READ_WRITE", @@ -891,31 +460,17 @@ async function createMetafieldDefinition( } `; - type MetafieldDefinitionCreateData = { - metafieldDefinitionCreate: { - createdDefinition?: { - id: string; - }; - }; - }; - const variables = { definition }; - const result = await adminApiQuery(query, { - variables, - }); + const result = await adminApiQuery(query, { variables }); return result?.data?.metafieldDefinitionCreate?.createdDefinition; } -function createSettings( - products: Product[], - configuration: Object, -): Record { +function createSettings(products, configuration) { const settings = {}; products.forEach(({ variants }) => { variants.forEach(({ id }) => { - // Read existing product limits from metafield const limit = configuration[id]; if (limit) { @@ -927,9 +482,9 @@ function createSettings( return settings; } -{%- elsif flavor contains 'vanilla-js' -%} +{%- else -%} import { - extend, + extension, Text, Box, FunctionSettings, @@ -943,10 +498,9 @@ import { const TARGET = "admin.settings.validation.render"; -export default extend(TARGET, async (root, api) => { +export default extension(TARGET, async (root, api) => { const existingDefinition = await getMetafieldDefinition(api.query); if (!existingDefinition) { - // Create a metafield definition for persistence if no pre-existing definition exists const metafieldDefinition = await createMetafieldDefinition(api.query); if (!metafieldDefinition) { @@ -954,12 +508,10 @@ export default extend(TARGET, async (root, api) => { } } - // Read existing persisted data about product limits from the associated metafield const configuration = JSON.parse( api.data.validation?.metafields?.[0]?.value ?? "{}", ); - // Query product data needed to render the settings UI const products = await getProducts(api.query); renderValidationSettings(root, configuration, products, api); @@ -967,7 +519,6 @@ export default extend(TARGET, async (root, api) => { function renderValidationSettings(root, configuration, products, api) { let errors = []; - // State to keep track of product limit settings, initialized to any persisted metafield value let settings = createSettings(products, configuration); const onError = (newErrors) => { @@ -983,8 +534,6 @@ function renderValidationSettings(root, configuration, products, api) { }; settings = newSettings; - // On input change, commit updated product variant limits to memory. - // Caution: the changes are only persisted on save! const result = await api.applyMetafieldChange({ type: "updateMetafield", namespace: "$app:product-limits", @@ -1018,7 +567,6 @@ function renderValidationSettings(root, configuration, products, api) { const renderContent = () => { return root.append( root.createComponent( - // Note: FunctionSettings must be rendered for the host to receive metafield updates FunctionSettings, { onError }, ...renderErrors(errors, root), @@ -1085,7 +633,6 @@ function renderProductQuantitySettings(root, product, settings, onChange) { ); }; - // Render table of product variants and inputs to assign limits return root.createComponent( Section, { heading: product.title }, @@ -1185,7 +732,6 @@ function createSettings(products, configuration) { products.forEach(({ variants }) => { variants.forEach(({ id }) => { - // Read existing product limits from metafield const limit = configuration[id]; if (limit) {