Thanks Liam but none of that works. I am using the Shopify supplied tutorial on the React UI for discounts, and there is some React magic that does not allow me to set the metafields directly.
Below is the DiscountFunctionsSettings,jsx:
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.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");
}
}
return <App />;
});
function PercentageField({ label, defaultValue, value, onChange, name }) {
return (
<Box>
<BlockStack gap="base">
<NumberField
label={label}
name={name}
value={Number(value)}
defaultValue={String(defaultValue)}
onChange={(value) => onChange(String(value))}
suffix="$"
/>
</BlockStack>
</Box>
);
}
function TagField({ label, defaultValue, value, onChange, name }) {
return (
<Box>
<BlockStack gap="base">
<TextField
label={label}
name={name}
value={String(value)}
defaultValue={String(defaultValue)}
onChange={(value) => onChange(String(value))}
suffix=""
/>
</BlockStack>
</Box>
);
}
function AppliesToCollections({
onClickAdd,
onClickRemove,
value,
defaultValue,
i18n,
appliesTo,
onAppliesToChange,
}) {
return (
<Section>
{/* [START discount-ui-extension.hidden-box] */}
<Box display="none">
<TextField
value={value.map(({ id }) => id).join(",")}
label=""
name="collectionsIds"
defaultValue={defaultValue.map(({ id }) => id).join(",")}
/>
</Box>
{/* [END discount-ui-extension.hidden-box] */}
<BlockStack gap="base">
<InlineStack blockAlignment="end" gap="base">
<Select
label={i18n.translate("collections.appliesTo")}
name="appliesTo"
value={appliesTo}
onChange={onAppliesToChange}
options={[
{
label: i18n.translate("collections.allProducts"),
value: "all",
},
{
label: i18n.translate("collections.collections"),
value: "collections",
},
]}
/>
{appliesTo === "all" ? null : (
<Box inlineSize={180}>
<Button onClick={onClickAdd}>
{i18n.translate("collections.buttonLabel")}
</Button>
</Box>
)}
</InlineStack>
<CollectionsSection collections={value} onClickRemove={onClickRemove} />
</BlockStack>
</Section>
);
}
function CollectionsSection({ collections, onClickRemove }) {
if (collections.length === 0) {
return null;
}
return collections.map((collection) => (
<BlockStack gap="base" key={collection.id}>
<InlineStack blockAlignment="center" inlineAlignment="space-between">
<Link
href={`shopify://admin/collections/${collection.id.split("/").pop()}`}
tone="inherit"
target="_blank"
>
{collection.title}
</Link>
<Button variant="tertiary" onClick={() => onClickRemove(collection.id)}>
<Icon name="CircleCancelMajor" />
</Button>
</InlineStack>
<Divider />
</BlockStack>
));
}
function App() {
const {
applyExtensionMetafieldChange,
i18n,
initialPercentages,
onPercentageValueChange,
percentages,
initialTag,
onTagValueChange,
tag,
resetForm,
initialCollections,
collections,
appliesTo,
onAppliesToChange,
removeCollection,
onSelectedCollections,
loading,
} = useExtensionData();
if (loading) {
return <Text>{i18n.translate("loading")}</Text>;
}
return (
<FunctionSettings onSave={applyExtensionMetafieldChange}>
<Heading size={6}>{i18n.translate("title")}</Heading>
<Form onReset={resetForm} onSubmit={applyExtensionMetafieldChange}>
<Section>
<BlockStack gap="base">
<BlockStack gap="base">
<PercentageField
value={String(percentages.product)}
defaultValue={String(initialPercentages.product)}
onChange={(value) => onPercentageValueChange("product", value)}
label={i18n.translate("percentage.Product")}
name="product"
/>
<TagField
value={String(tag)}
defaultValue={String(initialTag)}
onChange={(value) => onTagValueChange("tag", value)}
label={i18n.translate("tag")}
name="tag"
/>
</BlockStack>
{collections.length === 0 ? <Divider /> : null}
</BlockStack>
</Section>
</Form>
</FunctionSettings>
);
}
function useExtensionData() {
const { applyMetafieldChange, i18n, data, resourcePicker, query } =
useApi(TARGET);
const metafieldConfig = useMemo(
() =>
parseMetafield(
data?.metafields.find(
(metafield) => metafield.key === "function-configuration"
)?.value
),
[data?.metafields]
);
const [percentages, setPercentages] = useState(metafieldConfig.percentages);
const [tag, setTags] = useState(metafieldConfig.tag)
const [initialCollections, setInitialCollections] = useState([]);
const [collections, setCollections] = useState([]);
const [appliesTo, setAppliesTo] = useState("all");
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchCollections = async () => {
setLoading(true);
const selectedCollections = await getCollections(
metafieldConfig.collectionIds,
query
);
setInitialCollections(selectedCollections);
setCollections(selectedCollections);
setLoading(false);
setAppliesTo(selectedCollections.length > 0 ? "collections" : "all");
};
fetchCollections();
}, [metafieldConfig.collectionIds, query]);
const onPercentageValueChange = async (type, value) => {
setPercentages((prev) => ({
...prev,
[type]: Number(value),
}));
};
const onTagValueChange = async (type, value) => {
setTags((prev) => ({
...prev,
[type]: String(value),
}));
};
/* const onTagValueChange = async (type, value) => {
setTag({
[type]: String(value),
});
}; */
const onAppliesToChange = (value) => {
setAppliesTo(value);
if (value === "all") {
setCollections([]);
}
};
async function applyExtensionMetafieldChange() {
await applyMetafieldChange({
type: "updateMetafield",
namespace: "$app:example-discounts--ui-extension",
key: "function-configuration",
value: JSON.stringify({
cartLinePercentage: percentages.product,
cartLineTag: tag,
orderPercentage: percentages.order,
deliveryPercentage: percentages.shipping,
collectionIds: collections.map(({ id }) => id),
}),
valueType: "json",
});
setInitialCollections(collections);
}
const resetForm = () => {
setPercentages(metafieldConfig.percentages);
setTags(metafieldConfig.tag);
setCollections(initialCollections);
setAppliesTo(initialCollections.length > 0 ? "collections" : "all");
};
const onSelectedCollections = async () => {
const selection = await resourcePicker({
type: "collection",
selectionIds: collections.map(({ id }) => ({ id })),
action: "select",
filter: {
archived: true,
variants: true,
},
});
setCollections(selection ?? []);
};
const removeCollection = (id) => {
setCollections((prev) => prev.filter((collection) => collection.id !== id));
};
return {
applyExtensionMetafieldChange,
i18n,
initialPercentages: metafieldConfig.percentages,
initialTag: metafieldConfig.tag,
onPercentageValueChange,
percentages,
onTagValueChange,
tag,
resetForm,
collections,
initialCollections,
removeCollection,
onSelectedCollections,
loading,
appliesTo,
onAppliesToChange,
};
}
const METAFIELD_NAMESPACE = "$app:example-discounts--ui-extension";
const METAFIELD_KEY = "function-configuration";
async function getMetafieldDefinition(adminApiQuery) {
const query = `#graphql
query GetMetafieldDefinition {
metafieldDefinitions(first: 1, ownerType: DISCOUNT, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") {
nodes {
id
}
}
}
`;
const result = await adminApiQuery(query);
return result?.data?.metafieldDefinitions?.nodes[0];
}
async function createMetafieldDefinition(adminApiQuery) {
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 };
const result = await adminApiQuery(query, { variables });
return result?.data?.metafieldDefinitionCreate?.createdDefinition;
}
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),
},
tag: String(parsed.cartLineTag.tag ?? ""),
collectionIds: parsed.collectionIds ?? [],
};
} catch {
return {
percentages: { product: 0, order: 0, shipping: 0 },
collectionIds: [],
tag: "",
};
}
}
async function getCollections(collectionGids, adminApiQuery) {
const query = `#graphql
query GetCollections($ids: [ID!]!) {
collections: nodes(ids: $ids) {
... on Collection {
id
title
}
}
}
`;
const result = await adminApiQuery(query, {
variables: { ids: collectionGids },
});
return result?.data?.collections ?? [];
}
From my log files, the metafield configuration is:
"discountNode": {
"metafield": {
"value": "{\"cartLinePercentage\":25,\"cartLineTag\":{\"tag\":\"2FOR25\"},\"orderPercentage\":0,\"deliveryPercentage\":0,\"collectionIds\":[]}"
}
}
So there seems to be no way at all using the supplied template to FORCE, the metafields to be “flat” other than the supplied percentages and collectionIds. assume there is some library magic behind the curtains, is there an alternative template somewhere or resource where I can build my own UI?
I think the above function needs to modify the value of “tag” but I am unable to figure out why its setting it to a nested instead of flat value
Thanks.