diff --git a/.eslintrc.js b/.eslintrc.js index c4f6c682a5..79df2bb159 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,5 +60,12 @@ module.exports = { 'promise/catch-or-return': 'off', }, }, + { + files: ['packages/ui-extensions/src/surfaces/checkout/preact/*.ts'], + rules: { + // Support use of deprecated hooks until removal + 'import/no-deprecated': 'off', + }, + }, ], }; diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index efcf4577bf..34089aa64f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,10 @@ on: - 20[0-9][0-9]-[01][1470] # RC version branches - 20[0-9][0-9]-[01][1470]-rc + # Snapit trigger - runs when /snapit comment is made on a PR + issue_comment: + types: + - created concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/packages/ui-extensions/CHANGELOG.md b/packages/ui-extensions/CHANGELOG.md index e3fd657ce8..29d5b83e30 100644 --- a/packages/ui-extensions/CHANGELOG.md +++ b/packages/ui-extensions/CHANGELOG.md @@ -1,5 +1,19 @@ # @shopify/ui-extensions +## 2026.1.2 + +### Patch Changes + +- [#3864](https://github.com/Shopify/ui-extensions/pull/3864) [`24cc179`](https://github.com/Shopify/ui-extensions/commit/24cc17924ef9965c9c1bc5ebcb3a0bfb8742b794) Thanks [@avocadomayo](https://github.com/avocadomayo)! - Add deprecation warning to checkout metafield read API in Checkout UI Extensions + +## 2026.1.1 + +### Patch Changes + +- [#3781](https://github.com/Shopify/ui-extensions/pull/3781) [`21a83fa`](https://github.com/Shopify/ui-extensions/commit/21a83fad55126b256cc0a3fe830803e0ea49d9cf) Thanks [@billfienberg](https://github.com/billfienberg)! - Admin: Expose picker and resource picker APIs for all rendering extensions + +- [#3788](https://github.com/Shopify/ui-extensions/pull/3788) [`a8b9485`](https://github.com/Shopify/ui-extensions/commit/a8b9485a19d7f0fdb4413636a3c667695d7c178d) Thanks [@avocadomayo](https://github.com/avocadomayo)! - Add deprecation warning to checkout metafield write APIs in Checkout UI Extension API + ## 2026.1.0 ### Minor Changes diff --git a/packages/ui-extensions/docs/shared/build-docs-type-resolver.mjs b/packages/ui-extensions/docs/shared/build-docs-type-resolver.mjs new file mode 100644 index 0000000000..73c1910da9 --- /dev/null +++ b/packages/ui-extensions/docs/shared/build-docs-type-resolver.mjs @@ -0,0 +1,399 @@ +/** + * Shared type resolver for build-docs-targets-json scripts + * + * This module provides utilities for parsing TypeScript type definitions + * and resolving component types, including handling of: + * - String literals: 'ComponentName' + * - Type references: TypeName + * - Union types: A | B | C + * - Exclude types: Exclude + * - Array types: (typeof ARRAY)[number] + * - @private JSDoc tags for excluding private components + */ + +import fs from 'fs'; +import path from 'path'; + +/** + * Creates a type resolver instance for a specific surface + * @param {string} sharedTsPath - Path to the shared.ts file for the surface + * @returns {Object} Type resolver with methods for resolving types + */ +export function createTypeResolver(sharedTsPath) { + // Cache for type definitions parsed from shared.ts + let typeDefinitionsCache = null; + const resolvedTypesCache = {}; + // Set of type names that are marked with @private JSDoc tag + let privateTypesCache = null; + + /** + * Parse all type definitions from shared.ts + * Returns a map of type name -> type definition string + * Also detects @private JSDoc tags to identify private types + */ + function parseTypeDefinitions() { + if (typeDefinitionsCache !== null) { + return typeDefinitionsCache; + } + + typeDefinitionsCache = {}; + privateTypesCache = new Set(); + + try { + if (!fs.existsSync(sharedTsPath)) { + return typeDefinitionsCache; + } + + const content = fs.readFileSync(sharedTsPath, 'utf-8'); + + // Parse const arrays (like SUPPORTED_COMPONENTS) - store as resolved arrays + const constArrayRegex = + /export const (\w+)\s*=\s*\[([\s\S]*?)\]\s*as const/g; + let constMatch; + while ((constMatch = constArrayRegex.exec(content)) !== null) { + const constName = constMatch[1]; + const arrayContent = constMatch[2]; + const components = arrayContent.match(/'([^']+)'/g); + if (components) { + typeDefinitionsCache[constName] = components.map((c) => + c.replace(/'/g, ''), + ); + } + } + + // Parse all type definitions: export type TypeName = Definition; + // Also check for @private JSDoc comments preceding the type + const typeRegex = /export type (\w+)(?:<[^>]+>)?\s*=\s*/g; + let match; + + while ((match = typeRegex.exec(content)) !== null) { + const typeName = match[1]; + const startPos = match.index + match[0].length; + + // Check for @private in JSDoc comment before this type definition + // Look backwards from the match to find a preceding JSDoc comment + const beforeMatch = content.substring(0, match.index); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + if (lastJsDocEnd !== -1) { + // Check if there's no other code between the JSDoc and this type + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + if (between === '') { + // Find the start of this JSDoc comment + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + // Check if this JSDoc contains @private tag + if (jsDocContent.includes('@private')) { + privateTypesCache.add(typeName); + } + } + } + } + + // Find the end of the type definition (semicolon at depth 0) + let endPos = startPos; + let depth = 0; + let inString = false; + + for (let i = startPos; i < content.length; i++) { + const char = content[i]; + const prevChar = i > 0 ? content[i - 1] : ''; + + if (char === "'" && prevChar !== '\\') { + inString = !inString; + } + + if (!inString) { + if (char === '<' || char === '(' || char === '{' || char === '[') { + depth++; + } else if ( + char === '>' || + char === ')' || + char === '}' || + char === ']' + ) { + depth--; + } else if (char === ';' && depth === 0) { + endPos = i; + break; + } + } + } + + const definition = content.substring(startPos, endPos).trim(); + typeDefinitionsCache[typeName] = definition; + } + + return typeDefinitionsCache; + } catch (error) { + console.warn('Warning: Failed to parse type definitions:', error.message); + return typeDefinitionsCache; + } + } + + /** + * Check if a type is marked as @private + */ + function isPrivateType(typeName) { + // Ensure type definitions are parsed (which also populates privateTypesCache) + parseTypeDefinitions(); + return privateTypesCache && privateTypesCache.has(typeName); + } + + /** + * Get the components from @private types (to exclude them from other types) + */ + function getPrivateComponents() { + parseTypeDefinitions(); + const privateComponents = []; + if (privateTypesCache) { + for (const typeName of privateTypesCache) { + const resolved = resolveTypeInternal(typeName, false); // Don't filter private when getting private components + privateComponents.push(...resolved); + } + } + return privateComponents; + } + + /** + * Internal type resolver - can optionally skip filtering of @private components + * @param {string} typeExpr - The type expression to resolve + * @param {boolean} filterPrivate - Whether to filter out @private components (default: true) + */ + function resolveTypeInternal(typeExpr, filterPrivate = true) { + if (!typeExpr) return []; + + typeExpr = typeExpr.trim(); + + // Check cache first (but only for filtered results) + const cacheKey = filterPrivate ? typeExpr : `__unfiltered__${typeExpr}`; + if (resolvedTypesCache[cacheKey]) { + return [...resolvedTypesCache[cacheKey]]; + } + + const typeDefs = parseTypeDefinitions(); + let result = []; + + // Handle string literal: 'ComponentName' + const stringLiteralMatch = typeExpr.match(/^'([^']+)'$/); + if (stringLiteralMatch) { + result = [stringLiteralMatch[1]]; + resolvedTypesCache[cacheKey] = result; + return [...result]; + } + + // IMPORTANT: Check for unions FIRST before other patterns + // This ensures we don't match partial patterns within a union + if (typeExpr.includes('|')) { + const parts = splitByTopLevelPipe(typeExpr); + // Only treat as union if we actually got multiple parts + if (parts.filter((p) => p.trim()).length > 1) { + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed) { + result.push(...resolveTypeInternal(trimmed, filterPrivate)); + } + } + // Deduplicate + result = [...new Set(result)]; + resolvedTypesCache[cacheKey] = result; + return [...result]; + } + } + + // Handle (typeof ARRAY_NAME)[number] - anchored to match entire expression + const typeofMatch = typeExpr.match(/^\(typeof (\w+)\)\[number\]$/); + if (typeofMatch) { + const arrayName = typeofMatch[1]; + if (Array.isArray(typeDefs[arrayName])) { + result = [...typeDefs[arrayName]]; + resolvedTypesCache[cacheKey] = result; + return [...result]; + } + } + + // Handle Exclude + const excludeMatch = typeExpr.match(/^Exclude<(.+)>$/); + if (excludeMatch) { + const innerContent = excludeMatch[1]; + const parts = splitByTopLevelComma(innerContent); + if (parts.length >= 2) { + const baseType = parts[0].trim(); + const excludedType = parts[1].trim(); + + const baseComponents = resolveTypeInternal(baseType, filterPrivate); + const excludedComponents = resolveTypeInternal( + excludedType, + filterPrivate, + ); + + result = baseComponents.filter((c) => !excludedComponents.includes(c)); + resolvedTypesCache[cacheKey] = result; + return [...result]; + } + } + + // Handle type reference (look up in definitions and resolve recursively) + if (/^\w+$/.test(typeExpr)) { + // If this type itself is marked @private, return empty (unless we're not filtering) + if (filterPrivate && isPrivateType(typeExpr)) { + resolvedTypesCache[cacheKey] = []; + return []; + } + + const definition = typeDefs[typeExpr]; + if (Array.isArray(definition)) { + // It's a pre-resolved array (like SUPPORTED_COMPONENTS) + result = [...definition]; + } else if (typeof definition === 'string') { + // Recursively resolve the definition + result = resolveTypeInternal(definition, filterPrivate); + } + resolvedTypesCache[cacheKey] = result; + return [...result]; + } + + // If we can't resolve it, return empty + resolvedTypesCache[cacheKey] = result; + return [...result]; + } + + /** + * Resolve a type expression to component strings + * Automatically excludes components from types marked with @private JSDoc tag + * @param {string} typeExpr - The type expression to resolve + * @returns {string[]} Array of component names + */ + function resolveType(typeExpr) { + // Get result with private components filtered out + let result = resolveTypeInternal(typeExpr, true); + + // Also filter out any components that come from @private types + const privateComponents = getPrivateComponents(); + if (privateComponents.length > 0) { + result = result.filter((c) => !privateComponents.includes(c)); + } + + return result; + } + + /** + * Resolve a type expression WITHOUT filtering @private components + * Use this for AllowedComponents<> which explicitly allows private components + * @param {string} typeExpr - The type expression to resolve + * @returns {string[]} Array of component names + */ + function resolveTypeUnfiltered(typeExpr) { + return resolveTypeInternal(typeExpr, false); + } + + /** + * Get the parsed type definitions (for debugging or advanced use) + */ + function getTypeDefinitions() { + return parseTypeDefinitions(); + } + + /** + * Get the set of private type names + */ + function getPrivateTypeNames() { + parseTypeDefinitions(); + return privateTypesCache ? new Set(privateTypesCache) : new Set(); + } + + return { + resolveType, + resolveTypeUnfiltered, + getTypeDefinitions, + getPrivateTypeNames, + getPrivateComponents, + isPrivateType, + }; +} + +/** + * Split a string by top-level pipe (|) characters, respecting nesting + */ +export function splitByTopLevelPipe(str) { + const parts = []; + let current = ''; + let depth = 0; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + if (char === '<' || char === '(' || char === '{' || char === '[') { + depth++; + current += char; + } else if (char === '>' || char === ')' || char === '}' || char === ']') { + depth--; + current += char; + } else if (char === '|' && depth === 0) { + parts.push(current); + current = ''; + } else { + current += char; + } + } + + if (current) { + parts.push(current); + } + + return parts; +} + +/** + * Split a string by top-level comma (,) characters, respecting nesting + */ +export function splitByTopLevelComma(str) { + const parts = []; + let current = ''; + let angleDepth = 0; + let braceDepth = 0; + let parenDepth = 0; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + if (char === '<') { + angleDepth++; + current += char; + } else if (char === '>') { + angleDepth--; + current += char; + } else if (char === '{') { + braceDepth++; + current += char; + } else if (char === '}') { + braceDepth--; + current += char; + } else if (char === '(') { + parenDepth++; + current += char; + } else if (char === ')') { + parenDepth--; + current += char; + } else if ( + char === ',' && + angleDepth === 0 && + braceDepth === 0 && + parenDepth === 0 + ) { + parts.push(current); + current = ''; + } else { + current += char; + } + } + + if (current) { + parts.push(current); + } + + return parts; +} diff --git a/packages/ui-extensions/docs/surfaces/admin/build-docs-admin-json.mjs b/packages/ui-extensions/docs/surfaces/admin/build-docs-admin-json.mjs new file mode 100644 index 0000000000..f54cd6d9a0 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/admin/build-docs-admin-json.mjs @@ -0,0 +1,639 @@ +const fs = require('fs'); +const path = require('path'); + +// Configuration for admin surface +const config = { + basePath: path.join( + __dirname, + '../packages/ui-extensions/src/surfaces/admin', + ), + outputPath: path.join( + __dirname, + '../packages/ui-extensions/src/surfaces/admin/generated-targets.json', + ), + componentTypesPath: 'components', + hasComponentTypes: true, +}; + +// All components will be populated from StandardComponents +let allComponents = []; + +// Cache for parsed API files to avoid re-reading +const apiDefinitionsCache = {}; + +// Cache for checkout components +let checkoutComponentsCache = null; + +/** + * Parse components from checkout's shared.ts file + */ +function parseCheckoutComponents() { + // Return cached value if available + if (checkoutComponentsCache !== null) { + return checkoutComponentsCache; + } + + try { + // Always look in the checkout surface directory + const checkoutBasePath = path.join( + __dirname, + '../packages/ui-extensions/src/surfaces/checkout', + ); + const sharedPath = path.join(checkoutBasePath, 'shared.ts'); + + if (!fs.existsSync(sharedPath)) { + checkoutComponentsCache = ['[CheckoutComponentsNotFound]']; + return checkoutComponentsCache; + } + + const content = fs.readFileSync(sharedPath, 'utf-8'); + + // Look for SUPPORTED_COMPONENTS array + const supportedMatch = content.match( + /export const SUPPORTED_COMPONENTS\s*=\s*\[([\s\S]*?)\]\s*as const/, + ); + + if (supportedMatch) { + const arrayContent = supportedMatch[1]; + // Extract all quoted strings + const components = arrayContent.match(/'([^']+)'/g); + if (components) { + checkoutComponentsCache = components.map((c) => c.replace(/'/g, '')); + return checkoutComponentsCache; + } + } + + checkoutComponentsCache = ['[CheckoutComponentsParseError]']; + return checkoutComponentsCache; + } catch (error) { + checkoutComponentsCache = ['[CheckoutComponentsError]']; + return checkoutComponentsCache; + } +} + +/** + * Parse a string union type from a component file and resolve type references + * e.g., export type SmartGridComponents = 'Tile'; + * or: export type BlockExtensionComponents = StandardComponents | 'AdminBlock'; + * or: export type StandardComponents = AnyComponent | 'Avatar' | ... (with AnyComponent imported) + */ +function parseStringUnionType(filePath, componentTypesMap = {}) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Extract all quoted component names from the file (but not from import statements) + // Remove import lines first + const contentWithoutImports = content.replace(/^import.*?;$/gm, ''); + const componentNames = contentWithoutImports.match(/'([^']+)'/g); + const quotedComponents = componentNames + ? componentNames.map((name) => name.replace(/'/g, '')) + : []; + + // Check if the type references other types (like StandardComponents or AnyComponent) + // Look for patterns like: StandardComponents | 'OtherComponent' + const typeRefPattern = /export type \w+ =\s*([\s\S]*?);/; + const typeDefMatch = content.match(typeRefPattern); + + if (typeDefMatch) { + const typeDef = typeDefMatch[1]; + // Find references to other types (capitalized words that aren't in quotes) + const typeRefs = typeDef.match(/\b([A-Z]\w+(?:Components?)?)\b/g); + + if (typeRefs) { + const allComponents = [...quotedComponents]; + + for (const typeRef of typeRefs) { + // Check if this is AnyComponent (imported from checkout) + if (typeRef === 'AnyComponent') { + // Add all checkout components + const checkoutComponents = parseCheckoutComponents(); + allComponents.push(...checkoutComponents); + } + // If this type reference exists in our map, include its components + else if (componentTypesMap[typeRef]) { + allComponents.push(...componentTypesMap[typeRef]); + } + } + + // Remove duplicates + return [...new Set(allComponents)]; + } + } + + return quotedComponents.length > 0 ? quotedComponents : null; + } catch (error) { + console.error(`Error reading component file ${filePath}:`, error.message); + } + return null; +} + +/** + * Parse component types from files in the components directory + * Uses a two-pass approach: + * 1. First pass: Parse all component types that only contain quoted strings + * 2. Second pass: Parse types that reference other types (like StandardComponents) + */ +function parseComponentTypesFromFiles() { + if (!config.hasComponentTypes || !config.componentTypesPath) { + return {}; + } + + const componentsPath = path.join(config.basePath, config.componentTypesPath); + const componentTypesMap = {}; + + try { + // Look for all TypeScript files in the components directory + const files = fs.readdirSync(componentsPath); + const tsFiles = files.filter( + (file) => file.endsWith('.ts') && !file.endsWith('.d.ts'), + ); + + // First pass: Parse files with only quoted strings + for (const file of tsFiles) { + const filePath = path.join(componentsPath, file); + const componentTypeName = file.replace('.ts', ''); + + // Skip certain files + if ( + componentTypeName === 'shared' || + componentTypeName === 'components' + ) { + continue; + } + + const components = parseStringUnionType(filePath, {}); + if (components && components.length > 0) { + componentTypesMap[componentTypeName] = components; + + // If this is StandardComponents, use it as the base + if (componentTypeName === 'StandardComponents') { + allComponents = components.sort(); + } + } + } + + // Second pass: Re-parse files that might reference other types + // This allows types like BlockExtensionComponents = StandardComponents | 'AdminBlock' + for (const file of tsFiles) { + const filePath = path.join(componentsPath, file); + const componentTypeName = file.replace('.ts', ''); + + // Skip certain files + if ( + componentTypeName === 'shared' || + componentTypeName === 'components' + ) { + continue; + } + + const components = parseStringUnionType(filePath, componentTypesMap); + if (components && components.length > 0) { + componentTypesMap[componentTypeName] = components; + } + } + } catch (error) { + console.error('Error parsing component types:', error.message); + } + + return componentTypesMap; +} + +function parseTargetsFile() { + const targetsFilePath = path.join(config.basePath, 'extension-targets.ts'); + + const content = fs.readFileSync(targetsFilePath, 'utf-8'); + + // Parse component type definitions + const componentTypesMap = parseComponentTypesFromFiles(); + + const targets = {}; + + // Look for all interfaces that might contain RenderExtension targets + const interfaceNames = [ + 'RenderExtensionTargets', + 'OrderStatusExtensionTargets', + 'CustomerAccountExtensionTargets', + 'ExtensionTargets', + ]; + + for (const interfaceName of interfaceNames) { + // Try to find this interface + const regex = new RegExp( + `export interface ${interfaceName}[^{]*\\{([\\s\\S]+?)\\n\\}`, + ); + const match = content.match(regex); + + if (match && match[1].includes('RenderExtension<')) { + parseTargetsFromInterfaceBody(match[1], targets, componentTypesMap); + } + } + + if (Object.keys(targets).length === 0) { + throw new Error('Could not find extension targets interface'); + } + + return targets; +} + +function parseTargetsFromInterfaceBody(interfaceBody, targets, componentTypesMap) { + // Parse each target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RenderExtension<([\s\S]*?)>;/g; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + let renderExtensionContent = match[2].trim(); + + // Remove comments before parsing + renderExtensionContent = renderExtensionContent + .replace(/\/\/[^\n]*/g, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments + + // Split by comma to separate API and Components + const parts = splitByTopLevelComma(renderExtensionContent); + + if (parts.length >= 2) { + const apiString = parts[0].trim(); + const componentString = parts[1].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // Parse components + const components = parseComponents(componentString, componentTypesMap); + + targets[targetName] = { + components: components.sort(), + apis: apis.sort(), + }; + } + } +} + +function splitByTopLevelComma(str) { + const parts = []; + let current = ''; + let angleDepth = 0; + let braceDepth = 0; + let parenDepth = 0; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + if (char === '<') { + angleDepth++; + current += char; + } else if (char === '>') { + angleDepth--; + current += char; + } else if (char === '{') { + braceDepth++; + current += char; + } else if (char === '}') { + braceDepth--; + current += char; + } else if (char === '(') { + parenDepth++; + current += char; + } else if (char === ')') { + parenDepth--; + current += char; + } else if ( + char === ',' && + angleDepth === 0 && + braceDepth === 0 && + parenDepth === 0 + ) { + parts.push(current); + current = ''; + } else { + current += char; + } + } + + if (current) { + parts.push(current); + } + + return parts; +} + +function getNestedApis(apiName) { + // Check if we've already parsed this API + if (apiDefinitionsCache.hasOwnProperty(apiName)) { + return apiDefinitionsCache[apiName]; + } + + // Try to find the API file in the surface's api directory + const apiDir = path.join(config.basePath, 'api'); + + if (!fs.existsSync(apiDir)) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Convert API name to potential file paths + // e.g., StandardApi -> standard-api, CartApi -> cart-api + const kebabName = apiName + .replace(/Api$/, '') + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); + + // Try multiple possible locations + const possiblePaths = [ + path.join(apiDir, `${kebabName}.ts`), + path.join(apiDir, `${kebabName}`, `${kebabName}.ts`), + path.join(apiDir, kebabName.replace(/-/g, ''), `${kebabName}.ts`), + path.join(apiDir, `${kebabName}-api`, `${kebabName}-api.ts`), + path.join(apiDir, `${kebabName}-api.ts`), + path.join(apiDir, `standard-api`, `standard-api.ts`), + ]; + + let content = null; + for (const apiFilePath of possiblePaths) { + try { + if (fs.existsSync(apiFilePath)) { + content = fs.readFileSync(apiFilePath, 'utf-8'); + break; + } + } catch (error) { + // Continue to next path + } + } + + if (!content) { + apiDefinitionsCache[apiName] = []; + return []; + } + + try { + // Find the export type definition for this API + const typeDefStartRegex = new RegExp(`export type ${apiName}[^=]*=`, 's'); + const startMatch = content.match(typeDefStartRegex); + + if (!startMatch) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Find the end position (semicolon at the correct nesting level) + let startPos = startMatch.index + startMatch[0].length; + let endPos = startPos; + let braceDepth = 0; + let angleDepth = 0; + + for (let i = startPos; i < content.length; i++) { + const char = content[i]; + + if (char === '{') braceDepth++; + else if (char === '}') braceDepth--; + else if (char === '<') angleDepth++; + else if (char === '>') angleDepth--; + else if (char === ';' && braceDepth === 0 && angleDepth === 0) { + endPos = i; + break; + } + } + + const typeDef = content.substring(startPos, endPos); + + // Extract all API names from the type definition + const nestedApis = []; + const apiMatches = typeDef.matchAll(/(\w+Api)\b/g); + + for (const apiMatch of apiMatches) { + const nestedApiName = apiMatch[1]; + // Don't include the API itself + if (nestedApiName !== apiName && !nestedApis.includes(nestedApiName)) { + nestedApis.push(nestedApiName); + } + } + + apiDefinitionsCache[apiName] = nestedApis; + return nestedApis; + } catch (error) { + // Error parsing, cache and return empty array + apiDefinitionsCache[apiName] = []; + return []; + } +} + +function parseApis(apiString) { + const apisSet = new Set(); + + // Remove any comments + apiString = apiString + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by & and extract API names + const parts = apiString + .split('&') + .map((s) => s.trim()) + .filter((s) => s); + + for (const part of parts) { + // Match API names (e.g., StandardApi<'...'> or just ApiName) + let apiName = null; + + // Extract just the type name before any generic parameters + const apiMatch = part.match(/^(\w+Api)/); + if (apiMatch) { + apiName = apiMatch[1]; + } else { + // Try to match any capitalized type name ending in Api + const generalMatch = part.match(/(\w+Api)\b/); + if (generalMatch) { + apiName = generalMatch[1]; + } + } + + if (apiName) { + // Add the API itself + apisSet.add(apiName); + + // Get nested APIs from this API (recursively) + const nestedApis = getNestedApis(apiName); + for (const nestedApi of nestedApis) { + apisSet.add(nestedApi); + // Recursively get nested APIs of nested APIs + const deepNestedApis = getNestedApis(nestedApi); + deepNestedApis.forEach((api) => apisSet.add(api)); + } + } + } + + return Array.from(apisSet); +} + +/** + * Parse a TypeScript component type expression + * Handles various patterns: type refs, unions, Exclude, AllowedComponents, etc. + */ +function parseComponents(componentString, componentTypesMap) { + // Normalize whitespace + componentString = componentString.replace(/\s+/g, ' ').trim(); + + // Handle AnyCheckoutComponentExcept<'Component1' | 'Component2'> + const checkoutExceptMatch = componentString.match(/AnyCheckoutComponentExcept<([^>]+)>/); + if (checkoutExceptMatch) { + const excludedUnion = checkoutExceptMatch[1]; + // Get all checkout components + const allCheckoutComponents = parseCheckoutComponents(); + // Parse the union of excluded components + const excludedComponents = parseUnionOfStrings(excludedUnion); + // Filter out the excluded components + return allCheckoutComponents.filter((c) => !excludedComponents.includes(c)); + } + + // Handle Exclude + const excludeMatch = componentString.match(/Exclude<(\w+),\s*'([^']+)'>/); + if (excludeMatch) { + const baseType = excludeMatch[1]; + const excluded = excludeMatch[2]; + const baseComponents = resolveComponentType(baseType, componentTypesMap); + return baseComponents.filter((c) => c !== excluded); + } + + // Handle AllowedComponents + const allowedMatch = componentString.match(/AllowedComponents<([^>]+)>/); + if (allowedMatch) { + const innerType = allowedMatch[1].trim(); + return resolveComponentType(innerType, componentTypesMap); + } + + // Check if it's a direct type reference + const result = resolveComponentType(componentString, componentTypesMap); + if (result.length > 0) { + return result; + } + + // Default to all components if we have them + if (allComponents.length > 0) { + return allComponents; + } + + return ['[Unknown]']; +} + +/** + * Parse a union of string literals (e.g., "'Image' | 'Banner'") + * Returns an array of the string values + */ +function parseUnionOfStrings(unionString) { + const components = []; + // Split by | and extract quoted strings + const parts = unionString.split('|'); + for (const part of parts) { + const trimmed = part.trim(); + const match = trimmed.match(/^'([^']+)'$/); + if (match) { + components.push(match[1]); + } + } + return components; +} + +/** + * Resolve a component type name to a list of component names + */ +function resolveComponentType(typeName, componentTypesMap) { + typeName = typeName.trim(); + + // Check if it's in our component types map + if (componentTypesMap[typeName]) { + return componentTypesMap[typeName]; + } + + // Handle special checkout types + if (typeName === 'AnyCheckoutComponent' || typeName === 'AnyComponent' || typeName === 'AnyThankYouComponent') { + return parseCheckoutComponents(); + } + + // Check if it's a quoted string literal + const quotedMatch = typeName.match(/^'([^']+)'$/); + if (quotedMatch) { + return [quotedMatch[1]]; + } + + return []; +} + +function createCombinedMapping(targetsJson) { + const result = {...targetsJson}; + + // Create reverse mappings for APIs + const apiToTargets = {}; + const componentToTargets = {}; + + // Iterate through all targets + for (const [targetName, targetData] of Object.entries(targetsJson)) { + // Map APIs to targets + for (const api of targetData.apis) { + if (!apiToTargets[api]) { + apiToTargets[api] = []; + } + apiToTargets[api].push(targetName); + } + + // Map Components to targets + for (const component of targetData.components) { + if (!componentToTargets[component]) { + componentToTargets[component] = []; + } + componentToTargets[component].push(targetName); + } + } + + // Add API reverse mappings to result + for (const [api, targets] of Object.entries(apiToTargets)) { + result[api] = { + targets: targets.sort(), + }; + } + + // Add Component reverse mappings to result + for (const [component, targets] of Object.entries(componentToTargets)) { + result[component] = { + targets: targets.sort(), + }; + } + + return result; +} + +// Main execution +try { + console.log('\nšŸ” Generating targets JSON for admin surface'); + console.log(`šŸ“ Base path: ${config.basePath}`); + + // Generate the JSON + const targetsJson = parseTargetsFile(); + + // Create the combined JSON with reverse mappings + const combinedJson = createCombinedMapping(targetsJson); + + // Write to output file + fs.writeFileSync(config.outputPath, JSON.stringify(combinedJson, null, 2)); + + console.log('āœ… Generated combined targets JSON at:', config.outputPath); + + // Count the different types of entries + const targetEntries = Object.keys(targetsJson).length; + const apiEntries = Object.keys(combinedJson).filter( + (key) => combinedJson[key].targets && !targetsJson[key] && key.endsWith('Api') + ).length; + const componentEntries = Object.keys(combinedJson).filter( + (key) => combinedJson[key].targets && !targetsJson[key] && !key.endsWith('Api') + ).length; + + console.log('\nšŸ“‹ Summary:'); + console.log(` Extension targets: ${targetEntries}`); + console.log(` API reverse mappings: ${apiEntries}`); + console.log(` Component reverse mappings: ${componentEntries}`); + console.log(` Total entries in JSON: ${Object.keys(combinedJson).length}`); +} catch (error) { + console.error('āŒ Error generating targets JSON:', error.message); + console.error(error.stack); + process.exit(1); +} diff --git a/packages/ui-extensions/docs/surfaces/admin/build-docs-targets-json.mjs b/packages/ui-extensions/docs/surfaces/admin/build-docs-targets-json.mjs new file mode 100644 index 0000000000..af5894bb1b --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/admin/build-docs-targets-json.mjs @@ -0,0 +1,718 @@ +import fs from 'fs'; +import path from 'path'; +import {fileURLToPath} from 'url'; +import { + createTypeResolver, + splitByTopLevelComma, +} from '../../shared/build-docs-type-resolver.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Find the generated_docs_data.json file to determine output location +function findGeneratedDocsPath() { + const generatedDir = path.join(__dirname, 'generated'); + + // Look for generated_docs_data.json recursively + function findFile(dir) { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + const result = findFile(fullPath); + if (result) return result; + } else if (file === 'generated_docs_data.json') { + return path.dirname(fullPath); + } + } + return null; + } + + const docsPath = findFile(generatedDir); + return docsPath || generatedDir; // Fallback to generated root if not found +} + +// Configuration for admin surface +const config = { + basePath: path.join(__dirname, '../../../src/surfaces/admin'), + outputPath: path.join( + findGeneratedDocsPath(), + 'targets.json', + ), + componentTypesPath: 'components', + hasComponentTypes: true, +}; + +// All components will be populated from StandardComponents +let allComponents = []; + +// Cache for parsed API files to avoid re-reading +const apiDefinitionsCache = {}; + +// Create type resolver for checkout's shared.ts (to resolve AnyComponent, etc.) +const checkoutSharedPath = path.join( + __dirname, + '../../../src/surfaces/checkout/shared.ts', +); +const checkoutTypeResolver = createTypeResolver(checkoutSharedPath); + +// Cache for checkout components (resolved via type resolver, excluding @private) +let checkoutComponentsCache = null; + +/** + * Parse components from checkout's shared.ts file + * Uses the shared type resolver to properly handle @private types + */ +function parseCheckoutComponents() { + // Return cached value if available + if (checkoutComponentsCache !== null) { + return checkoutComponentsCache; + } + + try { + // Use the type resolver to get AnyComponent (which excludes @private) + checkoutComponentsCache = checkoutTypeResolver.resolveType('AnyComponent'); + + if (checkoutComponentsCache.length === 0) { + // Fallback: try to get just SUPPORTED_COMPONENTS + const typeDefs = checkoutTypeResolver.getTypeDefinitions(); + if (Array.isArray(typeDefs['SUPPORTED_COMPONENTS'])) { + checkoutComponentsCache = [...typeDefs['SUPPORTED_COMPONENTS']]; + } else { + checkoutComponentsCache = ['[CheckoutComponentsNotFound]']; + } + } + + return checkoutComponentsCache; + } catch (error) { + checkoutComponentsCache = ['[CheckoutComponentsError]']; + return checkoutComponentsCache; + } +} + +/** + * Parse a string union type from a component file and resolve type references + * e.g., export type SmartGridComponents = 'Tile'; + * or: export type BlockExtensionComponents = StandardComponents | 'AdminBlock'; + * or: export type StandardComponents = AnyComponent | 'Avatar' | ... (with AnyComponent imported) + */ +function parseStringUnionType(filePath, componentTypesMap = {}) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Extract all quoted component names from the file (but not from import statements) + // Remove import lines first + const contentWithoutImports = content.replace(/^import.*?;$/gm, ''); + const componentNames = contentWithoutImports.match(/'([^']+)'/g); + const quotedComponents = componentNames + ? componentNames.map((name) => name.replace(/'/g, '')) + : []; + + // Check if the type references other types (like StandardComponents or AnyComponent) + // Look for patterns like: StandardComponents | 'OtherComponent' + const typeRefPattern = /export type \w+ =\s*([\s\S]*?);/; + const typeDefMatch = content.match(typeRefPattern); + + if (typeDefMatch) { + const typeDef = typeDefMatch[1]; + // Find references to other types (capitalized words that aren't in quotes) + const typeRefs = typeDef.match(/\b([A-Z]\w+(?:Components?)?)\b/g); + + if (typeRefs) { + const allComponents = [...quotedComponents]; + + for (const typeRef of typeRefs) { + // Check if this is AnyComponent (imported from checkout) + if (typeRef === 'AnyComponent') { + // Add all checkout components + const checkoutComponents = parseCheckoutComponents(); + allComponents.push(...checkoutComponents); + } + // If this type reference exists in our map, include its components + else if (componentTypesMap[typeRef]) { + allComponents.push(...componentTypesMap[typeRef]); + } + } + + // Remove duplicates + return [...new Set(allComponents)]; + } + } + + return quotedComponents.length > 0 ? quotedComponents : null; + } catch (error) { + console.error(`Error reading component file ${filePath}:`, error.message); + } + return null; +} + +/** + * Parse component types from files in the components directory + * Uses a two-pass approach: + * 1. First pass: Parse all component types that only contain quoted strings + * 2. Second pass: Parse types that reference other types (like StandardComponents) + */ +function parseComponentTypesFromFiles() { + if (!config.hasComponentTypes || !config.componentTypesPath) { + return {}; + } + + const componentsPath = path.join(config.basePath, config.componentTypesPath); + const componentTypesMap = {}; + + try { + // Look for all TypeScript files in the components directory + const files = fs.readdirSync(componentsPath); + const tsFiles = files.filter( + (file) => file.endsWith('.ts') && !file.endsWith('.d.ts'), + ); + + // First pass: Parse files with only quoted strings + for (const file of tsFiles) { + const filePath = path.join(componentsPath, file); + const componentTypeName = file.replace('.ts', ''); + + // Skip certain files + if ( + componentTypeName === 'shared' || + componentTypeName === 'components' + ) { + continue; + } + + const components = parseStringUnionType(filePath, {}); + if (components && components.length > 0) { + componentTypesMap[componentTypeName] = components; + + // If this is StandardComponents, use it as the base + if (componentTypeName === 'StandardComponents') { + allComponents = components.sort(); + } + } + } + + // Second pass: Re-parse files that might reference other types + // This allows types like BlockExtensionComponents = StandardComponents | 'AdminBlock' + for (const file of tsFiles) { + const filePath = path.join(componentsPath, file); + const componentTypeName = file.replace('.ts', ''); + + // Skip certain files + if ( + componentTypeName === 'shared' || + componentTypeName === 'components' + ) { + continue; + } + + const components = parseStringUnionType(filePath, componentTypesMap); + if (components && components.length > 0) { + componentTypesMap[componentTypeName] = components; + } + } + } catch (error) { + console.error('Error parsing component types:', error.message); + } + + return componentTypesMap; +} + +function parseTargetsFile() { + const targetsFilePath = path.join(config.basePath, 'extension-targets.ts'); + + const content = fs.readFileSync(targetsFilePath, 'utf-8'); + + // Parse component type definitions + const componentTypesMap = parseComponentTypesFromFiles(); + + const targets = {}; + + // Look for all interfaces that might contain RenderExtension or RunnableExtension targets + const interfaceNames = [ + 'RenderExtensionTargets', + 'OrderStatusExtensionTargets', + 'CustomerAccountExtensionTargets', + 'ExtensionTargets', + ]; + + for (const interfaceName of interfaceNames) { + // Try to find this interface + const regex = new RegExp( + `export interface ${interfaceName}[^{]*\\{([\\s\\S]+?)\\n\\}`, + ); + const match = content.match(regex); + + if (match) { + // Parse RenderExtension targets (have components) + if (match[1].includes('RenderExtension<')) { + parseTargetsFromInterfaceBody(match[1], targets, componentTypesMap); + } + // Parse RunnableExtension targets (no components, like should-render) + if (match[1].includes('RunnableExtension<')) { + parseRunnableTargetsFromInterfaceBody(match[1], targets); + } + } + } + + if (Object.keys(targets).length === 0) { + throw new Error('Could not find extension targets interface'); + } + + return targets; +} + +function parseTargetsFromInterfaceBody( + interfaceBody, + targets, + componentTypesMap, +) { + // Parse each target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RenderExtension<([\s\S]*?)>;/g; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + const matchStartPos = match.index; + + // Check if this target has @private in its JSDoc comment + // Look backwards from the match to find a preceding JSDoc comment + const beforeMatch = interfaceBody.substring(0, matchStartPos); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + + if (lastJsDocEnd !== -1) { + // Check if there's no other target between the JSDoc and this target + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + // If the text between JSDoc end and target is empty (or just whitespace), the JSDoc belongs to this target + if (between === '') { + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + // Skip this target if it's marked @private + if (jsDocContent.includes('@private')) { + continue; + } + } + } + } + + let renderExtensionContent = match[2].trim(); + + // Remove comments before parsing + renderExtensionContent = renderExtensionContent + .replace(/\/\/[^\n]*/g, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments + + // Split by comma to separate API and Components + const parts = splitByTopLevelComma(renderExtensionContent); + + if (parts.length >= 2) { + const apiString = parts[0].trim(); + const componentString = parts[1].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // Parse components + const components = parseComponents(componentString, componentTypesMap); + + targets[targetName] = { + components: components.sort(), + apis: apis.sort(), + }; + } + } +} + +/** + * Parse RunnableExtension targets from an interface body + * These targets don't have components (they return data, not UI) + */ +function parseRunnableTargetsFromInterfaceBody(interfaceBody, targets) { + // Parse each RunnableExtension target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RunnableExtension<([\s\S]*?)>;/g; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + const matchStartPos = match.index; + + // Check if this target has @private in its JSDoc comment + const beforeMatch = interfaceBody.substring(0, matchStartPos); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + + if (lastJsDocEnd !== -1) { + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + if (between === '') { + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + if (jsDocContent.includes('@private')) { + continue; + } + } + } + } + + let runnableExtensionContent = match[2].trim(); + + // Remove comments before parsing + runnableExtensionContent = runnableExtensionContent + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by comma to separate API and Output type + const parts = splitByTopLevelComma(runnableExtensionContent); + + if (parts.length >= 1) { + const apiString = parts[0].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // RunnableExtension targets don't have components - they return data + targets[targetName] = { + components: [], + apis: apis.sort(), + }; + } + } +} + +function getNestedApis(apiName) { + // Check if we've already parsed this API + if (apiDefinitionsCache.hasOwnProperty(apiName)) { + return apiDefinitionsCache[apiName]; + } + + // Try to find the API file in the surface's api directory + const apiDir = path.join(config.basePath, 'api'); + + if (!fs.existsSync(apiDir)) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Convert API name to potential file paths + // e.g., StandardApi -> standard-api, CartApi -> cart-api + const kebabName = apiName + .replace(/Api$/, '') + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); + + // Try multiple possible locations + const possiblePaths = [ + path.join(apiDir, `${kebabName}.ts`), + path.join(apiDir, `${kebabName}`, `${kebabName}.ts`), + path.join(apiDir, kebabName.replace(/-/g, ''), `${kebabName}.ts`), + path.join(apiDir, `${kebabName}-api`, `${kebabName}-api.ts`), + path.join(apiDir, `${kebabName}-api.ts`), + path.join(apiDir, `standard-api`, `standard-api.ts`), + ]; + + let content = null; + for (const apiFilePath of possiblePaths) { + try { + if (fs.existsSync(apiFilePath)) { + content = fs.readFileSync(apiFilePath, 'utf-8'); + break; + } + } catch (error) { + // Continue to next path + } + } + + if (!content) { + apiDefinitionsCache[apiName] = []; + return []; + } + + try { + // Find the export type definition for this API + const typeDefStartRegex = new RegExp(`export type ${apiName}[^=]*=`, 's'); + const startMatch = content.match(typeDefStartRegex); + + if (!startMatch) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Find the end position (semicolon at the correct nesting level) + let startPos = startMatch.index + startMatch[0].length; + let endPos = startPos; + let braceDepth = 0; + let angleDepth = 0; + + for (let i = startPos; i < content.length; i++) { + const char = content[i]; + + if (char === '{') braceDepth++; + else if (char === '}') braceDepth--; + else if (char === '<') angleDepth++; + else if (char === '>') angleDepth--; + else if (char === ';' && braceDepth === 0 && angleDepth === 0) { + endPos = i; + break; + } + } + + const typeDef = content.substring(startPos, endPos); + + // Extract all API names from the type definition + const nestedApis = []; + const apiMatches = typeDef.matchAll(/(\w+Api)\b/g); + + for (const apiMatch of apiMatches) { + const nestedApiName = apiMatch[1]; + // Don't include the API itself + if (nestedApiName !== apiName && !nestedApis.includes(nestedApiName)) { + nestedApis.push(nestedApiName); + } + } + + apiDefinitionsCache[apiName] = nestedApis; + return nestedApis; + } catch (error) { + // Error parsing, cache and return empty array + apiDefinitionsCache[apiName] = []; + return []; + } +} + +function parseApis(apiString) { + const apisSet = new Set(); + + // Remove any comments + apiString = apiString + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by & and extract API names + const parts = apiString + .split('&') + .map((s) => s.trim()) + .filter((s) => s); + + for (const part of parts) { + // Match API names (e.g., StandardApi<'...'> or just ApiName) + let apiName = null; + + // Extract just the type name before any generic parameters + const apiMatch = part.match(/^(\w+Api)/); + if (apiMatch) { + apiName = apiMatch[1]; + } else { + // Try to match any capitalized type name ending in Api + const generalMatch = part.match(/(\w+Api)\b/); + if (generalMatch) { + apiName = generalMatch[1]; + } + } + + if (apiName) { + // Add the API itself + apisSet.add(apiName); + + // Get nested APIs from this API (recursively) + const nestedApis = getNestedApis(apiName); + for (const nestedApi of nestedApis) { + apisSet.add(nestedApi); + // Recursively get nested APIs of nested APIs + const deepNestedApis = getNestedApis(nestedApi); + deepNestedApis.forEach((api) => apisSet.add(api)); + } + } + } + + return Array.from(apisSet); +} + +/** + * Parse a TypeScript component type expression + * Handles various patterns: type refs, unions, Exclude, AllowedComponents, etc. + */ +function parseComponents(componentString, componentTypesMap) { + // Normalize whitespace + componentString = componentString.replace(/\s+/g, ' ').trim(); + + // Handle AnyCheckoutComponentExcept<'Component1' | 'Component2'> + const checkoutExceptMatch = componentString.match( + /AnyCheckoutComponentExcept<([^>]+)>/, + ); + if (checkoutExceptMatch) { + const excludedUnion = checkoutExceptMatch[1]; + // Get all checkout components + const allCheckoutComponents = parseCheckoutComponents(); + // Parse the union of excluded components + const excludedComponents = parseUnionOfStrings(excludedUnion); + // Filter out the excluded components + return allCheckoutComponents.filter((c) => !excludedComponents.includes(c)); + } + + // Handle Exclude + const excludeMatch = componentString.match(/Exclude<(\w+),\s*'([^']+)'>/); + if (excludeMatch) { + const baseType = excludeMatch[1]; + const excluded = excludeMatch[2]; + const baseComponents = resolveComponentType(baseType, componentTypesMap); + return baseComponents.filter((c) => c !== excluded); + } + + // Handle AllowedComponents + const allowedMatch = componentString.match(/AllowedComponents<([^>]+)>/); + if (allowedMatch) { + const innerType = allowedMatch[1].trim(); + return resolveComponentType(innerType, componentTypesMap); + } + + // Check if it's a direct type reference + const result = resolveComponentType(componentString, componentTypesMap); + if (result.length > 0) { + return result; + } + + // Default to all components if we have them + if (allComponents.length > 0) { + return allComponents; + } + + return ['[Unknown]']; +} + +/** + * Parse a union of string literals (e.g., "'Image' | 'Banner'") + * Returns an array of the string values + */ +function parseUnionOfStrings(unionString) { + const components = []; + // Split by | and extract quoted strings + const parts = unionString.split('|'); + for (const part of parts) { + const trimmed = part.trim(); + const match = trimmed.match(/^'([^']+)'$/); + if (match) { + components.push(match[1]); + } + } + return components; +} + +/** + * Resolve a component type name to a list of component names + */ +function resolveComponentType(typeName, componentTypesMap) { + typeName = typeName.trim(); + + // Check if it's in our component types map + if (componentTypesMap[typeName]) { + return componentTypesMap[typeName]; + } + + // Handle special checkout types + if ( + typeName === 'AnyCheckoutComponent' || + typeName === 'AnyComponent' || + typeName === 'AnyThankYouComponent' + ) { + return parseCheckoutComponents(); + } + + // Check if it's a quoted string literal + const quotedMatch = typeName.match(/^'([^']+)'$/); + if (quotedMatch) { + return [quotedMatch[1]]; + } + + return []; +} + +function createCombinedMapping(targetsJson) { + const result = {...targetsJson}; + + // Create reverse mappings for APIs + const apiToTargets = {}; + const componentToTargets = {}; + + // Iterate through all targets + for (const [targetName, targetData] of Object.entries(targetsJson)) { + // Map APIs to targets + for (const api of targetData.apis) { + if (!apiToTargets[api]) { + apiToTargets[api] = []; + } + apiToTargets[api].push(targetName); + } + + // Map Components to targets + for (const component of targetData.components) { + if (!componentToTargets[component]) { + componentToTargets[component] = []; + } + componentToTargets[component].push(targetName); + } + } + + // Add API reverse mappings to result + for (const [api, targets] of Object.entries(apiToTargets)) { + result[api] = { + targets: targets.sort(), + }; + } + + // Add Component reverse mappings to result + for (const [component, targets] of Object.entries(componentToTargets)) { + result[component] = { + targets: targets.sort(), + }; + } + + return result; +} + +// Main execution +try { + console.log('\nšŸ” Generating targets JSON for admin surface'); + console.log(`šŸ“ Base path: ${config.basePath}`); + + // Generate the JSON + const targetsJson = parseTargetsFile(); + + // Create the combined JSON with reverse mappings + const combinedJson = createCombinedMapping(targetsJson); + + // Write to output file + const outputDir = path.dirname(config.outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + fs.writeFileSync(config.outputPath, JSON.stringify(combinedJson, null, 2)); + + console.log('āœ… Generated combined targets JSON at:', config.outputPath); + + // Count the different types of entries + const targetEntries = Object.keys(targetsJson).length; + const apiEntries = Object.keys(combinedJson).filter( + (key) => + combinedJson[key].targets && !targetsJson[key] && key.endsWith('Api'), + ).length; + const componentEntries = Object.keys(combinedJson).filter( + (key) => + combinedJson[key].targets && !targetsJson[key] && !key.endsWith('Api'), + ).length; + + console.log('\nšŸ“‹ Summary:'); + console.log(` Extension targets: ${targetEntries}`); + console.log(` API reverse mappings: ${apiEntries}`); + console.log(` Component reverse mappings: ${componentEntries}`); + console.log(` Total entries in JSON: ${Object.keys(combinedJson).length}`); +} catch (error) { + console.error('āŒ Error generating targets JSON:', error.message); + console.error(error.stack); + process.exit(1); +} diff --git a/packages/ui-extensions/docs/surfaces/admin/build-docs.mjs b/packages/ui-extensions/docs/surfaces/admin/build-docs.mjs index 659595c780..c67ab106d5 100644 --- a/packages/ui-extensions/docs/surfaces/admin/build-docs.mjs +++ b/packages/ui-extensions/docs/surfaces/admin/build-docs.mjs @@ -26,7 +26,7 @@ const generatedDocsPath = path.join(docsPath, 'generated'); const shopifyDevPath = path.join(rootPath, '../../../shopify-dev'); const shopifyDevDBPath = path.join( shopifyDevPath, - 'db/data/docs/templated_apis', + 'areas/platforms/shopify-dev/db/data/docs/templated_apis', ); const shopifyDevExists = existsSync(shopifyDevPath); @@ -501,6 +501,20 @@ try { }); await generateExtensionsDocs(); await generateAppBridgeDocs(); + + // Generate targets.json + console.log('Generating targets.json...'); + try { + const {execSync} = await import('child_process'); + execSync(`node ${path.join(docsPath, 'build-docs-targets-json.mjs')}`, { + stdio: 'inherit', + cwd: rootPath, + }); + console.log('āœ… Generated targets.json'); + } catch (targetsError) { + console.warn('Warning: Failed to generate targets.json:', targetsError.message); + } + await copyGeneratedToShopifyDev({ generatedDocsPath, shopifyDevPath, @@ -511,7 +525,7 @@ try { path.join(docsPath, 'screenshots'), path.join( shopifyDevPath, - 'react-app/public/images/templated-apis-screenshots/admin', + 'areas/platforms/shopify-dev/content/assets/images/templated-apis-screenshots/admin', ), {recursive: true}, ); diff --git a/packages/ui-extensions/docs/surfaces/checkout/build-docs-fast.sh b/packages/ui-extensions/docs/surfaces/checkout/build-docs-fast.sh index c378a2b8e0..98d6488c35 100755 --- a/packages/ui-extensions/docs/surfaces/checkout/build-docs-fast.sh +++ b/packages/ui-extensions/docs/surfaces/checkout/build-docs-fast.sh @@ -39,12 +39,20 @@ if [ $build_exit -ne 0 ]; then exit $build_exit fi +# Generate targets.json +echo "Generating targets.json..." +node $DOCS_PATH/build-docs-targets-json.mjs +targets_exit=$? +if [ $targets_exit -ne 0 ]; then + echo "Warning: Failed to generate targets.json" +fi + # Copy generated docs to shopify-dev copy_generated_docs_to_shopify_dev() { if [ -d $SHOPIFY_DEV_PATH ]; then - mkdir -p $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION - cp ./$DOCS_PATH/generated/* $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION - echo "āœ“ Copied docs to shopify-dev: $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION" + mkdir -p $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION + cp ./$DOCS_PATH/generated/* $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION + echo "āœ“ Copied docs to shopify-dev: $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION" else echo "Not copying docs to shopify-dev because it was not found at $SHOPIFY_DEV_PATH." fi diff --git a/packages/ui-extensions/docs/surfaces/checkout/build-docs-targets-json.mjs b/packages/ui-extensions/docs/surfaces/checkout/build-docs-targets-json.mjs new file mode 100644 index 0000000000..bdf30a143f --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/checkout/build-docs-targets-json.mjs @@ -0,0 +1,524 @@ +import fs from 'fs'; +import path from 'path'; +import {fileURLToPath} from 'url'; +import { + createTypeResolver, + splitByTopLevelComma, +} from '../../shared/build-docs-type-resolver.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Find the generated_docs_data.json file to determine output location +function findGeneratedDocsPath() { + const generatedDir = path.join(__dirname, 'generated'); + + // Look for generated_docs_data.json recursively + function findFile(dir) { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + const result = findFile(fullPath); + if (result) return result; + } else if (file === 'generated_docs_data.json') { + return path.dirname(fullPath); + } + } + return null; + } + + const docsPath = findFile(generatedDir); + return docsPath || generatedDir; // Fallback to generated root if not found +} + +// Configuration for checkout surface +const config = { + basePath: path.join(__dirname, '../../../src/surfaces/checkout'), + outputPath: path.join( + findGeneratedDocsPath(), + 'targets.json', + ), + componentTypesPath: null, + hasComponentTypes: false, +}; + +// All components will be populated from SUPPORTED_COMPONENTS +const allComponents = []; + +// Cache for parsed API files to avoid re-reading +const apiDefinitionsCache = {}; + +// Create type resolver for checkout surface +const sharedTsPath = path.join(config.basePath, 'shared.ts'); +const typeResolver = createTypeResolver(sharedTsPath); + +// Expose resolver methods for use in this script +const resolveType = typeResolver.resolveType; +const resolveTypeUnfiltered = typeResolver.resolveTypeUnfiltered; +const getTypeDefinitions = typeResolver.getTypeDefinitions; + +function parseComponentTypesFromFiles() { + // Checkout doesn't have separate component type files + return {}; +} + +function parseTargetsFile() { + const targetsFilePath = path.join(config.basePath, 'extension-targets.ts'); + + const content = fs.readFileSync(targetsFilePath, 'utf-8'); + + // Parse component type definitions + const componentTypesMap = parseComponentTypesFromFiles(); + + const targets = {}; + + // Look for all interfaces that might contain RenderExtension or RunnableExtension targets + const interfaceNames = [ + 'RenderExtensionTargets', + 'RunnableExtensionTargets', + 'OrderStatusExtensionTargets', + 'CustomerAccountExtensionTargets', + 'ExtensionTargets', + ]; + + for (const interfaceName of interfaceNames) { + // Try to find this interface + const regex = new RegExp( + `export interface ${interfaceName}[^{]*\\{([\\s\\S]+?)\\n\\}`, + ); + const match = content.match(regex); + + if (match) { + // Parse RenderExtension targets (have components) + if (match[1].includes('RenderExtension<')) { + parseTargetsFromInterfaceBody(match[1], targets, componentTypesMap); + } + // Parse RunnableExtension targets (no components, like address-autocomplete) + if (match[1].includes('RunnableExtension<')) { + parseRunnableTargetsFromInterfaceBody(match[1], targets); + } + } + } + + if (Object.keys(targets).length === 0) { + throw new Error('Could not find extension targets interface'); + } + + return targets; +} + +function parseTargetsFromInterfaceBody( + interfaceBody, + targets, + componentTypesMap, +) { + // Parse each target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RenderExtension<([\s\S]*?)>;/g; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + const matchStartPos = match.index; + + // Check if this target has @private in its JSDoc comment + // Look backwards from the match to find a preceding JSDoc comment + const beforeMatch = interfaceBody.substring(0, matchStartPos); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + + if (lastJsDocEnd !== -1) { + // Check if there's no other target between the JSDoc and this target + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + // If the text between JSDoc end and target is empty (or just whitespace), the JSDoc belongs to this target + if (between === '') { + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + // Skip this target if it's marked @private + if (jsDocContent.includes('@private')) { + continue; + } + } + } + } + + let renderExtensionContent = match[2].trim(); + + // Remove comments before parsing + renderExtensionContent = renderExtensionContent + .replace(/\/\/[^\n]*/g, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments + + // Split by comma to separate API and Components + const parts = splitByTopLevelComma(renderExtensionContent); + + if (parts.length >= 2) { + const apiString = parts[0].trim(); + const componentString = parts[1].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // Parse components + const components = parseComponents(componentString, componentTypesMap); + + targets[targetName] = { + components: components.sort(), + apis: apis.sort(), + }; + } + } +} + +/** + * Parse RunnableExtension targets from an interface body + * These targets don't have components (they return data, not UI) + */ +function parseRunnableTargetsFromInterfaceBody(interfaceBody, targets) { + // Parse each RunnableExtension target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RunnableExtension<([\s\S]*?)>;/g; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + const matchStartPos = match.index; + + // Check if this target has @private in its JSDoc comment + const beforeMatch = interfaceBody.substring(0, matchStartPos); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + + if (lastJsDocEnd !== -1) { + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + if (between === '') { + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + if (jsDocContent.includes('@private')) { + continue; + } + } + } + } + + let runnableExtensionContent = match[2].trim(); + + // Remove comments before parsing + runnableExtensionContent = runnableExtensionContent + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by comma to separate API and Output type + const parts = splitByTopLevelComma(runnableExtensionContent); + + if (parts.length >= 1) { + const apiString = parts[0].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // RunnableExtension targets don't have components - they return data + targets[targetName] = { + components: [], + apis: apis.sort(), + }; + } + } +} + +function getNestedApis(apiName) { + // Check if we've already parsed this API + if (apiDefinitionsCache.hasOwnProperty(apiName)) { + return apiDefinitionsCache[apiName]; + } + + // Try to find the API file in the surface's api directory + const apiDir = path.join(config.basePath, 'api'); + + if (!fs.existsSync(apiDir)) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Convert API name to potential file paths + // e.g., StandardApi -> standard-api, CartApi -> cart-api + const kebabName = apiName + .replace(/Api$/, '') + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); + + // Try multiple possible locations + const possiblePaths = [ + path.join(apiDir, `${kebabName}.ts`), + path.join(apiDir, `${kebabName}`, `${kebabName}.ts`), + path.join(apiDir, kebabName.replace(/-/g, ''), `${kebabName}.ts`), + path.join(apiDir, `${kebabName}-api`, `${kebabName}-api.ts`), + path.join(apiDir, `${kebabName}-api.ts`), + path.join(apiDir, `standard-api`, `standard-api.ts`), + ]; + + let content = null; + for (const apiFilePath of possiblePaths) { + try { + if (fs.existsSync(apiFilePath)) { + content = fs.readFileSync(apiFilePath, 'utf-8'); + break; + } + } catch (error) { + // Continue to next path + } + } + + if (!content) { + apiDefinitionsCache[apiName] = []; + return []; + } + + try { + // Find the export type definition for this API + const typeDefStartRegex = new RegExp(`export type ${apiName}[^=]*=`, 's'); + const startMatch = content.match(typeDefStartRegex); + + if (!startMatch) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Find the end position (semicolon at the correct nesting level) + const startPos = startMatch.index + startMatch[0].length; + let endPos = startPos; + let braceDepth = 0; + let angleDepth = 0; + + for (let i = startPos; i < content.length; i++) { + const char = content[i]; + + if (char === '{') braceDepth++; + else if (char === '}') braceDepth--; + else if (char === '<') angleDepth++; + else if (char === '>') angleDepth--; + else if (char === ';' && braceDepth === 0 && angleDepth === 0) { + endPos = i; + break; + } + } + + const typeDef = content.substring(startPos, endPos); + + // Extract all API names from the type definition + const nestedApis = []; + const apiMatches = typeDef.matchAll(/(\w+Api)\b/g); + + for (const apiMatch of apiMatches) { + const nestedApiName = apiMatch[1]; + // Don't include the API itself + if (nestedApiName !== apiName && !nestedApis.includes(nestedApiName)) { + nestedApis.push(nestedApiName); + } + } + + apiDefinitionsCache[apiName] = nestedApis; + return nestedApis; + } catch (error) { + // Error parsing, cache and return empty array + apiDefinitionsCache[apiName] = []; + return []; + } +} + +function parseApis(apiString) { + const apisSet = new Set(); + + // Remove any comments + apiString = apiString + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by & and extract API names + const parts = apiString + .split('&') + .map((s) => s.trim()) + .filter((s) => s); + + for (const part of parts) { + // Match API names (e.g., StandardApi<'...'> or just ApiName) + let apiName = null; + + // Extract just the type name before any generic parameters + const apiMatch = part.match(/^(\w+Api)/); + if (apiMatch) { + apiName = apiMatch[1]; + } else { + // Try to match any capitalized type name ending in Api + const generalMatch = part.match(/(\w+Api)\b/); + if (generalMatch) { + apiName = generalMatch[1]; + } + } + + if (apiName) { + // Add the API itself + apisSet.add(apiName); + + // Get nested APIs from this API (recursively) + const nestedApis = getNestedApis(apiName); + for (const nestedApi of nestedApis) { + apisSet.add(nestedApi); + // Recursively get nested APIs of nested APIs + const deepNestedApis = getNestedApis(nestedApi); + deepNestedApis.forEach((api) => apisSet.add(api)); + } + } + } + + return Array.from(apisSet); +} + +/** + * Parse a TypeScript component type expression + * Uses the generic resolveType() to handle any type expression dynamically + */ +function parseComponents(componentString, componentTypesMap) { + // Normalize whitespace + componentString = componentString.replace(/\s+/g, ' ').trim(); + + // Handle generic wrapper types like AnyCheckoutComponentExcept<'X' | 'Y'> + // These are defined as: type AnyCheckoutComponentExcept = Exclude + // We need to expand them to their actual Exclude form + const genericExceptMatch = componentString.match(/^(\w+Except)<([^>]+)>$/); + if (genericExceptMatch) { + const genericTypeName = genericExceptMatch[1]; + const typeArg = genericExceptMatch[2]; + + // Look up the generic type definition to find the base type + const typeDefs = getTypeDefinitions(); + const genericDef = typeDefs[genericTypeName]; + + if (genericDef) { + // Parse the generic definition to extract the base type from Exclude + const baseTypeMatch = genericDef.match(/Exclude<(\w+),\s*Except>/); + if (baseTypeMatch) { + const baseTypeName = baseTypeMatch[1]; + // Resolve as Exclude + const baseComponents = resolveType(baseTypeName); + const excludedComponents = resolveType(typeArg); + return baseComponents.filter((c) => !excludedComponents.includes(c)); + } + } + } + + // Handle AllowedComponents - it's just an identity wrapper + // BUT: AllowedComponents explicitly allows components, so don't filter private + // This is how targets like purchase.checkout.chat.render explicitly allow 'Chat' + const allowedMatch = componentString.match(/AllowedComponents<([^>]+)>/); + if (allowedMatch) { + const innerType = allowedMatch[1].trim(); + // Use unfiltered resolution since AllowedComponents explicitly allows these + return resolveTypeUnfiltered(innerType); + } + + // Use the generic resolver for everything else + // This handles: type references, Exclude<>, unions, string literals, etc. + const result = resolveType(componentString); + if (result.length > 0) { + return result; + } + + // Default to all components if we have them + if (allComponents.length > 0) { + return allComponents; + } + + return ['[Unknown]']; +} + +function createCombinedMapping(targetsJson) { + const result = {...targetsJson}; + + // Create reverse mappings for APIs + const apiToTargets = {}; + const componentToTargets = {}; + + // Iterate through all targets + for (const [targetName, targetData] of Object.entries(targetsJson)) { + // Map APIs to targets + for (const api of targetData.apis) { + if (!apiToTargets[api]) { + apiToTargets[api] = []; + } + apiToTargets[api].push(targetName); + } + + // Map Components to targets + for (const component of targetData.components) { + if (!componentToTargets[component]) { + componentToTargets[component] = []; + } + componentToTargets[component].push(targetName); + } + } + + // Add API reverse mappings to result + for (const [api, targets] of Object.entries(apiToTargets)) { + result[api] = { + targets: targets.sort(), + }; + } + + // Add Component reverse mappings to result + for (const [component, targets] of Object.entries(componentToTargets)) { + result[component] = { + targets: targets.sort(), + }; + } + + return result; +} + +// Main execution +try { + console.log('\nšŸ” Generating targets JSON for checkout surface'); + console.log(`šŸ“ Base path: ${config.basePath}`); + + // Generate the JSON + const targetsJson = parseTargetsFile(); + + // Create the combined JSON with reverse mappings + const combinedJson = createCombinedMapping(targetsJson); + + // Write to output file + const outputDir = path.dirname(config.outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + fs.writeFileSync(config.outputPath, JSON.stringify(combinedJson, null, 2)); + + console.log('āœ… Generated combined targets JSON at:', config.outputPath); + + // Count the different types of entries + const targetEntries = Object.keys(targetsJson).length; + const apiEntries = Object.keys(combinedJson).filter( + (key) => + combinedJson[key].targets && !targetsJson[key] && key.endsWith('Api'), + ).length; + const componentEntries = Object.keys(combinedJson).filter( + (key) => + combinedJson[key].targets && !targetsJson[key] && !key.endsWith('Api'), + ).length; + + console.log('\nšŸ“‹ Summary:'); + console.log(` Extension targets: ${targetEntries}`); + console.log(` API reverse mappings: ${apiEntries}`); + console.log(` Component reverse mappings: ${componentEntries}`); + console.log(` Total entries in JSON: ${Object.keys(combinedJson).length}`); +} catch (error) { + console.error('āŒ Error generating targets JSON:', error.message); + console.error(error.stack); + process.exit(1); +} diff --git a/packages/ui-extensions/docs/surfaces/checkout/build-docs-watch.sh b/packages/ui-extensions/docs/surfaces/checkout/build-docs-watch.sh index 31a041e011..9a6b71f7c0 100755 --- a/packages/ui-extensions/docs/surfaces/checkout/build-docs-watch.sh +++ b/packages/ui-extensions/docs/surfaces/checkout/build-docs-watch.sh @@ -40,11 +40,19 @@ build_docs() { return $build_exit fi + # Generate targets.json + echo "Generating targets.json..." + node $DOCS_PATH/build-docs-targets-json.mjs + targets_exit=$? + if [ $targets_exit -ne 0 ]; then + echo "Warning: Failed to generate targets.json" + fi + # Copy generated docs to shopify-dev copy_generated_docs_to_shopify_dev() { if [ -d $SHOPIFY_DEV_PATH ]; then - mkdir -p $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION - cp ./$DOCS_PATH/generated/* $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION + mkdir -p $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION + cp ./$DOCS_PATH/generated/* $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION echo "āœ“ Copied docs to shopify-dev" fi } diff --git a/packages/ui-extensions/docs/surfaces/checkout/build-docs.sh b/packages/ui-extensions/docs/surfaces/checkout/build-docs.sh index cec2db4796..f664384821 100644 --- a/packages/ui-extensions/docs/surfaces/checkout/build-docs.sh +++ b/packages/ui-extensions/docs/surfaces/checkout/build-docs.sh @@ -95,25 +95,33 @@ if [ $sed_exit -ne 0 ]; then fail_and_exit $sed_exit fi +# Generate targets.json +echo "Generating targets.json..." +node $DOCS_PATH/build-docs-targets-json.mjs +targets_exit=$? +if [ $targets_exit -ne 0 ]; then + echo "Warning: Failed to generate targets.json" +fi + copy_generated_docs_to_shopify_dev() { # Copy the generated docs to shopify-dev if [ -d $SHOPIFY_DEV_PATH ]; then - mkdir -p $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION - cp ./$DOCS_PATH/generated/* $SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION + mkdir -p $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION + cp ./$DOCS_PATH/generated/* $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION # Replace 'latest' with the exact API version in relative doc links for file in generated_docs_data.json generated_static_pages.json; do run_sed \ "s/\/docs\/api\/checkout-ui-extensions\/latest/\/docs\/api\/checkout-ui-extensions\/$API_VERSION/gi" \ - "$SHOPIFY_DEV_PATH/db/data/docs/templated_apis/checkout_extensions/$API_VERSION/$file" + "$SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/db/data/docs/templated_apis/checkout_extensions/$API_VERSION/$file" sed_exit=$? if [ $sed_exit -ne 0 ]; then fail_and_exit $sed_exit fi done - rsync -a --delete ./$DOCS_PATH/screenshots/ $SHOPIFY_DEV_PATH/react-app/public/images/templated-apis-screenshots/checkout-ui-extensions/$API_VERSION + rsync -a --delete ./$DOCS_PATH/screenshots/ $SHOPIFY_DEV_PATH/areas/platforms/shopify-dev/content/assets/images/templated-apis-screenshots/checkout-ui-extensions/$API_VERSION echo "Docs: https://shopify-dev.shop.dev/docs/api/checkout-ui-extensions" else echo "Not copying docs to shopify-dev because it was not found at $SHOPIFY_DEV_PATH." diff --git a/packages/ui-extensions/docs/surfaces/checkout/reference/apis/metafields.doc.ts b/packages/ui-extensions/docs/surfaces/checkout/reference/apis/metafields.doc.ts index 224d7fcc39..02fed0fdae 100644 --- a/packages/ui-extensions/docs/surfaces/checkout/reference/apis/metafields.doc.ts +++ b/packages/ui-extensions/docs/surfaces/checkout/reference/apis/metafields.doc.ts @@ -29,13 +29,14 @@ const data: ReferenceEntityTemplateSchema = { }, { title: 'useMetafield', - description: 'Returns a single filtered `Metafield` or `undefined`.', + description: + 'Returns a single filtered `Metafield` or `undefined`.\n > Caution: `useMetafield` is deprecated. Use `useAppMetafields` with cart metafields instead.', type: 'UseMetafieldGeneratedType', }, { title: 'useMetafields', description: - 'Returns the current array of `metafields` applied to the checkout. You can optionally filter the list.', + 'Returns the current array of `metafields` applied to the checkout. You can optionally filter the list.\n > Caution: `useMetafields` is deprecated. Use `useAppMetafields` with cart metafields instead.', type: 'UseMetafieldsGeneratedType', }, { diff --git a/packages/ui-extensions/docs/surfaces/customer-account/build-docs-targets-json.mjs b/packages/ui-extensions/docs/surfaces/customer-account/build-docs-targets-json.mjs new file mode 100644 index 0000000000..9ba621a211 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/customer-account/build-docs-targets-json.mjs @@ -0,0 +1,652 @@ +import fs from 'fs'; +import path from 'path'; +import {fileURLToPath} from 'url'; +import { + createTypeResolver, + splitByTopLevelComma, +} from '../../shared/build-docs-type-resolver.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Find the generated_docs_data.json file to determine output location +function findGeneratedDocsPath() { + const generatedDir = path.join(__dirname, 'generated'); + + // Look for generated_docs_data.json recursively + function findFile(dir) { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + const result = findFile(fullPath); + if (result) return result; + } else if (file === 'generated_docs_data.json') { + return path.dirname(fullPath); + } + } + return null; + } + + const docsPath = findFile(generatedDir); + return docsPath || generatedDir; // Fallback to generated root if not found +} + +// Configuration for customer-account surface +const config = { + basePath: path.join(__dirname, '../../../src/surfaces/customer-account'), + outputPath: path.join( + findGeneratedDocsPath(), + 'targets.json', + ), + componentTypesPath: 'components', + hasComponentTypes: true, +}; + +// All components will be populated from StandardComponents +let allComponents = []; + +// Cache for parsed API files to avoid re-reading +const apiDefinitionsCache = {}; + +// Create type resolver for checkout's shared.ts (to resolve AnyComponent, etc.) +const checkoutSharedPath = path.join( + __dirname, + '../../../src/surfaces/checkout/shared.ts', +); +const checkoutTypeResolver = createTypeResolver(checkoutSharedPath); + +// Cache for checkout components (resolved via type resolver, excluding @private) +let checkoutComponentsCache = null; + +/** + * Parse components from checkout's shared.ts file + * Uses the shared type resolver to properly handle @private types + */ +function parseCheckoutComponents() { + // Return cached value if available + if (checkoutComponentsCache !== null) { + return checkoutComponentsCache; + } + + try { + // Use the type resolver to get AnyComponent (which excludes @private) + // AnyComponent = SUPPORTED_COMPONENTS + PrivateComponent + // But resolveType automatically filters out PrivateComponent because it's @private + checkoutComponentsCache = checkoutTypeResolver.resolveType('AnyComponent'); + + if (checkoutComponentsCache.length === 0) { + // Fallback: try to get just SUPPORTED_COMPONENTS + const typeDefs = checkoutTypeResolver.getTypeDefinitions(); + if (Array.isArray(typeDefs['SUPPORTED_COMPONENTS'])) { + checkoutComponentsCache = [...typeDefs['SUPPORTED_COMPONENTS']]; + } else { + checkoutComponentsCache = ['[CheckoutComponentsNotFound]']; + } + } + + return checkoutComponentsCache; + } catch (error) { + checkoutComponentsCache = ['[CheckoutComponentsError]']; + return checkoutComponentsCache; + } +} + +/** + * Parse a string union type from a component file and resolve type references + * e.g., export type StandardComponents = AnyComponent | 'Avatar' | ... (with AnyComponent imported) + */ +function parseStringUnionType(filePath, componentTypesMap = {}) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Extract all quoted component names from the file (but not from import statements) + // Remove import lines first + const contentWithoutImports = content.replace(/^import.*?;$/gm, ''); + const componentNames = contentWithoutImports.match(/'([^']+)'/g); + const quotedComponents = componentNames + ? componentNames.map((name) => name.replace(/'/g, '')) + : []; + + // Check if the type references other types (like StandardComponents or AnyComponent) + // Look for patterns like: StandardComponents | 'OtherComponent' + const typeRefPattern = /export type \w+ =\s*([\s\S]*?);/; + const typeDefMatch = content.match(typeRefPattern); + + if (typeDefMatch) { + const typeDef = typeDefMatch[1]; + // Find references to other types (capitalized words that aren't in quotes) + const typeRefs = typeDef.match(/\b([A-Z]\w+(?:Components?)?)\b/g); + + if (typeRefs) { + const allComponents = [...quotedComponents]; + + for (const typeRef of typeRefs) { + // Check if this is AnyComponent (imported from checkout) + if (typeRef === 'AnyComponent') { + // Add all checkout components + const checkoutComponents = parseCheckoutComponents(); + allComponents.push(...checkoutComponents); + } + // If this type reference exists in our map, include its components + else if (componentTypesMap[typeRef]) { + allComponents.push(...componentTypesMap[typeRef]); + } + } + + // Remove duplicates + return [...new Set(allComponents)]; + } + } + + return quotedComponents.length > 0 ? quotedComponents : null; + } catch (error) { + console.error(`Error reading component file ${filePath}:`, error.message); + } + return null; +} + +/** + * Parse component types from files in the components directory + * Uses a two-pass approach: + * 1. First pass: Parse all component types that only contain quoted strings + * 2. Second pass: Parse types that reference other types (like StandardComponents) + */ +function parseComponentTypesFromFiles() { + if (!config.hasComponentTypes || !config.componentTypesPath) { + return {}; + } + + const componentsPath = path.join(config.basePath, config.componentTypesPath); + const componentTypesMap = {}; + + try { + // Look for all TypeScript files in the components directory + const files = fs.readdirSync(componentsPath); + const tsFiles = files.filter( + (file) => file.endsWith('.ts') && !file.endsWith('.d.ts'), + ); + + // First pass: Parse files with only quoted strings + for (const file of tsFiles) { + const filePath = path.join(componentsPath, file); + const componentTypeName = file.replace('.ts', ''); + + // Skip certain files + if ( + componentTypeName === 'shared' || + componentTypeName === 'components' + ) { + continue; + } + + const components = parseStringUnionType(filePath, {}); + if (components && components.length > 0) { + componentTypesMap[componentTypeName] = components; + + // If this is StandardComponents, use it as the base + if (componentTypeName === 'StandardComponents') { + allComponents = components.sort(); + } + } + } + + // Second pass: Re-parse files that might reference other types + for (const file of tsFiles) { + const filePath = path.join(componentsPath, file); + const componentTypeName = file.replace('.ts', ''); + + // Skip certain files + if ( + componentTypeName === 'shared' || + componentTypeName === 'components' + ) { + continue; + } + + const components = parseStringUnionType(filePath, componentTypesMap); + if (components && components.length > 0) { + componentTypesMap[componentTypeName] = components; + } + } + } catch (error) { + console.error('Error parsing component types:', error.message); + } + + return componentTypesMap; +} + +function parseTargetsFile() { + const targetsFilePath = path.join(config.basePath, 'extension-targets.ts'); + + const content = fs.readFileSync(targetsFilePath, 'utf-8'); + + // Parse component type definitions + const componentTypesMap = parseComponentTypesFromFiles(); + + const targets = {}; + + // Look for all interfaces that might contain RenderExtension targets + const interfaceNames = [ + 'RenderExtensionTargets', + 'OrderStatusExtensionTargets', + 'CustomerAccountExtensionTargets', + 'ExtensionTargets', + ]; + + for (const interfaceName of interfaceNames) { + // Try to find this interface + const regex = new RegExp( + `export interface ${interfaceName}[^{]*\\{([\\s\\S]+?)\\n\\}`, + ); + const match = content.match(regex); + + if (match && match[1].includes('RenderExtension<')) { + parseTargetsFromInterfaceBody(match[1], targets, componentTypesMap); + } + } + + if (Object.keys(targets).length === 0) { + throw new Error('Could not find extension targets interface'); + } + + return targets; +} + +function parseTargetsFromInterfaceBody( + interfaceBody, + targets, + componentTypesMap, +) { + // Parse each target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RenderExtension<([\s\S]*?)>;/g; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + const matchStartPos = match.index; + + // Check if this target has @private in its JSDoc comment + // Look backwards from the match to find a preceding JSDoc comment + const beforeMatch = interfaceBody.substring(0, matchStartPos); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + + if (lastJsDocEnd !== -1) { + // Check if there's no other target between the JSDoc and this target + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + // If the text between JSDoc end and target is empty (or just whitespace), the JSDoc belongs to this target + if (between === '') { + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + // Skip this target if it's marked @private + if (jsDocContent.includes('@private')) { + continue; + } + } + } + } + + let renderExtensionContent = match[2].trim(); + + // Remove comments before parsing + renderExtensionContent = renderExtensionContent + .replace(/\/\/[^\n]*/g, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments + + // Split by comma to separate API and Components + const parts = splitByTopLevelComma(renderExtensionContent); + + if (parts.length >= 2) { + const apiString = parts[0].trim(); + const componentString = parts[1].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // Parse components + const components = parseComponents(componentString, componentTypesMap); + + targets[targetName] = { + components: components.sort(), + apis: apis.sort(), + }; + } + } +} + +function getNestedApis(apiName) { + // Check if we've already parsed this API + if (apiDefinitionsCache.hasOwnProperty(apiName)) { + return apiDefinitionsCache[apiName]; + } + + // Try to find the API file in the surface's api directory + const apiDir = path.join(config.basePath, 'api'); + + if (!fs.existsSync(apiDir)) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Convert API name to potential file paths + // e.g., StandardApi -> standard-api, CartApi -> cart-api + const kebabName = apiName + .replace(/Api$/, '') + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase(); + + // Try multiple possible locations + const possiblePaths = [ + path.join(apiDir, `${kebabName}.ts`), + path.join(apiDir, `${kebabName}`, `${kebabName}.ts`), + path.join(apiDir, kebabName.replace(/-/g, ''), `${kebabName}.ts`), + path.join(apiDir, `${kebabName}-api`, `${kebabName}-api.ts`), + path.join(apiDir, `${kebabName}-api.ts`), + path.join(apiDir, `standard-api`, `standard-api.ts`), + ]; + + let content = null; + for (const apiFilePath of possiblePaths) { + try { + if (fs.existsSync(apiFilePath)) { + content = fs.readFileSync(apiFilePath, 'utf-8'); + break; + } + } catch (error) { + // Continue to next path + } + } + + if (!content) { + apiDefinitionsCache[apiName] = []; + return []; + } + + try { + // Find the export type definition for this API + const typeDefStartRegex = new RegExp(`export type ${apiName}[^=]*=`, 's'); + const startMatch = content.match(typeDefStartRegex); + + if (!startMatch) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Find the end position (semicolon at the correct nesting level) + const startPos = startMatch.index + startMatch[0].length; + let endPos = startPos; + let braceDepth = 0; + let angleDepth = 0; + + for (let i = startPos; i < content.length; i++) { + const char = content[i]; + + if (char === '{') braceDepth++; + else if (char === '}') braceDepth--; + else if (char === '<') angleDepth++; + else if (char === '>') angleDepth--; + else if (char === ';' && braceDepth === 0 && angleDepth === 0) { + endPos = i; + break; + } + } + + const typeDef = content.substring(startPos, endPos); + + // Extract all API names from the type definition + const nestedApis = []; + const apiMatches = typeDef.matchAll(/(\w+Api)\b/g); + + for (const apiMatch of apiMatches) { + const nestedApiName = apiMatch[1]; + // Don't include the API itself + if (nestedApiName !== apiName && !nestedApis.includes(nestedApiName)) { + nestedApis.push(nestedApiName); + } + } + + apiDefinitionsCache[apiName] = nestedApis; + return nestedApis; + } catch (error) { + // Error parsing, cache and return empty array + apiDefinitionsCache[apiName] = []; + return []; + } +} + +function parseApis(apiString) { + const apisSet = new Set(); + + // Remove any comments + apiString = apiString + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by & and extract API names + const parts = apiString + .split('&') + .map((s) => s.trim()) + .filter((s) => s); + + for (const part of parts) { + // Match API names (e.g., StandardApi<'...'> or just ApiName) + let apiName = null; + + // Extract just the type name before any generic parameters + const apiMatch = part.match(/^(\w+Api)/); + if (apiMatch) { + apiName = apiMatch[1]; + } else { + // Try to match any capitalized type name ending in Api + const generalMatch = part.match(/(\w+Api)\b/); + if (generalMatch) { + apiName = generalMatch[1]; + } + } + + if (apiName) { + // Add the API itself + apisSet.add(apiName); + + // Get nested APIs from this API (recursively) + const nestedApis = getNestedApis(apiName); + for (const nestedApi of nestedApis) { + apisSet.add(nestedApi); + // Recursively get nested APIs of nested APIs + const deepNestedApis = getNestedApis(nestedApi); + deepNestedApis.forEach((api) => apisSet.add(api)); + } + } + } + + return Array.from(apisSet); +} + +/** + * Parse a TypeScript component type expression + * Handles various patterns: type refs, unions, Exclude, AllowedComponents, etc. + */ +function parseComponents(componentString, componentTypesMap) { + // Normalize whitespace + componentString = componentString.replace(/\s+/g, ' ').trim(); + + // Handle AnyCheckoutComponentExcept<'Component1' | 'Component2'> + const checkoutExceptMatch = componentString.match( + /AnyCheckoutComponentExcept<([^>]+)>/, + ); + if (checkoutExceptMatch) { + const excludedUnion = checkoutExceptMatch[1]; + // Get all checkout components + const allCheckoutComponents = parseCheckoutComponents(); + // Parse the union of excluded components + const excludedComponents = parseUnionOfStrings(excludedUnion); + // Filter out the excluded components + return allCheckoutComponents.filter((c) => !excludedComponents.includes(c)); + } + + // Handle Exclude + const excludeMatch = componentString.match(/Exclude<(\w+),\s*'([^']+)'>/); + if (excludeMatch) { + const baseType = excludeMatch[1]; + const excluded = excludeMatch[2]; + const baseComponents = resolveComponentType(baseType, componentTypesMap); + return baseComponents.filter((c) => c !== excluded); + } + + // Handle AllowedComponents + const allowedMatch = componentString.match(/AllowedComponents<([^>]+)>/); + if (allowedMatch) { + const innerType = allowedMatch[1].trim(); + return resolveComponentType(innerType, componentTypesMap); + } + + // Check if it's a direct type reference + const result = resolveComponentType(componentString, componentTypesMap); + if (result.length > 0) { + return result; + } + + // Default to all components if we have them + if (allComponents.length > 0) { + return allComponents; + } + + return ['[Unknown]']; +} + +/** + * Parse a union of string literals (e.g., "'Image' | 'Banner'") + * Returns an array of the string values + */ +function parseUnionOfStrings(unionString) { + const components = []; + // Split by | and extract quoted strings + const parts = unionString.split('|'); + for (const part of parts) { + const trimmed = part.trim(); + const match = trimmed.match(/^'([^']+)'$/); + if (match) { + components.push(match[1]); + } + } + return components; +} + +/** + * Resolve a component type name to a list of component names + */ +function resolveComponentType(typeName, componentTypesMap) { + typeName = typeName.trim(); + + // Check if it's in our component types map + if (componentTypesMap[typeName]) { + return componentTypesMap[typeName]; + } + + // Handle special checkout types + if ( + typeName === 'AnyCheckoutComponent' || + typeName === 'AnyComponent' || + typeName === 'AnyThankYouComponent' + ) { + return parseCheckoutComponents(); + } + + // Check if it's a quoted string literal + const quotedMatch = typeName.match(/^'([^']+)'$/); + if (quotedMatch) { + return [quotedMatch[1]]; + } + + return []; +} + +function createCombinedMapping(targetsJson) { + const result = {...targetsJson}; + + // Create reverse mappings for APIs + const apiToTargets = {}; + const componentToTargets = {}; + + // Iterate through all targets + for (const [targetName, targetData] of Object.entries(targetsJson)) { + // Map APIs to targets + for (const api of targetData.apis) { + if (!apiToTargets[api]) { + apiToTargets[api] = []; + } + apiToTargets[api].push(targetName); + } + + // Map Components to targets + for (const component of targetData.components) { + if (!componentToTargets[component]) { + componentToTargets[component] = []; + } + componentToTargets[component].push(targetName); + } + } + + // Add API reverse mappings to result + for (const [api, targets] of Object.entries(apiToTargets)) { + result[api] = { + targets: targets.sort(), + }; + } + + // Add Component reverse mappings to result + for (const [component, targets] of Object.entries(componentToTargets)) { + result[component] = { + targets: targets.sort(), + }; + } + + return result; +} + +// Main execution +try { + console.log('\nšŸ” Generating targets JSON for customer-account surface'); + console.log(`šŸ“ Base path: ${config.basePath}`); + + // Generate the JSON + const targetsJson = parseTargetsFile(); + + // Create the combined JSON with reverse mappings + const combinedJson = createCombinedMapping(targetsJson); + + // Write to output file + const outputDir = path.dirname(config.outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); + } + fs.writeFileSync(config.outputPath, JSON.stringify(combinedJson, null, 2)); + + console.log('āœ… Generated combined targets JSON at:', config.outputPath); + + // Count the different types of entries + const targetEntries = Object.keys(targetsJson).length; + const apiEntries = Object.keys(combinedJson).filter( + (key) => + combinedJson[key].targets && !targetsJson[key] && key.endsWith('Api'), + ).length; + const componentEntries = Object.keys(combinedJson).filter( + (key) => + combinedJson[key].targets && !targetsJson[key] && !key.endsWith('Api'), + ).length; + + console.log('\nšŸ“‹ Summary:'); + console.log(` Extension targets: ${targetEntries}`); + console.log(` API reverse mappings: ${apiEntries}`); + console.log(` Component reverse mappings: ${componentEntries}`); + console.log(` Total entries in JSON: ${Object.keys(combinedJson).length}`); +} catch (error) { + console.error('āŒ Error generating targets JSON:', error.message); + console.error(error.stack); + process.exit(1); +} diff --git a/packages/ui-extensions/docs/surfaces/customer-account/build-docs.mjs b/packages/ui-extensions/docs/surfaces/customer-account/build-docs.mjs index c25af56b5b..dcd5a80459 100644 --- a/packages/ui-extensions/docs/surfaces/customer-account/build-docs.mjs +++ b/packages/ui-extensions/docs/surfaces/customer-account/build-docs.mjs @@ -25,7 +25,7 @@ const generatedDocsPath = path.join(docsPath, 'generated'); const shopifyDevPath = path.join(rootPath, '../../../shopify-dev'); const shopifyDevDBPath = path.join( shopifyDevPath, - 'db/data/docs/templated_apis', + 'areas/platforms/shopify-dev/db/data/docs/templated_apis', ); const generatedDocsDataFile = 'generated_docs_data.json'; @@ -85,7 +85,7 @@ const generateExtensionsDocs = async () => { path.join(docsPath, 'screenshots'), path.join( shopifyDevPath, - 'react-app/public/images/templated-apis-screenshots/customer-account-ui-extensions', + 'areas/platforms/shopify-dev/content/assets/images/templated-apis-screenshots/customer-account-ui-extensions', EXTENSIONS_API_VERSION, ), {recursive: true}, @@ -103,6 +103,20 @@ try { replaceValue: 'any', }); await generateExtensionsDocs(); + + // Generate targets.json + console.log('Generating targets.json...'); + try { + const {execSync} = await import('child_process'); + execSync(`node ${path.join(docsPath, 'build-docs-targets-json.mjs')}`, { + stdio: 'inherit', + cwd: rootPath, + }); + console.log('āœ… Generated targets.json'); + } catch (targetsError) { + console.warn('Warning: Failed to generate targets.json:', targetsError.message); + } + await copyGeneratedToShopifyDev({ generatedDocsPath, shopifyDevPath, diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs-targets-json.mjs b/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs-targets-json.mjs new file mode 100644 index 0000000000..c5351389b7 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs-targets-json.mjs @@ -0,0 +1,505 @@ +/* eslint-disable no-console */ +/* eslint-env node */ +import fs from 'fs'; +import path from 'path'; +import {fileURLToPath} from 'url'; +import {splitByTopLevelComma} from '../../shared/build-docs-type-resolver.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// All POS components will be populated from StandardComponents.ts +let allComponents = []; + +// Cache for parsed API files to avoid re-reading +const apiDefinitionsCache = {}; + +/** + * Parse a string union type from a component file + * e.g., export type SmartGridComponents = 'Tile'; + * or multi-line: export type BlockExtensionComponents = 'Badge' | 'Box' | ...; + */ +function parseStringUnionType(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + // Extract all quoted component names from the file + const componentNames = content.match(/'([^']+)'/g); + if (componentNames) { + return componentNames.map((name) => name.replace(/'/g, '')); + } + } catch (error) { + console.error(`Error reading component file ${filePath}:`, error.message); + } + return null; +} + +/** + * Parse component types from the separate files in ./components/targets/ + */ +function parseComponentTypesFromFiles() { + const basePath = path.join( + __dirname, + '../../../src/surfaces/point-of-sale/components/targets', + ); + + const componentTypesMap = {}; + + // Parse SmartGridComponents + const smartGridComponents = parseStringUnionType( + path.join(basePath, 'SmartGridComponents.ts'), + ); + if (smartGridComponents) { + componentTypesMap.SmartGridComponents = smartGridComponents; + } + + // Parse ActionExtensionComponents + const actionComponents = parseStringUnionType( + path.join(basePath, 'ActionExtensionComponents.ts'), + ); + if (actionComponents) { + componentTypesMap.ActionExtensionComponents = actionComponents; + } + + // Parse BlockExtensionComponents + const blockComponents = parseStringUnionType( + path.join(basePath, 'BlockExtensionComponents.ts'), + ); + if (blockComponents) { + componentTypesMap.BlockExtensionComponents = blockComponents; + } + + // Parse ReceiptComponents + const receiptComponents = parseStringUnionType( + path.join(basePath, 'ReceiptComponents.ts'), + ); + if (receiptComponents) { + componentTypesMap.ReceiptComponents = receiptComponents; + } + + // Parse StandardComponents (for BasicComponents which excludes 'Tile') + const standardComponents = parseStringUnionType( + path.join(basePath, 'StandardComponents.ts'), + ); + if (standardComponents) { + componentTypesMap.StandardComponents = standardComponents; + // BasicComponents = Exclude + componentTypesMap.BasicComponents = standardComponents.filter( + (component) => component !== 'Tile', + ); + // Update global allComponents + allComponents = standardComponents.sort(); + } + + return componentTypesMap; +} + +function parseTargetsFile() { + const targetsFilePath = path.join( + __dirname, + '../../../src/surfaces/point-of-sale/extension-targets.ts', + ); + + const content = fs.readFileSync(targetsFilePath, 'utf-8'); + + // Parse component type definitions from the separate files + const componentTypesMap = parseComponentTypesFromFiles(); + + // Extract the RenderExtensionTargets interface + const interfaceMatch = content.match( + /export interface RenderExtensionTargets \{([\s\S]+?)\n\}/, + ); + if (!interfaceMatch) { + throw new Error('Could not find RenderExtensionTargets interface'); + } + + const interfaceBody = interfaceMatch[1]; + + // Parse each target definition (handle multi-line) + const targetRegex = /'([^']+)':\s*RenderExtension<([\s\S]*?)>;/g; + const targets = {}; + + let match; + while ((match = targetRegex.exec(interfaceBody)) !== null) { + const targetName = match[1]; + const matchStartPos = match.index; + + // Check if this target has @private in its JSDoc comment + // Look backwards from the match to find a preceding JSDoc comment + const beforeMatch = interfaceBody.substring(0, matchStartPos); + const lastJsDocEnd = beforeMatch.lastIndexOf('*/'); + + if (lastJsDocEnd !== -1) { + // Check if there's no other target between the JSDoc and this target + const between = beforeMatch.substring(lastJsDocEnd + 2).trim(); + // If the text between JSDoc end and target is empty (or just whitespace), the JSDoc belongs to this target + if (between === '') { + const jsDocStart = beforeMatch.lastIndexOf('/**'); + if (jsDocStart !== -1) { + const jsDocContent = beforeMatch.substring( + jsDocStart, + lastJsDocEnd + 2, + ); + // Skip this target if it's marked @private + if (jsDocContent.includes('@private')) { + continue; + } + } + } + } + + let renderExtensionContent = match[2].trim(); + + // Remove comments before parsing (they can contain commas that break splitting) + renderExtensionContent = renderExtensionContent + .replace(/\/\/[^\n]*/g, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments + + // Split by comma to separate API and Components (but be careful with nested <> brackets) + const parts = splitByTopLevelComma(renderExtensionContent); + + if (parts.length >= 2) { + const apiString = parts[0].trim(); + const componentString = parts[1].trim(); + + // Parse APIs from the intersection type + const apis = parseApis(apiString); + + // Parse components + const components = parseComponents(componentString, componentTypesMap); + + targets[targetName] = { + components: components.sort(), + apis: apis.sort(), + }; + } + } + + return targets; +} + +function getNestedApis(apiName) { + // Check if we've already parsed this API + if (Object.prototype.hasOwnProperty.call(apiDefinitionsCache, apiName)) { + return apiDefinitionsCache[apiName]; + } + + // Map API names to their file paths (try multiple possible locations) + const apiFilePaths = { + StandardApi: [ + './api/standard/standard-api', + './render/api/standard/standard-api', + ], + SmartGridApi: ['./api/smartgrid-api/smartgrid-api'], + ActionApi: [ + './api/action-api/action-api', + './render/api/action-api/action-api', + ], + NavigationApi: [ + './api/navigation-api/navigation-api', + './render/api/navigation-api/navigation-api', + ], + ScannerApi: [ + './api/scanner-api/scanner-api', + './render/api/scanner-api/scanner-api', + ], + CartApi: ['./api/cart-api/cart-api', './render/api/cart-api/cart-api'], + OrderApi: ['./api/order-api/order-api', './render/api/order-api/order-api'], + ConnectivityApi: [ + './api/connectivity-api/connectivity-api', + './render/api/connectivity-api/connectivity-api', + ], + DeviceApi: [ + './api/device-api/device-api', + './render/api/device-api/device-api', + ], + LocaleApi: [ + './api/locale-api/locale-api', + './render/api/locale-api/locale-api', + ], + SessionApi: [ + './api/session-api/session-api', + './render/api/session-api/session-api', + ], + ToastApi: ['./api/toast-api/toast-api', './render/api/toast-api/toast-api'], + ProductSearchApi: [ + './api/product-search-api/product-search-api', + './render/api/product-search-api/product-search-api', + ], + ActionTargetApi: [ + './api/action-target-api/action-target-api', + './render/api/action-target-api/action-target-api', + ], + ProductApi: [ + './api/product-api/product-api', + './render/api/product-api/product-api', + ], + CustomerApi: [ + './api/customer-api/customer-api', + './render/api/customer-api/customer-api', + ], + DraftOrderApi: [ + './api/draft-order-api/draft-order-api', + './render/api/draft-order-api/draft-order-api', + ], + PrintApi: ['./api/print-api/print-api', './render/api/print-api/print-api'], + StorageApi: ['./api/storage-api/storage-api'], + CartLineItemApi: ['./api/cart-line-item-api/cart-line-item-api'], + CashDrawerApi: ['./api/cash-drawer-api/cash-drawer-api'], + PinPadApi: ['./api/pin-pad-api'], + }; + + const relativePaths = apiFilePaths[apiName]; + if (!relativePaths) { + // Unknown API, cache and return empty array + apiDefinitionsCache[apiName] = []; + return []; + } + + // Try each possible path + let content = null; + + for (const relativePath of relativePaths) { + const basePath = path.join( + __dirname, + '../../../src/surfaces/point-of-sale', + relativePath, + ); + + // Try both .ts and .tsx extensions + for (const ext of ['.ts', '.tsx']) { + try { + const apiFilePath = basePath + ext; + content = fs.readFileSync(apiFilePath, 'utf-8'); + break; + } catch (error) { + // Try next extension + } + } + + if (content) { + break; + } + } + + if (!content) { + apiDefinitionsCache[apiName] = []; + return []; + } + + try { + // Find the export type definition for this API + // We need to capture everything until we find a semicolon that's not inside braces + const typeDefStartRegex = new RegExp(`export type ${apiName}[^=]*=`, 's'); + const startMatch = content.match(typeDefStartRegex); + + if (!startMatch) { + apiDefinitionsCache[apiName] = []; + return []; + } + + // Find the end position (semicolon at the correct nesting level) + const startPos = startMatch.index + startMatch[0].length; + let endPos = startPos; + let braceDepth = 0; + let angleDepth = 0; + + for (let i = startPos; i < content.length; i++) { + const char = content[i]; + + if (char === '{') braceDepth++; + else if (char === '}') braceDepth--; + else if (char === '<') angleDepth++; + else if (char === '>') angleDepth--; + else if (char === ';' && braceDepth === 0 && angleDepth === 0) { + endPos = i; + break; + } + } + + const typeDef = content.substring(startPos, endPos); + + // Extract all API names from the type definition + const nestedApis = []; + const apiMatches = typeDef.matchAll(/(\w+Api)\b/g); + + for (const apiMatch of apiMatches) { + const nestedApiName = apiMatch[1]; + // Don't include the API itself + if (nestedApiName !== apiName && !nestedApis.includes(nestedApiName)) { + nestedApis.push(nestedApiName); + } + } + + apiDefinitionsCache[apiName] = nestedApis; + return nestedApis; + } catch (error) { + // Error parsing, cache and return empty array + apiDefinitionsCache[apiName] = []; + return []; + } +} + +// APIs that are composites of other documented APIs - we list their constituent APIs, not these wrapper types +const COMPOSITE_APIS = new Set(['StandardApi', 'ActionTargetApi']); + +function parseApis(apiString) { + const apisSet = new Set(); + + // Remove any comments + const cleanedApiString = apiString + .replace(/\/\/[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Split by & and extract API names + const parts = cleanedApiString + .split('&') + .map((part) => part.trim()) + .filter((part) => part); + + for (const part of parts) { + // Match StandardApi<'...'> or just ApiName + let apiName = null; + + if (part.includes('StandardApi')) { + apiName = 'StandardApi'; + } else { + // Extract just the type name before any generic parameters + const apiMatch = part.match(/^(\w+Api)/); + if (apiMatch) { + apiName = apiMatch[1]; + } + } + + if (apiName) { + if (!COMPOSITE_APIS.has(apiName)) { + apisSet.add(apiName); + } + + const nestedApis = getNestedApis(apiName); + for (const nestedApi of nestedApis) { + if (COMPOSITE_APIS.has(nestedApi)) { + const deepNestedApis = getNestedApis(nestedApi); + deepNestedApis.forEach((api) => { + if (!COMPOSITE_APIS.has(api)) apisSet.add(api); + }); + } else { + apisSet.add(nestedApi); + const deepNestedApis = getNestedApis(nestedApi); + deepNestedApis.forEach((api) => { + if (!COMPOSITE_APIS.has(api)) apisSet.add(api); + }); + } + } + } + } + + return Array.from(apisSet); +} + +function parseComponents(componentString, componentTypesMap) { + // Remove whitespace and newlines + const cleanedComponentString = componentString.replace(/\s+/g, ' ').trim(); + + // Check if it directly references one of our parsed component types + for (const [typeName, components] of Object.entries(componentTypesMap)) { + if (cleanedComponentString.includes(typeName)) { + return components; + } + } + + // Default to all components if no match + return allComponents; +} + +function createReverseMapping(targetsJson) { + const result = {...targetsJson}; + + // Create reverse mappings for APIs + const apiToTargets = {}; + const componentToTargets = {}; + + // Iterate through all targets + for (const [targetName, targetData] of Object.entries(targetsJson)) { + // Map APIs to targets + for (const api of targetData.apis) { + if (!apiToTargets[api]) { + apiToTargets[api] = []; + } + apiToTargets[api].push(targetName); + } + + // Map Components to targets + for (const component of targetData.components) { + if (!componentToTargets[component]) { + componentToTargets[component] = []; + } + componentToTargets[component].push(targetName); + } + } + + // Add API reverse mappings to result + for (const [api, targets] of Object.entries(apiToTargets)) { + result[api] = { + targets: targets.sort(), + }; + } + + // Add Component reverse mappings to result + for (const [component, targets] of Object.entries(componentToTargets)) { + result[component] = { + targets: targets.sort(), + }; + } + + return result; +} + +// Find the generated_docs_data.json file to determine output location +function findGeneratedDocsPath() { + const generatedDir = path.join(__dirname, 'generated'); + + function findFile(dir) { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + const result = findFile(fullPath); + if (result) return result; + } else if (file === 'generated_docs_data.json') { + return path.dirname(fullPath); + } + } + return null; + } + + const docsPath = findFile(generatedDir); + return docsPath ?? generatedDir; +} + +// Generate the JSON +const targetsJson = parseTargetsFile(); + +// Create the extended JSON with reverse mappings +const extendedJson = createReverseMapping(targetsJson); + +// Write to output file +const outputPath = path.join(findGeneratedDocsPath(), 'targets.json'); +const outputDir = path.dirname(outputPath); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, {recursive: true}); +} +fs.writeFileSync(outputPath, JSON.stringify(extendedJson, null, 2)); + +console.log('āœ… Generated extended targets JSON at:', outputPath); +console.log('\nšŸ“‹ Parsed component types:'); +console.log( + ' SmartGridComponents, ActionComponents, ReceiptComponents, BlockComponents, BasicComponents', +); +console.log('\nšŸ“‹ Sample - First target:'); +const firstTarget = Object.keys(targetsJson)[0]; +console.log(JSON.stringify({[firstTarget]: targetsJson[firstTarget]}, null, 2)); +console.log('\nšŸ“‹ Sample - API reverse mapping:'); +console.log(JSON.stringify({StandardApi: extendedJson.StandardApi}, null, 2)); +console.log('\nšŸ“‹ Sample - Component reverse mapping:'); +console.log(JSON.stringify({Tile: extendedJson.Tile}, null, 2)); diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs.mjs b/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs.mjs index 9ca2577d05..2d09d92eb0 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs.mjs +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/build-docs.mjs @@ -33,7 +33,7 @@ const generatedDocsPath = path.join(docsPath, 'generated'); const shopifyDevPath = await resolveShopifyDevPath(rootPath); const shopifyDevDBPath = path.join( shopifyDevPath, - 'db/data/docs/templated_apis', + 'areas/platforms/shopify-dev/db/data/docs/templated_apis', ); const generatedDocsDataFile = 'generated_docs_data.json'; @@ -145,7 +145,7 @@ const generateExtensionsDocs = async () => { path.join(docsPath, 'screenshots'), path.join( shopifyDevPath, - 'react-app/public/images/templated-apis-screenshots/pos-ui-extensions', + 'areas/platforms/shopify-dev/content/assets/images/templated-apis-screenshots/pos-ui-extensions', EXTENSIONS_API_VERSION, ), {recursive: true}, @@ -163,6 +163,23 @@ try { replaceValue: 'any', }); await generateExtensionsDocs(); + + // Generate targets.json + console.log('Generating targets.json...'); + try { + const {execSync} = await import('child_process'); + execSync(`node ${path.join(docsPath, 'build-docs-targets-json.mjs')}`, { + stdio: 'inherit', + cwd: rootPath, + }); + console.log('āœ… Generated targets.json'); + } catch (targetsError) { + console.warn( + 'Warning: Failed to generate targets.json:', + targetsError.message, + ); + } + await copyGeneratedToShopifyDev({ generatedDocsPath, shopifyDevPath, diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/action-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/action-api.doc.ts index db66ccad7d..6c587359b8 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/action-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/action-api.doc.ts @@ -13,9 +13,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'ActionApi', + title: 'Properties', description: - 'The `ActionApi` object provides methods for presenting modal interfaces. Access these methods through `shopify.action` to launch full-screen modal experiences.', + 'The `ActionApi` object provides properties for presenting modal interfaces. Access these properties through `shopify.action` to launch full-screen modal experiences.', type: 'ActionApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/camera-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/camera-api.doc.ts index 91b47c61bb..f6e1f49532 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/camera-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/camera-api.doc.ts @@ -12,14 +12,14 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'CameraApi', + title: 'Properties', description: - 'The `CameraApi` object provides methods for capturing photos using the device camera. Access these methods through `shopify.camera` to take photos and retrieve image data with metadata.', + 'The `CameraApi` object provides properties for capturing photos using the device camera. Access these properties through `shopify.camera` to take photos and retrieve image data with metadata.', type: 'CameraApiContent', }, ], category: 'Target APIs', - subCategory: 'Standard APIs', + subCategory: 'Platform APIs', subSections: [ { type: 'Generic', diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-api.doc.ts index e48abdfd4d..afecbf80dc 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-api.doc.ts @@ -13,7 +13,7 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'CartApi', + title: 'Properties', description: 'The `CartApi` object provides access to cart management functionality and real-time cart state monitoring. Access these properties through `shopify.cart` to interact with the current POS cart.', type: 'CartApiContent', diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-line-item-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-line-item-api.doc.ts index e625f88018..f94573ec7a 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-line-item-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cart-line-item-api.doc.ts @@ -15,9 +15,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'CartLineItemApi', + title: 'Properties', description: - 'The `CartLineItemApi` object provides access to the current line item. Access this property through `api.cartLineItem` to interact with the current line item context.', + 'The `CartLineItemApi` object provides access to the current line item. Access these properties through `api.cartLineItem` to interact with the current line item context.', type: 'CartLineItemApi', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cash-drawer-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cash-drawer-api.doc.ts index 3636f28062..168e5247a7 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cash-drawer-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/cash-drawer-api.doc.ts @@ -15,9 +15,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'CashDrawerApi', + title: 'Properties', description: - 'The `CashDrawerApi` object provides methods for controlling cash drawer hardware. Access these methods through `shopify.cashDrawer` to trigger cash drawer operations.', + 'The `CashDrawerApi` object provides properties for controlling cash drawer hardware. Access these properties through `shopify.cashDrawer` to trigger cash drawer operations.', type: 'CashDrawerApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/connectivity-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/connectivity-api.doc.ts index 98660643c5..b8ca506f73 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/connectivity-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/connectivity-api.doc.ts @@ -14,7 +14,7 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'ConnectivityApi', + title: 'Properties', description: 'The `ConnectivityApi` object provides access to current connectivity information and change notifications. Access these properties through `shopify.connectivity` to monitor network status.', type: 'ConnectivityApiContent', diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/customer-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/customer-api.doc.ts index 0c2c87abba..f22da9bde1 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/customer-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/customer-api.doc.ts @@ -13,9 +13,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'CustomerApi', + title: 'Properties', description: - 'The `CustomerApi` object provides access to customer data. Access this property through `shopify.customer` to interact with the current customer context.', + 'The `CustomerApi` object provides access to customer data. Access these properties through `shopify.customer` to interact with the current customer context.', type: 'CustomerApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/device-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/device-api.doc.ts index 14f2d76ca0..c399fcf786 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/device-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/device-api.doc.ts @@ -12,9 +12,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'DeviceApi', + title: 'Properties', description: - 'The `DeviceApi` object provides access to device information and capabilities. Access these properties and methods through `shopify.device` to retrieve device details and check device characteristics.', + 'The `DeviceApi` object provides access to device information and capabilities. Access these properties through `shopify.device` to retrieve device details and check device characteristics.', type: 'DeviceApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/draft-order-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/draft-order-api.doc.ts index fcc1a4fc51..f5245de366 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/draft-order-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/draft-order-api.doc.ts @@ -15,9 +15,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'DraftOrderApi', + title: 'Properties', description: - 'The `DraftOrderApi` object provides access to draft order data. Access this property through `api.draftOrder` to interact with the current draft order context.', + 'The `DraftOrderApi` object provides access to draft order data. Access these properties through `api.draftOrder` to interact with the current draft order context.', type: 'DraftOrderApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/locale-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/locale-api.doc.ts index fdf771b3b6..7c593563eb 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/locale-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/locale-api.doc.ts @@ -12,7 +12,7 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'LocaleApi', + title: 'Properties', description: 'The `LocaleApi` object provides access to current locale information and change notifications. Access these properties through `shopify.locale` to retrieve and monitor locale data.', type: 'LocaleApiContent', diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/navigation-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/navigation-api.doc.ts index 65fa3c3776..f2a2c33209 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/navigation-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/navigation-api.doc.ts @@ -15,15 +15,15 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'NavigationApi', + title: 'Properties', description: - 'The global `navigation` object provides web-standard navigation functionality. Access these properties and methods directly through the global `navigation` object to manage navigation within modal interfaces.', + 'The global `navigation` object provides web-standard navigation functionality. Access these properties directly through the global `navigation` object to manage navigation within modal interfaces.', type: 'Navigation', }, { title: 'Window', description: - 'The global `window` object provides control over the extension screen lifecycle. Access these properties and methods directly through the global `window` object to manage the modal interface.', + 'The global `window` object provides control over the extension screen lifecycle. Access these properties directly through the global `window` object to manage the modal interface.', type: 'Window', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/order-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/order-api.doc.ts index 594ffb6eb2..296e8e52cd 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/order-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/order-api.doc.ts @@ -13,9 +13,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'OrderApi', + title: 'Properties', description: - 'The `OrderApi` object provides access to order data. Access this property through `shopify.order` to interact with the current order context.', + 'The `OrderApi` object provides access to order data. Access these properties through `shopify.order` to interact with the current order context.', type: 'OrderApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/pinpad-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/pinpad-api.doc.ts index 2d1e49cb7f..9d0a6cdf78 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/pinpad-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/pinpad-api.doc.ts @@ -12,9 +12,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'PinPadApi', + title: 'Properties', description: - 'The `PinPadApi` object provides methods for displaying secure PIN entry interfaces. Access these methods through `shopify.pinPad` to show PIN pad modals and handle PIN validation.', + 'The `PinPadApi` object provides properties for displaying secure PIN entry interfaces. Access these properties through `shopify.pinPad` to show PIN pad modals and handle PIN validation.', type: 'PinPadApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/print-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/print-api.doc.ts index 2508bd0f6d..e4abcb197b 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/print-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/print-api.doc.ts @@ -17,9 +17,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'PrintApi', + title: 'Properties', description: - 'The `PrintApi` object provides methods for triggering document printing. Access these methods through `shopify.print` to initiate print operations with various document types.', + 'The `PrintApi` object provides properties for triggering document printing. Access these properties through `shopify.print` to initiate print operations with various document types.', type: 'PrintApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-api.doc.ts index 36c452e502..7674319198 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-api.doc.ts @@ -13,9 +13,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'ProductApi', + title: 'Properties', description: - 'The `ProductApi` object provides access to product data. Access this property through `shopify.product` to interact with the current product context.', + 'The `ProductApi` object provides access to product data. Access these properties through `shopify.product` to interact with the current product context.', type: 'ProductApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-search-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-search-api.doc.ts index 4d05f08385..f7301d9863 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-search-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/product-search-api.doc.ts @@ -14,9 +14,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'ProductSearchApi', + title: 'Properties', description: - 'The `ProductSearchApi` object provides methods for searching and retrieving product information. Access these methods through `shopify.productSearch` to search products and fetch detailed product data.', + 'The `ProductSearchApi` object provides properties for searching and retrieving product information. Access these properties through `shopify.productSearch` to search products and fetch detailed product data.', type: 'ProductSearchApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/scanner-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/scanner-api.doc.ts index 6f37e14b61..996a23f466 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/scanner-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/scanner-api.doc.ts @@ -13,7 +13,7 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'ScannerApi', + title: 'Properties', description: 'The `ScannerApi` object provides access to scanning functionality and scanner source information. Access these properties through `shopify.scanner` to monitor scan events and available scanner sources.', type: 'ScannerApiContent', diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/session-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/session-api.doc.ts index ba682c7936..a73295be55 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/session-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/session-api.doc.ts @@ -12,9 +12,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'SessionApi', + title: 'Properties', description: - 'The `SessionApi` object provides access to current session information and authentication methods. Access these properties and methods through `shopify.session` to retrieve shop data and generate secure tokens. These methods enable secure API calls while maintaining user privacy and [app permissions](https://help.shopify.com/manual/your-account/users/roles/permissions/store-permissions#apps-and-channels-permissions).', + 'The `SessionApi` object provides access to current session information and authentication. Access these properties through `shopify.session` to retrieve shop data and generate secure tokens. These properties enable secure API calls while maintaining user privacy and [app permissions](https://help.shopify.com/manual/your-account/users/roles/permissions/store-permissions#apps-and-channels-permissions).', type: 'SessionApiContent', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/storage-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/storage-api.doc.ts index 9f83f3011c..933500d2be 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/storage-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/storage-api.doc.ts @@ -16,9 +16,9 @@ const data: ReferenceEntityTemplateSchema = { related: [], definitions: [ { - title: 'StorageApi', + title: 'Properties', description: - 'The `StorageApi` object provides access to persistent local storage methods for your POS UI extension. Access these methods through `shopify.storage` to store, retrieve, and manage data that persists across sessions.', + 'The `StorageApi` object provides access to persistent local storage for your POS UI extension. Access these properties through `shopify.storage` to store, retrieve, and manage data that persists across sessions.', type: 'Storage', }, ], diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/toast-api.doc.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/toast-api.doc.ts index b8b694bab6..24f3449775 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/toast-api.doc.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/apis/toast-api.doc.ts @@ -12,9 +12,9 @@ const data: ReferenceEntityTemplateSchema = { type: 'APIs', definitions: [ { - title: 'ToastApi', + title: 'Properties', description: - 'The `ToastApi` object provides methods for displaying temporary notification messages. Access these methods through `shopify.toast` to show user feedback and status updates.', + 'The `ToastApi` object provides properties for displaying temporary notification messages. Access these properties through `shopify.toast` to show user feedback and status updates.', type: 'ToastApiContent', }, ], diff --git a/packages/ui-extensions/package.json b/packages/ui-extensions/package.json index 9172c93a57..fc37e9c4de 100644 --- a/packages/ui-extensions/package.json +++ b/packages/ui-extensions/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/ui-extensions", - "version": "2026.1.0", + "version": "2026.1.2", "scripts": { "docs:admin": "node ./docs/surfaces/admin/build-docs.mjs", "docs:checkout": "bash ./docs/surfaces/checkout/build-docs.sh", diff --git a/packages/ui-extensions/src/api.ts b/packages/ui-extensions/src/api.ts index 3e6d3f48b3..5661de42c6 100644 --- a/packages/ui-extensions/src/api.ts +++ b/packages/ui-extensions/src/api.ts @@ -1,11 +1,15 @@ /** - * This defines the i18n.translate() signature. + * The translation function signature for internationalization. Use this to translate string keys defined in your locale files into localized content for the current user's language. */ export interface I18nTranslate { /** - * This returns a translated string matching a key in a locale file. + * Returns a translated string matching a key in a locale file. Use this to display localized text in your extension based on the merchant's language preferences. Supports interpolation with replacement values and pluralization with the `count` option. Returns a string when replacements are primitives, or an array when replacements include UI components. + * + * @param key - The translation key from your locale file (for example, "banner.title") + * @param options - Optional replacement values for interpolation or the special `count` property for pluralization * * @example translate("banner.title") + * @example translate("items.count", { count: 5 }) */ ( key: string, @@ -15,14 +19,16 @@ export interface I18nTranslate { : (string | ReplacementType)[]; } +/** + * Internationalization utilities for formatting and translating content according to the user's locale. Use these methods to display numbers, currency, dates, and translated strings that match the merchant's language and regional preferences. + */ export interface I18n { /** - * Returns a localized number. - * - * This function behaves like the standard `Intl.NumberFormat()` - * with a style of `decimal` applied. It uses the buyer's locale by default. + * Returns a localized number formatted according to the user's locale. Use this to display numbers like quantities, percentages, or measurements in the appropriate format for the merchant's region. This function behaves like the standard `Intl.NumberFormat()` with a style of `decimal` applied. Uses the current user's locale by default. * - * @param options.inExtensionLocale - if true, use the extension's locale + * @param number - The number to format + * @param options.inExtensionLocale - If true, use the extension's default locale instead of the user's locale + * @param options - Additional Intl.NumberFormatOptions for customizing the number format */ formatNumber: ( number: number | bigint, @@ -30,12 +36,11 @@ export interface I18n { ) => string; /** - * Returns a localized currency value. + * Returns a localized currency value formatted according to the user's locale and currency conventions. Use this to display prices, totals, or financial amounts in the appropriate format for the merchant's region. This function behaves like the standard `Intl.NumberFormat()` with a style of `currency` applied. Uses the current user's locale by default. * - * This function behaves like the standard `Intl.NumberFormat()` - * with a style of `currency` applied. It uses the buyer's locale by default. - * - * @param options.inExtensionLocale - if true, use the extension's locale + * @param number - The currency amount to format + * @param options.inExtensionLocale - If true, use the extension's default locale instead of the user's locale + * @param options - Additional Intl.NumberFormatOptions for customizing the currency format, such as the currency code */ formatCurrency: ( number: number | bigint, @@ -43,16 +48,14 @@ export interface I18n { ) => string; /** - * Returns a localized date value. + * Returns a localized date value formatted according to the user's locale and date conventions. Use this to display dates and times in the appropriate format for the merchant's region, such as order dates, timestamps, or schedule information. This function behaves like the standard `Intl.DateTimeFormat()` and uses the current user's locale by default. Formatting options can be passed to customize the date display style. * - * This function behaves like the standard `Intl.DateTimeFormatOptions()` and uses - * the buyer's locale by default. Formatting options can be passed in as - * options. + * @param date - The Date object to format + * @param options.inExtensionLocale - If true, use the extension's default locale instead of the user's locale + * @param options - Additional Intl.DateTimeFormatOptions for customizing the date format * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat0 + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options - * - * @param options.inExtensionLocale - if true, use the extension's locale */ formatDate: ( date: Date, @@ -60,13 +63,7 @@ export interface I18n { ) => string; /** - * Returns translated content in the buyer's locale, - * as supported by the extension. - * - * - `options.count` is a special numeric value used in pluralization. - * - The other option keys and values are treated as replacements for interpolation. - * - If the replacements are all primitives, then `translate()` returns a single string. - * - If replacements contain UI components, then `translate()` returns an array of elements. + * Returns translated content in the user's locale, as supported by the extension. Use this to display localized strings from your extension's locale files. The special `options.count` property enables pluralization. Other option keys and values are treated as replacements for interpolation in your translation strings. Returns a single string when replacements are primitives, or an array when replacements contain UI components. */ translate: I18nTranslate; } diff --git a/packages/ui-extensions/src/extension.ts b/packages/ui-extensions/src/extension.ts index 719294df51..b05fa1470c 100644 --- a/packages/ui-extensions/src/extension.ts +++ b/packages/ui-extensions/src/extension.ts @@ -1,10 +1,31 @@ +/** + * Defines the structure of a render extension, which displays UI in the Shopify admin. + */ export interface RenderExtension { + /** + * The API object providing access to extension capabilities, data, and methods. The specific API type depends on the extension target and determines what functionality is available to your extension, such as authentication, storage, data access, and GraphQL querying. + */ api: Api; + /** + * The set of UI components available for rendering your extension. This defines which Polaris components and custom components can be used to build your extension's interface. The available components vary by extension target. + */ components: ComponentsSet; + /** + * The render function output. Your extension's render function should return void or a Promise that resolves to void. Use this to perform any necessary setup, rendering, or async operations when your extension loads. + */ output: void | Promise; } +/** + * Defines the structure of a runnable extension, which executes logic and returns data without rendering UI. + */ export interface RunnableExtension { + /** + * The API object providing access to extension capabilities and methods. The specific API type depends on the extension target and determines what functionality is available to your extension. + */ api: Api; + /** + * The function output. Your extension function should return the expected output type or a Promise that resolves to that type. The output type is determined by your specific extension target and use case. + */ output: Output | Promise; } diff --git a/packages/ui-extensions/src/shared.ts b/packages/ui-extensions/src/shared.ts index 621a864cc9..f7f24cd395 100644 --- a/packages/ui-extensions/src/shared.ts +++ b/packages/ui-extensions/src/shared.ts @@ -1,5 +1,5 @@ /** - * Union of supported API versions + * The supported GraphQL Admin API versions. Use this to specify which API version your GraphQL queries should execute against. Each version includes specific features, bug fixes, and breaking changes. The `unstable` version provides access to the latest features but may change without notice. */ export type ApiVersion = | '2023-04' @@ -984,6 +984,6 @@ interface ValidationError { /** * A function that updates a signal and returns a result indicating success or failure. - * The function is typically used alongisde a ReadonlySignalLike object. + * The function is typically used along with a `ReadonlySignalLike` object. */ export type UpdateSignalFunction = (value: T) => Result; diff --git a/packages/ui-extensions/src/surfaces/admin/api.ts b/packages/ui-extensions/src/surfaces/admin/api.ts index 7ebd12ab0e..831c944f71 100644 --- a/packages/ui-extensions/src/surfaces/admin/api.ts +++ b/packages/ui-extensions/src/surfaces/admin/api.ts @@ -1,5 +1,6 @@ export type {I18n, I18nTranslate} from '../../api'; export type {StandardApi, Intents} from './api/standard/standard'; +export type {StandardRenderingExtensionApi} from './api/standard/standard-rendering'; export type {Navigation} from './api/block/block'; export type { CustomerSegmentTemplateApi, diff --git a/packages/ui-extensions/src/surfaces/admin/api/action/action.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/action/action.doc.ts index 5a17bef384..6ccc8b05c4 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/action/action.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/action/action.doc.ts @@ -3,19 +3,90 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Action Extension API', description: - 'This API is available to all action extension types. Refer to the [tutorial](/docs/apps/admin/admin-actions-and-blocks/build-an-admin-action) for more information. Note that the [`AdminAction`](/docs/api/admin-extensions/polaris-web-components/other/adminaction) component is required to build Admin action extensions.', + 'The Action Extension API lets you [build action extensions](/docs/apps/build/admin/actions-blocks/build-admin-action) that merchants access from the **More actions** menu on details and index pages. Use this API to create workflows for processing resources, configuring settings, or integrating with external systems.', isVisualComponent: false, type: 'API', + requires: + 'the [`AdminAction`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/settings-and-templates/adminaction) component.', + defaultExample: { + description: + 'Send selected product IDs to your backend for bulk processing. This example shows how to map selected items, make an authenticated API call, and close the modal when the operation completes.', + codeblock: { + title: 'Process selected products', + tabs: [ + { + title: 'jsx', + code: './examples/process-selected-products.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'ActionExtensionApi', - description: '', + title: 'Properties', + description: + 'The `ActionExtensionApi` object provides properties for action extensions that render in modal overlays. Access the following properties on the `ActionExtensionApi` object to interact with the current context, control the modal, and display picker dialogs.', type: 'ActionExtensionApi', }, ], + examples: { + description: + 'Examples that demonstrate how to use the Action Extension API.', + examples: [ + { + description: + 'Launch the [resource picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) to select component products for a [bundle](/docs/apps/build/product-merchandising/bundles), then save the bundle configuration to your backend. This example demonstrates opening a resource picker from within an action modal and handling the selection result.', + codeblock: { + title: 'Select additional resources', + tabs: [ + { + title: 'jsx', + code: './examples/select-additional-resources.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Fulfill an order through your app backend with proper error handling. This example uses `try-catch` blocks to catch errors and displays error messages when your backend fulfillment service fails.', + codeblock: { + title: 'Fulfill order with error handling', + tabs: [ + { + title: 'jsx', + code: './examples/handle-errors.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Core APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Check array length for bulk operations:** When actions appear on index pages with bulk selection, `api.data.selected` can contain multiple resources. Check the array length and handle batch operations accordingly.\n' + + "- **Use `loading` state on buttons:** Modal actions don't show loading indicators automatically. Use the `loading` prop on [`Button`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/actions/button) components during async operations to prevent duplicate submissions.", + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + '- Action extensions must call `api.close()` to dismiss the modal. Modal actions remain open indefinitely until explicitly closed.\n' + + "- Modal overlays can't be resized. The modal dimensions are fixed by the Shopify admin.\n" + + "- Action extensions can't modify the page layout underneath the modal or persist UI after closing.\n" + + "- Multiple modals can't be stacked. Opening a [picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/picker-api) or [intent](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/intents-api) closes the current modal context temporarily.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/action/action.ts b/packages/ui-extensions/src/surfaces/admin/api/action/action.ts index 12fae510fc..a4c5c66694 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/action/action.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/action/action.ts @@ -1,28 +1,19 @@ -import type {StandardApi} from '../standard/standard'; +import type {StandardRenderingExtensionApi} from '../standard/standard-rendering'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import type {Data} from '../shared'; -import type {ResourcePickerApi} from '../resource-picker/resource-picker'; -import type {PickerApi} from '../picker/picker'; +/** + * The `ActionExtensionApi` object provides methods for action extensions that render in modal overlays. Access the following properties on the `ActionExtensionApi` object to interact with the current context, control the modal, and display picker dialogs. + */ export interface ActionExtensionApi - extends StandardApi { + extends StandardRenderingExtensionApi { /** - * Closes the extension. Calling this method is equivalent to the merchant clicking the "x" in the corner of the overlay. + * Closes the extension modal. Use this when your extension completes its task or the merchant wants to exit. Equivalent to clicking the close button in the overlay corner. */ close: () => void; /** - * Information about the currently viewed or selected items. + * An array of currently viewed or selected resource identifiers. Use this to access the IDs of items in the current context, such as selected products in an index page or the product being viewed on a details page. The available IDs depend on the extension target and user interactions. */ data: Data; - - /** - * Renders the [Resource Picker](resource-picker), allowing users to select a resource for the extension to use as part of its flow. - */ - resourcePicker: ResourcePickerApi; - - /** - * Renders a custom [Picker](picker) dialog allowing users to select values from a list. - */ - picker: PickerApi; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/action/examples/handle-errors.jsx b/packages/ui-extensions/src/surfaces/admin/api/action/examples/handle-errors.jsx new file mode 100644 index 0000000000..5b26475298 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/action/examples/handle-errors.jsx @@ -0,0 +1,47 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleFulfill = async () => { + setLoading(true); + setError(null); + + try { + const orderId = data.selected[0]?.id; + + const response = await fetch(`/api/orders/${orderId}/fulfill`, { + method: 'POST', + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'Fulfillment failed'); + } + + console.log('Order fulfilled:', result); + shopify.close(); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( + + {error && {error}} + + {loading ? 'Fulfilling...' : 'Fulfill Order'} + + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/action/examples/process-selected-products.jsx b/packages/ui-extensions/src/surfaces/admin/api/action/examples/process-selected-products.jsx new file mode 100644 index 0000000000..b13ca93b92 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/action/examples/process-selected-products.jsx @@ -0,0 +1,33 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + + const handleProcess = async () => { + const productIds = data.selected.map((item) => item.id); + + const response = await fetch('/api/bulk-process', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({productIds}), + }); + + if (response.ok) { + console.log('Products processed successfully'); + shopify.close(); + } else { + console.error('Failed to process products'); + } + }; + + return ( + + Processing {data.selected.length} products + Process Products + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/action/examples/select-additional-resources.jsx b/packages/ui-extensions/src/surfaces/admin/api/action/examples/select-additional-resources.jsx new file mode 100644 index 0000000000..974024ea79 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/action/examples/select-additional-resources.jsx @@ -0,0 +1,46 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [selected, setSelected] = useState(null); + + const currentProductId = data.selected[0]?.id; + + const handleSelectProducts = async () => { + const selectedProducts = await shopify.resourcePicker({ + type: 'product', + multiple: 5, + action: 'select', + }); + + if (selectedProducts) { + setSelected(selectedProducts); + + await fetch('/api/create-bundle', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + mainProduct: currentProductId, + components: selectedProducts.map((p) => p.id), + }), + }); + + shopify.close(); + } + }; + + return ( + + Main product: {currentProductId} + + Select Component Products + + {selected && Selected {selected.length} products} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/block/block.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/block/block.doc.ts index 11bc07fd0b..a656b60641 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/block/block.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/block/block.doc.ts @@ -3,19 +3,88 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Block Extension API', description: - 'This API is available to all block extension types. Refer to the [tutorial](/docs/apps/admin/admin-actions-and-blocks/build-an-admin-block) for more information.', + 'The Block Extension API lets you [build block extensions](/docs/apps/build/admin/actions-blocks/build-admin-block) that display inline content directly within admin pages. Use this API to show contextual information, tools, or actions related to the current page without requiring merchants to open a modal.', isVisualComponent: false, type: 'API', + requires: + 'the [`AdminBlock`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/settings-and-templates/adminblock) component.', + defaultExample: { + description: + "Fetch and display a product's title, inventory, and status in a [block extension](/docs/api/admin-extensions/{API_VERSION}#building-your-extension). This example uses `useEffect` to query the [GraphQL Admin API](/docs/api/admin-graphql/) when the page loads and renders a loading [`Spinner`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/feedback-and-status-indicators/spinner) component while fetching.", + codeblock: { + title: 'Display product information', + tabs: [ + { + title: 'jsx', + code: './examples/display-product-info.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'BlockExtensionApi', - description: '', + title: 'Properties', + description: + 'The `BlockExtensionApi` object provides properties for block extensions that render inline content on admin pages. Access the following properties on the `BlockExtensionApi` object to interact with the current context, navigate to other extensions, and display picker dialogs.', type: 'BlockExtensionApi', }, ], + examples: { + description: 'Common block extension patterns', + examples: [ + { + description: + 'Check product eligibility with your backend API before launching an action extension. This example validates that the product meets criteria, shows a loading state during the check, and conditionally displays a navigation [button](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/actions/button).', + codeblock: { + title: 'Navigate to action extension', + tabs: [ + { + title: 'jsx', + code: './examples/navigate-to-action.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Open the [resource picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) to select related products, then save the associations to your backend. This example tracks selection count and shows feedback when relationships are created.', + codeblock: { + title: 'Select related products', + tabs: [ + { + title: 'jsx', + code: './examples/select-related-products.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Core APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Test layouts at narrow widths:** Block extensions render in responsive containers that resize with browser width. Test down to ~300px where blocks stack vertically to ensure your UI remains usable.\n' + + '- **Defer expensive operations until user interaction:** Blocks render immediately when pages load. Defer expensive operations until user interaction to avoid slowing down page rendering for merchants.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Block extensions share horizontal space with other blocks and must adapt to variable container widths. Placement order is determined by Shopify and can't be configured.\n" + + "- Navigation is limited to action extensions on the same resource page. You can't navigate to detail pages of other resources or to index pages.\n" + + "- Block extensions don't have access to information about other extensions on the page and can't communicate with them.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/block/block.ts b/packages/ui-extensions/src/surfaces/admin/api/block/block.ts index d7737a8858..7e59868e67 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/block/block.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/block/block.ts @@ -1,38 +1,32 @@ -import type {StandardApi} from '../standard/standard'; +import type {StandardRenderingExtensionApi} from '../standard/standard-rendering'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import type {Data} from '../shared'; -import type {ResourcePickerApi} from '../resource-picker/resource-picker'; -import type {PickerApi} from '../picker/picker'; +/** + * The `Navigation` object provides methods for navigating between extensions and admin pages. + */ export interface Navigation { /** - * Navigate to a specific route. + * Navigates to a specific extension or admin route. Currently supports navigating from a block extension to an action extension on the same resource page. * + * @param url - The destination URL, typically in the format 'extension://extension-handle' for other extensions * @example navigation.navigate('extension://my-admin-action-extension-handle') */ navigate: (url: string | URL) => void; } +/** + * The `BlockExtensionApi` object provides methods for block extensions that render inline content on admin pages. Access the following properties on the `BlockExtensionApi` object to interact with the current context, navigate to other extensions, and display picker dialogs. + */ export interface BlockExtensionApi - extends StandardApi { + extends StandardRenderingExtensionApi { /** - * Information about the currently viewed or selected items. + * An array of currently viewed or selected resource identifiers. Use this to access the IDs of items in the current context, such as selected products in an index page or the product being viewed on a details page. The available IDs depend on the extension target and user interactions. */ data: Data; /** - * Provides methods to navigate to other features in the Admin. Currently, only navigation from an admin block to an admin action extension *on the same resource page* is supported. - * For example, you can navigate from an admin block on the product details page (`admin.product-details.block.render`) to an admin action on the product details page (`admin.product-details.action.render`). + * Navigates to other extensions or admin pages. Currently supports navigation from a block to an action extension on the same resource page. For example, navigate from a product details block (`admin.product-details.block.render`) to a product details action (`admin.product-details.action.render`). */ navigation: Navigation; - - /** - * Renders the [Resource Picker](resource-picker), allowing users to select a resource for the extension to use as part of its flow. - */ - resourcePicker: ResourcePickerApi; - - /** - * Renders a custom [Picker](picker) dialog allowing users to select values from a list. - */ - picker: PickerApi; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/block/examples/display-product-info.jsx b/packages/ui-extensions/src/surfaces/admin/api/block/examples/display-product-info.jsx new file mode 100644 index 0000000000..9cceab82b6 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/block/examples/display-product-info.jsx @@ -0,0 +1,46 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [product, setProduct] = useState(null); + + useEffect(() => { + const fetchProduct = async () => { + const productId = data.selected[0]?.id; + + const {data: productData} = await shopify.query( + `query GetProduct($id: ID!) { + product(id: $id) { + title + totalInventory + status + } + }`, + {variables: {id: productId}}, + ); + + setProduct(productData.product); + }; + + fetchProduct(); + }, [data]); + + return ( + + {product ? ( + <> + Title: {product.title} + Inventory: {product.totalInventory} + Status: {product.status} + + ) : ( + + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/block/examples/navigate-to-action.jsx b/packages/ui-extensions/src/surfaces/admin/api/block/examples/navigate-to-action.jsx new file mode 100644 index 0000000000..a74e967a5b --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/block/examples/navigate-to-action.jsx @@ -0,0 +1,50 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [eligible, setEligible] = useState(false); + const [checking, setChecking] = useState(true); + + useEffect(() => { + const checkEligibility = async () => { + const productId = data.selected[0]?.id; + + if (!productId) { + setChecking(false); + return; + } + + const response = await fetch(`/api/products/${productId}/check-eligibility`, { + method: 'GET', + headers: {'Content-Type': 'application/json'}, + }); + + const {eligible} = await response.json(); + setEligible(eligible); + setChecking(false); + }; + + checkEligibility(); + }, [data]); + + const handleNavigate = () => { + shopify.navigation.navigate('extension://my-product-action-extension-handle'); + }; + + return ( + + {checking ? ( + + ) : eligible ? ( + Launch Advanced Workflow + ) : ( + Product not eligible for advanced actions + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/block/examples/select-related-products.jsx b/packages/ui-extensions/src/surfaces/admin/api/block/examples/select-related-products.jsx new file mode 100644 index 0000000000..739921b4fe --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/block/examples/select-related-products.jsx @@ -0,0 +1,46 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [relatedCount, setRelatedCount] = useState(0); + + const currentProductId = data.selected[0]?.id; + + const handleSelectRelated = async () => { + const selectedProducts = await shopify.resourcePicker({ + type: 'product', + multiple: true, + filter: { + hidden: false, + draft: false, + }, + }); + + if (selectedProducts) { + await fetch('/api/product-recommendations', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + productId: currentProductId, + relatedProducts: selectedProducts.map((p) => p.id), + }), + }); + + setRelatedCount(selectedProducts.length); + } + }; + + return ( + + Select Related Products + {relatedCount > 0 && ( + Added {relatedCount} related products + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/configure-shipping-restrictions.jsx b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/configure-shipping-restrictions.jsx new file mode 100644 index 0000000000..74a4e5df0f --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/configure-shipping-restrictions.jsx @@ -0,0 +1,52 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [countries, setCountries] = useState('US, CA, GB'); + const [errorMsg, setErrorMsg] = useState('Shipping not available to your location'); + + const handleSave = async () => { + const blockedCountries = countries.split(',').map((c) => c.trim()); + + await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'validation', + key: 'blocked_shipping_countries', + value: JSON.stringify(blockedCountries), + valueType: 'json', + }); + + await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'validation', + key: 'error_message', + value: errorMsg, + valueType: 'single_line_text_field', + }); + }; + + return ( + + + setCountries(value)} + /> + setErrorMsg(value)} + /> + Save Restrictions + Validation ID: {data.validation?.id} + Function ID: {data.shopifyFunction.id} + + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/load-validation-config.jsx b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/load-validation-config.jsx new file mode 100644 index 0000000000..a1d59a15d2 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/load-validation-config.jsx @@ -0,0 +1,55 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [mode, setMode] = useState('loading'); + const [settings, setSettings] = useState({}); + + useEffect(() => { + const initializeSettings = async () => { + if (data.validation) { + const config = data.validation.metafields.reduce((acc, field) => { + acc[field.key] = field.value; + return acc; + }, {}); + + setSettings(config); + setMode('edit'); + } else { + await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'validation', + key: 'default_rule', + value: 'require_minimum_cart_total', + valueType: 'single_line_text_field', + }); + + setMode('created'); + } + }; + + initializeSettings(); + }, [data]); + + return ( + + {mode === 'loading' && } + {mode === 'edit' && ( + <> + Editing existing validation + {Object.entries(settings).map(([key, value]) => ( + + {key}: {value} + + ))} + + )} + {mode === 'created' && Created new validation configuration} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/set-minimum-quantity.jsx b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/set-minimum-quantity.jsx new file mode 100644 index 0000000000..0c3f3e12d0 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/examples/set-minimum-quantity.jsx @@ -0,0 +1,40 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [quantity, setQuantity] = useState('3'); + const [result, setResult] = useState(null); + + const handleSave = async () => { + const res = await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'validation', + key: 'minimum_quantity', + value: quantity, + valueType: 'number_integer', + }); + + setResult(res); + }; + + return ( + + setQuantity(value)} + /> + Save Validation + {result?.type === 'success' && ( + Minimum quantity configured + )} + {result?.type === 'error' && ( + {result.message} + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/launch-options.ts b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/launch-options.ts index 700f72a927..3c3fbaf0bb 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/launch-options.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/launch-options.ts @@ -1,33 +1,45 @@ +/** + * A [metafield](/docs/apps/build/metafields) that stores validation function configuration data. Use metafields to persist settings that control how your validation function behaves, such as minimum quantities, restricted shipping zones, or custom validation rules. + */ interface Metafield { + /** A human-readable description explaining the metafield's purpose and how it affects validation behavior. Use this to document your settings for other developers. */ description?: string; + /** The unique global identifier (GID) for this metafield. Use this ID to reference the metafield in GraphQL queries or updates. */ id: string; + /** The namespace that organizes related metafields together. All metafields for a validation should use a consistent namespace to group related settings. */ namespace: string; + /** The unique key identifying this metafield within its namespace. This key determines how you access the metafield value (for example, `'min_quantity'` or `'blocked_countries'`). */ key: string; + /** The metafield value stored as a string. Parse this value according to the metafield type to use it in your settings UI. */ value: string; + /** The metafield [definition type](/docs/apps/build/metafields/list-of-data-types) that specifies the value format and validation rules. Use this to determine how to parse and display the value. */ type: string; } + +/** + * A validation configuration that exists and is active in the shop. Use this object to access the validation's current settings and metafields when merchants edit an existing validation. + */ interface Validation { - /** - * the validation's gid when active in a shop - */ + /** The validation's unique global identifier (GID). Use this ID to reference the validation in GraphQL operations or when saving updated settings. */ id: string; - /** - * the metafields owned by the validation - */ + /** An array of [metafields](/docs/apps/build/metafields) that store the validation's configuration values. Use these metafields to populate your settings UI with the current validation configuration. */ metafields: Metafield[]; } +/** + * A [Shopify Function](/docs/apps/build/functions) that implements cart and checkout validation logic. This identifies which function the settings interface is configuring. + */ interface ShopifyFunction { - /** - * the validation function's unique identifier - */ + /** The [Shopify Function's](/docs/apps/build/functions) unique global identifier (GID). Use this ID to associate settings changes with the correct function. */ id: string; } /** - * The object that exposes the validation with its settings. + * The `data` object exposed to validation settings extensions in the `admin.settings.validation.render` target. Use this to access the current validation configuration and populate your settings interface with existing values. */ export interface ValidationData { + /** The validation configuration containing the validation ID and metafields. Present when editing an existing validation, absent when creating a new validation. Use the presence of this value to determine if you're in create or edit mode. */ validation?: Validation; + /** The [Shopify Function](/docs/apps/build/functions) that implements the validation logic. Use this ID to associate configuration changes with the correct function. */ shopifyFunction: ShopifyFunction; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/metafields.ts b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/metafields.ts index a93e9be798..63ae263b2c 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/metafields.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/metafields.ts @@ -1,31 +1,66 @@ import {SupportedDefinitionType} from '../shared'; +/** + * A metafield update or creation operation. Use this to set or modify metafield values that store validation function configuration data. + */ interface MetafieldUpdateChange { + /** Identifies this as an update operation. Always set to `'updateMetafield'` for updates. */ type: 'updateMetafield'; + /** The unique key identifying the metafield within its namespace. Use descriptive keys that indicate the setting's purpose (for example, `'min_quantity'` or `'shipping_restriction'`). */ key: string; + /** The namespace that organizes related metafields. When omitted, a default namespace is assigned. Use consistent namespaces to group related settings. */ namespace?: string; + /** The metafield value to store. Can be a string or number depending on your configuration needs. */ value: string | number; + /** The [data type](/docs/apps/build/metafields/list-of-data-types) that defines how the value is formatted and validated. When omitted, preserves the existing type for updates or uses a default for new metafields. Choose a type that matches your value format. */ valueType?: SupportedDefinitionType; } +/** + * A metafield removal operation. Use this to delete metafields that are no longer needed for your validation configuration. + */ interface MetafieldRemoveChange { + /** Identifies this as a removal operation. Always set to `'removeMetafield'` for deletions. */ type: 'removeMetafield'; + /** The unique key of the metafield to remove. Must match the key used when the metafield was created. */ key: string; + /** The namespace containing the metafield to remove. Required to ensure the correct metafield is targeted, as the same key can exist in different namespaces. */ namespace: string; } +/** + * A metafield change operation that can either update or remove a metafield. Pass this to `applyMetafieldChange` to modify validation settings stored in metafields. + */ type MetafieldChange = MetafieldUpdateChange | MetafieldRemoveChange; + +/** + * A failed metafield change operation result. Use the error message to understand what went wrong and fix the issue, such as validation errors, permission problems, or invalid metafield types. + */ interface MetafieldChangeResultError { + /** Indicates the operation failed. Check this value to determine if you need to handle an error. */ type: 'error'; + /** A human-readable error message explaining why the operation failed. Use this to debug issues or display feedback to merchants. */ message: string; } + +/** + * A successful metafield change operation result. The metafield was updated or removed as requested and the changes are now saved. + */ interface MetafieldChangeSuccess { + /** Indicates the operation succeeded. When this value is `'success'`, the metafield change was applied successfully. */ type: 'success'; } + +/** + * The result returned after attempting to change a metafield. Check the `type` property to determine if the operation succeeded (`'success'`) or failed (`'error'`), then handle the result appropriately in your extension. + */ type MetafieldChangeResult = | MetafieldChangeSuccess | MetafieldChangeResultError; +/** + * A function that applies metafield changes to validation settings. Call this function with an update or removal operation, then await the Promise to receive a result indicating success or failure. Use the result to provide feedback or handle errors in your settings interface. + */ export type ApplyMetafieldChange = ( change: MetafieldChange, ) => Promise; diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.doc.ts index 395508cba9..0866e9ecbb 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.doc.ts @@ -3,24 +3,94 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Validation Settings API', description: - 'This API is available to Validation Settings extensions. Refer to the [tutorial](/docs/apps/checkout/validation/create-complex-validation-rules) for more information. Note that the [`FunctionSettings`](/docs/api/admin-extensions/components/forms/functionsettings) component is required to build Validation Settings extensions.', + 'The Validation Settings API lets you [create complex validation rules](/docs/apps/build/checkout/cart-checkout-validation/create-admin-ui-validation) for cart and checkout validation. Use this API to build custom settings interfaces for [Shopify Functions](/docs/apps/build/functions) that implement validation logic.', isVisualComponent: false, type: 'API', + requires: + 'the [`FunctionSettings`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/forms/functionsettings) component.', + defaultExample: { + description: + 'Save a minimum quantity validation rule with a [number field](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/forms/numberfield) input. This example calls `applyMetafieldChange`, checks the result type, and displays success or error banners based on the response.', + codeblock: { + title: 'Set minimum quantity', + tabs: [ + { + title: 'jsx', + code: './examples/set-minimum-quantity.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { title: 'applyMetafieldChange', - description: 'Applies a change to the validation settings.', + description: + 'Applies a [metafield](/docs/apps/build/metafields) change to the validation settings. Use this method to update or remove metafields that store validation function configuration data. The method accepts a change object specifying the operation type, metafield key, namespace, value, and [value type](/docs/apps/build/metafields/list-of-data-types). Returns a promise that resolves to indicate success or provides an error message if the operation fails.', type: 'ApplyMetafieldChange', }, { title: 'data', - description: 'The object that exposes the validation with its settings.', + description: + 'The `data` object exposed to the extension containing the validation settings. Provides access to the validation object with its identifier and [metafields](/docs/apps/build/metafields), plus the [Shopify Function](/docs/apps/build/functions) identifier. Use this data to populate your settings UI and understand the current validation configuration in the `admin.settings.validation.render` target.', type: 'ValidationData', }, ], + examples: { + description: 'Configure cart and checkout validation rules', + examples: [ + { + description: + 'Block shipping to specific countries with custom error messages. This example saves blocked countries as a JSON array and a custom error message, then displays the validation and function IDs.', + codeblock: { + title: 'Configure shipping restrictions', + tabs: [ + { + title: 'jsx', + code: './examples/configure-shipping-restrictions.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + "Detect whether you're editing an existing validation or creating a new one. This example checks for `data.validation`, loads existing metafields if present, or initializes default settings for new validations.", + codeblock: { + title: 'Load validation configuration', + tabs: [ + { + title: 'jsx', + code: './examples/load-validation-config.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + "- **Check operation result type:** `applyMetafieldChange` returns `{ type: 'success' }` or `{ type: 'error', message: string }`. Errors don't throw exceptions, so always check the returned `type` property.", + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Metafields have [size limits](/docs/apps/build/metafields/metafield-limits). Individual values can't exceed 256KB, and total metafield storage per validation is limited.\n" + + '- The `applyMetafieldChange` method is sequential. Operations process one at a time. Rapid successive calls can cause race conditions where later updates overwrite earlier ones.\n' + + '- Metafield changes apply immediately. Unlike admin forms, metafield changes persist right away without waiting for merchants to save.\n' + + "- Your extension can't modify the Function ID. The `shopifyFunctionId` is read-only and determined when the validation rule is created.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.ts b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.ts index 112c51c429..dc539ba109 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/checkout-rules/validation-settings.ts @@ -1,15 +1,19 @@ -import type {StandardApi} from '../standard/standard'; +import type {StandardRenderingExtensionApi} from '../standard/standard-rendering'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import {ApplyMetafieldChange} from './metafields'; import {ValidationData} from './launch-options'; +/** + * The `ValidationSettingsApi` object provides methods for configuring cart and checkout validation functions. Access the following properties on the `ValidationSettingsApi` object to manage validation settings and metafields. + */ export interface ValidationSettingsApi< ExtensionTarget extends AnyExtensionTarget, -> extends StandardApi { +> extends StandardRenderingExtensionApi { /** - * Applies a change to the validation settings. + * Updates or removes [metafields](/docs/apps/build/metafields) that store validation function configuration. Use this to save merchant settings for your validation function. */ applyMetafieldChange: ApplyMetafieldChange; + /** The validation being configured and its associated [metafields](/docs/apps/build/metafields) storing function settings. */ data: ValidationData; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.doc.ts index 49e7fe6a92..2cd7dffc85 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.doc.ts @@ -3,19 +3,88 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Customer Segment Template Extension API', description: - 'This API is available to all customer segment template extension types.', + 'The Customer Segment Template Extension API lets you [build a customer segment template extension](/docs/apps/build/marketing-analytics/customer-segments/build-a-template-extension). Merchants can use your templates to set up [customer segments](/docs/apps/build/marketing-analytics/customer-segments) based on custom criteria.', isVisualComponent: false, type: 'API', + defaultExample: { + description: + 'Create a segment template targeting customers who spent $500+ across 5+ orders. This example uses `total_spent` and `orders_count` queries to identify high-value customers, and demonstrates `shopify.i18n.translate` for internationalized template titles and descriptions.', + codeblock: { + title: 'Target high-value customers', + tabs: [ + { + title: 'jsx', + code: './examples/high-value-customers.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'CustomerSegmentTemplateApi', - description: '', + title: 'Properties', + description: + 'The `CustomerSegmentTemplateApi` object includes tools for creating segment templates and translating content. Access the following properties on the `CustomerSegmentTemplateApi` object in the `admin.customers.segmentation-templates.data` target.', type: 'CustomerSegmentTemplateApi', }, ], + examples: { + description: 'Pre-built customer segment templates', + examples: [ + { + description: + 'Create a segment template targeting customers with birthdays this month. This example requires the `facts.birth_date` metafield to be set up, which enables birthday-based customer targeting for marketing campaigns.', + codeblock: { + title: 'Target customers with birthdays this month', + tabs: [ + { + title: 'jsx', + code: './examples/birthday-this-month.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Create a segment template targeting customers who abandoned at least one checkout in the last 7 days. This example uses `abandoned_checkouts_count` and `last_abandoned_order_date` queries with dynamic date calculation to identify customers for abandoned cart email outreach.', + codeblock: { + title: "Target customers who started checkout but didn't finish", + tabs: [ + { + title: 'jsx', + code: './examples/abandoned-cart-recovery.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + "- **Test queries in admin before shipping:** Template queries aren't validated until merchants save them. Test query syntax in the [Shopify admin segment editor](https://help.shopify.com/manual/customers/customer-segmentation/create-customer-segments) before shipping to avoid merchant-facing errors.\n" + + '- **Declare all metafield dependencies:** Use both `standardMetafields` (for Shopify-defined metafields) and `customMetafields` (for app-defined metafields) in the `dependencies` object. Missing dependencies cause queries to fail when merchants lack required metafields.\n' + + '- **Use `queryToInsert` for formatted display queries:** If your `query` includes formatting or comments for readability, provide a clean executable version in `queryToInsert`. If omitting `queryToInsert`, ensure `query` has no comments that would break execution.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Query validation only occurs when merchants save. Syntax errors in queries aren't caught by the API and only surface in the admin when merchants attempt to save the segment.\n" + + "- Your extension can't programmatically create segments. Templates only provide the query and metadata. Merchants must manually save templates as segments.\n" + + "- Dependencies don't auto-create metafields. If required metafields don't exist, merchants see errors when trying to use the template. The API only declares dependencies, it doesn't create them.\n" + + "- Dynamic query generation isn't supported. Queries must be static strings. You can't parameterize queries based on merchant input or shop configuration.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.ts b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.ts index 0e34acf976..d72b90d19d 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/customer-segment-template.ts @@ -2,42 +2,54 @@ import type {StandardApi} from '../standard/standard'; import type {I18n} from '../../../../api'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; +/** + * The `CustomerSegmentTemplateApi` object provides methods for creating customer segment templates. Access the following properties on the `CustomerSegmentTemplateApi` object to build templates with translated content. + */ export interface CustomerSegmentTemplateApi< ExtensionTarget extends AnyExtensionTarget, > extends StandardApi { - /* Utilities for translating content according to the current `localization` of the admin. */ + /** Utilities for translating template content into the merchant's language. */ i18n: I18n; /** @private */ __enabledFeatures: string[]; } +/** + * Standard customer metafields that can be referenced as template dependencies. + */ export type CustomerStandardMetafieldDependency = 'facts.birth_date'; + +/** + * Defines a customer segment template that merchants can use to create targeted customer groups. + */ export interface CustomerSegmentTemplate { /** - * The localized title of the template. + * The template title in the merchant's language. */ title: string; /** - * The localized description of the template. An array can be used for multiple paragraphs. + * The template description in the merchant's language. Use an array for multiple paragraphs. */ description: string | string[]; /** - * The code snippet to render in the template with syntax highlighting. The `query` is not validated in the template. + * The segment query code to display in the template with syntax highlighting. This code is shown to merchants but not validated. Test your queries in the Shopify admin segment editor before including them in templates. */ query: string; /** - * The code snippet to insert in the segment editor. If missing, `query` will be used. The `queryToInsert` is not validated in the template. + * The segment query code to insert when the merchant selects this template. If omitted, the `query` value is used instead. This code is inserted into the editor but not validated. Test your queries in the Shopify admin segment editor before including them in templates. */ queryToInsert: string; /** - * The list of customer standard metafields or custom metafields used in the template's query. + * Customer metafields required by this template's query. Declare dependencies so the admin can guide merchants to set up required metafields before using the template. */ dependencies?: { + /** Standard Shopify customer metafields used in the query. */ standardMetafields?: CustomerStandardMetafieldDependency[]; + /** Custom [metafield](/docs/apps/build/metafields) definitions used in the query. */ customMetafields?: string[]; }; /** - * ISO 8601-encoded date and time string. A "New" badge will be rendered for templates introduced in the last month. + * The creation date in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. Templates created within the last month display a "New" badge. */ createdOn: string; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/abandoned-cart-recovery.jsx b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/abandoned-cart-recovery.jsx new file mode 100644 index 0000000000..479247912c --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/abandoned-cart-recovery.jsx @@ -0,0 +1,30 @@ +export default () => { + return { + templates: [ + { + title: 'Cart abandoners', + description: [ + 'Customers who abandoned carts in the last 7 days', + 'Use this segment for email recovery campaigns', + ], + query: `{ + abandoned_checkouts_count: { + min: 1 + } + last_abandoned_order_date: { + min: "${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()}" + } +}`, + queryToInsert: `{ + abandoned_checkouts_count: { + min: 1 + } + last_abandoned_order_date: { + min: "LAST_7_DAYS" + } +}`, + createdOn: '2025-01-15T00:00:00Z', + }, + ], + }; +}; diff --git a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/birthday-this-month.jsx b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/birthday-this-month.jsx new file mode 100644 index 0000000000..63a4cfefed --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/birthday-this-month.jsx @@ -0,0 +1,32 @@ +export default () => { + return { + templates: [ + { + title: 'Birthday this month', + description: 'Customers with birthdays in the current month', + query: `{ + metafields: { + key: "birth_date" + namespace: "customer" + value: { + month: ${new Date().getMonth() + 1} + } + } +}`, + queryToInsert: `{ + metafields: { + key: "birth_date" + namespace: "customer" + value: { + month: ${new Date().getMonth() + 1} + } + } +}`, + dependencies: { + standardMetafields: ['facts.birth_date'], + }, + createdOn: '2025-01-15T00:00:00Z', + }, + ], + }; +}; diff --git a/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/high-value-customers.jsx b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/high-value-customers.jsx new file mode 100644 index 0000000000..cef22eecbf --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/customer-segment-template/examples/high-value-customers.jsx @@ -0,0 +1,27 @@ +export default () => { + return { + templates: [ + { + title: shopify.i18n.translate('templates.highValue.title'), + description: shopify.i18n.translate('templates.highValue.description'), + query: `{ + total_spent: { + min: 500 + } + orders_count: { + min: 5 + } +}`, + queryToInsert: `{ + total_spent: { + min: 500 + } + orders_count: { + min: 5 + } +}`, + createdOn: '2025-01-15T00:00:00Z', + }, + ], + }; +}; diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.doc.ts index e3240c9989..186858506f 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.doc.ts @@ -3,31 +3,93 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Discount Function Settings API', description: - 'This API is available to Discount Function Settings extensions. Refer to the [tutorial](/docs/apps/build/discounts/build-ui-extension) for more information. Note that the [`FunctionSettings`](/docs/api/admin-extensions/components/forms/functionsettings) component is required to build Discount Function Settings extensions.', + 'The Discount Function Settings API lets you build UI extensions that provide custom configuration interfaces for [discount functions](/docs/apps/build/discounts/build-ui-extension). Use this API to create settings pages for [Shopify Functions](/docs/apps/build/functions) that implement discount logic.', isVisualComponent: false, type: 'API', + requires: + 'the [`FunctionSettings`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/forms/functionsettings) component.', + defaultExample: { + description: + 'Save a minimum purchase threshold to a metafield with decimal number validation. This example uses a [text field](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/forms/textfield) for input, calls `applyMetafieldChange`, and displays success or error feedback.', + codeblock: { + title: 'Configure discount threshold', + tabs: [ + { + title: 'jsx', + code: './examples/configure-discount-threshold.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { title: 'applyMetafieldChange', - description: 'Applies a change to the discount function settings.', + description: + 'Updates or removes [metafields](/docs/apps/build/metafields) that store discount function configuration data. Accepts a change object with the operation type, key, namespace, value, and [value type](/docs/apps/build/metafields/list-of-data-types).', type: 'ApplyMetafieldChange', }, { title: 'data', description: - 'The object exposed to the extension that contains the discount function settings.', + 'The `data` object exposed to the extension containing the discount function settings. Provides access to the discount identifier and associated [metafields](/docs/apps/build/metafields) that store function configuration values. Use this data to populate your settings UI and understand the current function configuration in the `admin.discount-details.function-settings.render` target.', type: 'DiscountFunctionSettingsData', }, - { - title: 'discounts', - description: - 'The reactive API for managing discount function configuration.', - type: 'DiscountsApi', - }, ], + examples: { + description: 'Configure discount function settings', + examples: [ + { + description: + 'Save multiple discount configuration settings in a single operation. This example stores customer tags as JSON and usage limits as an integer, demonstrating how to apply multiple metafield changes sequentially.', + codeblock: { + title: 'Configure eligibility rules', + tabs: [ + { + title: 'jsx', + code: './examples/configure-eligibility-rules.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Load discount metafields on mount and display current configuration. This example shows reducing metafields into a settings object, checking for missing values, and applying defaults only when needed.', + codeblock: { + title: 'Load existing settings', + tabs: [ + { + title: 'jsx', + code: './examples/load-existing-settings.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + "- **Check operation result type:** `applyMetafieldChange` returns `{ type: 'success' }` or `{ type: 'error', message: string }`. Errors don't throw exceptions, so always check the returned `type` property.", + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Metafields are subject to [size limits](/docs/apps/build/metafields/metafield-limits). Individual metafield values can't exceed 256KB, and total metafields per resource have storage limits.\n" + + '- The `applyMetafieldChange` method is sequential. Operations are processed one at a time. Rapid successive calls may lead to race conditions where later updates overwrite earlier ones.\n' + + '- Metafield changes are applied immediately. Unlike some admin forms, metafield changes persist right away without waiting for the merchant to save the discount.', + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.ts b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.ts index 9dab1d3e21..58fbbaeff0 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/discount-function-settings.ts @@ -4,13 +4,18 @@ import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-target import {ApplyMetafieldChange} from './metafields'; import {DiscountFunctionSettingsData, DiscountsApi} from './launch-options'; +/** + * The `DiscountFunctionSettingsApi` object provides methods for configuring discount functions. Access the following properties on the `DiscountFunctionSettingsApi` object to manage function settings and metafields. + */ export interface DiscountFunctionSettingsApi< ExtensionTarget extends AnyExtensionTarget, > extends Omit, 'data'> { /** - * Applies a change to the discount function settings. + * Updates or removes [metafields](/docs/apps/build/metafields) that store discount function configuration. Use this to save merchant settings for your discount function. */ applyMetafieldChange: ApplyMetafieldChange; + /** The discount being configured and its associated [metafields](/docs/apps/build/metafields) storing function settings. */ data: DiscountFunctionSettingsData; + /** The `discounts` object provides reactive access to discount configuration, including discount classes and the discount method. Use the signals to read current values and the update functions to modify discount classes in your settings UI. These values automatically update when changed by the merchant or system. */ discounts: DiscountsApi; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/configure-discount-threshold.jsx b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/configure-discount-threshold.jsx new file mode 100644 index 0000000000..6b22e3a253 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/configure-discount-threshold.jsx @@ -0,0 +1,39 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [threshold, setThreshold] = useState('50.00'); + const [saved, setSaved] = useState(false); + + const handleSave = async () => { + const result = await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'discount-config', + key: 'minimum_purchase', + value: threshold, + valueType: 'number_decimal', + }); + + if (result.type === 'success') { + setSaved(true); + } else { + console.error('Configuration failed:', result.message); + } + }; + + return ( + + setThreshold(value)} + /> + Save Threshold + {saved && Threshold configured!} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/configure-eligibility-rules.jsx b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/configure-eligibility-rules.jsx new file mode 100644 index 0000000000..7b87162afb --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/configure-eligibility-rules.jsx @@ -0,0 +1,45 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [tags, setTags] = useState('vip, wholesale, premium'); + const [maxUses, setMaxUses] = useState('5'); + + const handleSave = async () => { + await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'discount-config', + key: 'eligible_customer_tags', + value: JSON.stringify(tags.split(',').map((t) => t.trim())), + valueType: 'json', + }); + + await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'discount-config', + key: 'max_uses_per_customer', + value: maxUses, + valueType: 'number_integer', + }); + }; + + return ( + + setTags(value)} + /> + setMaxUses(value)} + /> + Save Eligibility Rules + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/load-existing-settings.jsx b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/load-existing-settings.jsx new file mode 100644 index 0000000000..7ec4069d04 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/examples/load-existing-settings.jsx @@ -0,0 +1,45 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [settings, setSettings] = useState({}); + + useEffect(() => { + const initializeSettings = async () => { + const existingSettings = data.metafields.reduce((acc, field) => { + acc[field.key] = field.value; + return acc; + }, {}); + + setSettings(existingSettings); + + if (!existingSettings.eligible_tags) { + await shopify.applyMetafieldChange({ + type: 'updateMetafield', + namespace: 'discount-config', + key: 'eligible_tags', + value: JSON.stringify(['vip', 'wholesale']), + valueType: 'json', + }); + } + }; + + initializeSettings(); + }, [data]); + + return ( + + Current settings: + {Object.entries(settings).map(([key, value]) => ( + + {key}: {String(value)} + + ))} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/launch-options.ts b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/launch-options.ts index 589a0b1cf0..607ad06cce 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/launch-options.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/launch-options.ts @@ -3,47 +3,65 @@ import type { UpdateSignalFunction, } from '../../../../shared'; +/** + * A [metafield](/docs/apps/build/metafields) that stores discount function configuration data. Use metafields to persist settings that control how your discount function behaves, such as discount thresholds, eligibility rules, or custom discount logic parameters. + */ interface Metafield { + /** A human-readable description explaining the metafield's purpose and how it affects discount behavior. Use this to document your settings for other developers. */ description?: string; + /** The unique global identifier (GID) for this metafield. Use this ID to reference the metafield in GraphQL queries or updates. */ id: string; + /** The namespace that organizes related metafields together. All metafields for a discount should use a consistent namespace to group related settings. */ namespace: string; + /** The unique key identifying this metafield within its namespace. This key determines how you access the metafield value (for example, `'min_purchase_amount'` or `'eligible_customer_tags'`). */ key: string; + /** The metafield value stored as a string. Parse this value according to the metafield type to use it in your settings UI. */ value: string; + /** The metafield [definition type](/docs/apps/build/metafields/list-of-data-types) that specifies the value format and validation rules. Use this to determine how to parse and display the value. */ type: string; } -type DiscountClass = 'product' | 'order' | 'shipping'; +/** + * The discount class that determines where the discount applies in the purchase flow. Use this to understand what type of discount the merchant is configuring (product-level, order-level, or shipping). + */ +export enum DiscountClass { + /** The discount applies to specific products or product variants. Use this for discounts that reduce the price of individual line items (for example, "20% off selected products"). */ + Product = 'PRODUCT', + /** The discount applies to the entire order total. Use this for cart-wide discounts that reduce the subtotal (for example, "$10 off orders over $50"). */ + Order = 'ORDER', + /** The discount applies to shipping costs. Use this for free shipping or reduced shipping rate discounts (for example, "Free shipping on orders over $100"). */ + Shipping = 'SHIPPING', +} +/** + * The method used to apply a discount. Use `'automatic'` for discounts that apply automatically at checkout, or `'code'` for discounts that require a code entered by the customer. + */ type DiscountMethod = 'automatic' | 'code'; /** - * The object that exposes the validation with its settings. + * The `data` object exposed to discount function settings extensions in the `admin.discount-details.function-settings.render` target. Use this to access the current discount configuration and populate your settings interface with existing values. */ export interface DiscountFunctionSettingsData { - /** - * The unique identifier for the discount. - */ + /** The discount's unique global identifier (GID) in the [GraphQL Admin API](/docs/api/admin-graphql) format (for example, `gid://shopify/DiscountAutomaticApp/123`). Use this ID to associate settings with the correct discount or query discount data. */ id: string; - /** - * The discount metafields. - */ + /** An array of [metafields](/docs/apps/build/metafields) that store the discount function's configuration values. Use these metafields to populate your settings UI with the current discount configuration and display existing settings to merchants. */ metafields: Metafield[]; } /** - * Reactive Api for managing discount function configuration. + * The `DiscountsApi` object provides reactive access to discount configuration. Use the signals to read discount classes and method, and the update function to change which parts of the purchase (products, order, or shipping) the discount affects. */ export interface DiscountsApi { /** - * A signal that contains the discount classes. + * A signal that contains the discount classes (Product, Order, or Shipping). Read this to determine where the discount applies in the purchase flow. A discount can apply to multiple classes simultaneously. */ discountClasses: ReadonlySignalLike; /** - * A function that updates the discount classes. + * A function that updates the discount classes to change where the discount applies. Call this function with an array of `DiscountClass` values to set which parts of the purchase (products, order total, or shipping) the discount affects. */ updateDiscountClasses: UpdateSignalFunction; /** - * A signal that contains the discount method. + * A signal that contains the discount method (`'automatic'` or `'code'`). Read this to determine whether the discount applies automatically at checkout or requires a customer-entered code. */ discountMethod: ReadonlySignalLike; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/metafields.ts b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/metafields.ts index a93e9be798..ac56ba7fcf 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/metafields.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/discount-function-settings/metafields.ts @@ -1,31 +1,66 @@ import {SupportedDefinitionType} from '../shared'; +/** + * A metafield update or creation operation. Use this to set or modify metafield values that store discount function configuration data. + */ interface MetafieldUpdateChange { + /** Identifies this as an update operation. Always set to `'updateMetafield'` for updates. */ type: 'updateMetafield'; + /** The unique key identifying the metafield within its namespace. Use descriptive keys that indicate the setting's purpose (for example, `'min_purchase_amount'` or `'eligible_customer_tags'`). */ key: string; + /** The namespace that organizes related metafields. When omitted, a default namespace is assigned. Use consistent namespaces to group related settings. */ namespace?: string; + /** The metafield value to store. Can be a string or number depending on your configuration needs. */ value: string | number; + /** The [data type](/docs/apps/build/metafields/list-of-data-types) that defines how the value is formatted and validated. When omitted, preserves the existing type for updates or uses a default for new metafields. Choose a type that matches your value format. */ valueType?: SupportedDefinitionType; } +/** + * A metafield removal operation. Use this to delete metafields that are no longer needed for your discount configuration. + */ interface MetafieldRemoveChange { + /** Identifies this as a removal operation. Always set to `'removeMetafield'` for deletions. */ type: 'removeMetafield'; + /** The unique key of the metafield to remove. Must match the key used when the metafield was created. */ key: string; + /** The namespace containing the metafield to remove. Required to ensure the correct metafield is targeted, as the same key can exist in different namespaces. */ namespace: string; } +/** + * A metafield change operation that can either update or remove a metafield. Pass this to `applyMetafieldChange` to modify discount settings stored in metafields. + */ type MetafieldChange = MetafieldUpdateChange | MetafieldRemoveChange; + +/** + * A failed metafield change operation result. Use the error message to understand what went wrong and fix the issue, such as validation errors, permission problems, or invalid metafield types. + */ interface MetafieldChangeResultError { + /** Indicates the operation failed. Check this value to determine if you need to handle an error. */ type: 'error'; + /** A human-readable error message explaining why the operation failed. Use this to debug issues or display feedback to merchants. */ message: string; } + +/** + * A successful metafield change operation result. The metafield was updated or removed as requested and the changes are now saved. + */ interface MetafieldChangeSuccess { + /** Indicates the operation succeeded. When this value is `'success'`, the metafield change was applied successfully. */ type: 'success'; } + +/** + * The result returned after attempting to change a metafield. Check the `type` property to determine if the operation succeeded (`'success'`) or failed (`'error'`), then handle the result appropriately in your extension. + */ type MetafieldChangeResult = | MetafieldChangeSuccess | MetafieldChangeResultError; +/** + * A function that applies metafield changes to discount function settings. Call this function with an update or removal operation, then await the Promise to receive a result indicating success or failure. Use the result to provide feedback or handle errors in your settings interface. + */ export type ApplyMetafieldChange = ( change: MetafieldChange, ) => Promise; diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-article.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-article.js deleted file mode 100644 index 16d1544fc1..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-article.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Article'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Article created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-article.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-article.jsx new file mode 100644 index 0000000000..7dacfd5e11 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-article.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [creating, setCreating] = useState(false); + + const handleCreate = async () => { + setCreating(true); + + const activity = await shopify.intents.invoke('create:shopify/Article'); + const response = await activity.complete; + + setResult(response); + setCreating(false); + }; + + return ( + + + {creating ? 'Creating...' : 'Launch Article Creator'} + + {result?.code === 'ok' && ( + Article created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-catalog.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-catalog.js deleted file mode 100644 index 8a0ece1d74..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-catalog.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Catalog'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Catalog created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-catalog.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-catalog.jsx new file mode 100644 index 0000000000..101e4115b5 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-catalog.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Catalog'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Catalog Creator'} + + {result?.code === 'ok' && ( + Catalog created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-collection.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-collection.js deleted file mode 100644 index 4fe2417ee3..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-collection.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Collection'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Collection created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-collection.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-collection.jsx new file mode 100644 index 0000000000..481bcf8dc2 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-collection.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Collection'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Collection Creator'} + + {result?.code === 'ok' && ( + Collection created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-customer.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-customer.js deleted file mode 100644 index 2ff3564868..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-customer.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Customer'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Customer created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-customer.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-customer.jsx new file mode 100644 index 0000000000..136ece32ee --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-customer.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Customer'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Customer Creator'} + + {result?.code === 'ok' && ( + Customer created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-discount.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-discount.js deleted file mode 100644 index a9ff1f33a3..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-discount.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Discount', { - data: {type: 'amount-off-product'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Discount created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-discount.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-discount.jsx new file mode 100644 index 0000000000..61602396bd --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-discount.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Discount'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Discount Creator'} + + {result?.code === 'ok' && ( + Discount created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-market.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-market.js deleted file mode 100644 index 66d9361eda..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-market.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Market'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Market created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-market.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-market.jsx new file mode 100644 index 0000000000..40d541fb3f --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-market.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Market'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Market Creator'} + + {result?.code === 'ok' && ( + Market created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-menu.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-menu.js deleted file mode 100644 index d52aa066db..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-menu.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Menu'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Menu created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-menu.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-menu.jsx new file mode 100644 index 0000000000..fd9278dce0 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-menu.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Menu'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Menu Creator'} + + {result?.code === 'ok' && ( + Menu created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metafield-definition.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metafield-definition.js deleted file mode 100644 index d9e69c1cad..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metafield-definition.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/MetafieldDefinition', { - data: {ownerType: 'product'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Metafield definition created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metafield-definition.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metafield-definition.jsx new file mode 100644 index 0000000000..4c20f808e4 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metafield-definition.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/MetafieldDefinition'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Metafield Definition Creator'} + + {result?.code === 'ok' && ( + Metafield Definition created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject-definition.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject-definition.js deleted file mode 100644 index 546b0dfdf2..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject-definition.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/MetaobjectDefinition'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Metaobject definition created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject-definition.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject-definition.jsx new file mode 100644 index 0000000000..56865d91dd --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject-definition.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/MetaobjectDefinition'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Metaobject Definition Creator'} + + {result?.code === 'ok' && ( + Metaobject Definition created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject.js deleted file mode 100644 index adb444b91d..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Metaobject', { - data: {type: 'shopify--color-pattern'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Metaobject created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject.jsx new file mode 100644 index 0000000000..fbce117970 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-metaobject.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Metaobject'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Metaobject Creator'} + + {result?.code === 'ok' && ( + Metaobject created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-page.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-page.js deleted file mode 100644 index 4495bcdc38..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-page.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Page'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Page created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-page.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-page.jsx new file mode 100644 index 0000000000..299fd2427c --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-page.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/Page'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Page Creator'} + + {result?.code === 'ok' && ( + Page created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-product.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-product.js deleted file mode 100644 index c564e2f117..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-product.js +++ /dev/null @@ -1,9 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/Product'); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Product created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-product.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-product.jsx new file mode 100644 index 0000000000..9527d08d2c --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-product.jsx @@ -0,0 +1,37 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [creating, setCreating] = useState(false); + + const handleCreate = async () => { + setCreating(true); + + const activity = await shopify.intents.invoke('create:shopify/Product'); + const response = await activity.complete; + + setResult(response); + setCreating(false); + }; + + return ( + + + {creating ? 'Creating...' : 'Launch Product Creator'} + + {result?.code === 'ok' && ( + + Product created: {result.data?.product?.id} + + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-variant.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-variant.js deleted file mode 100644 index 29e0ade06c..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-variant.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('create:shopify/ProductVariant', { - data: {productId: 'gid://shopify/Product/123456789'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Product variant created:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-variant.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-variant.jsx new file mode 100644 index 0000000000..9a36634a60 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/create-variant.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('create:shopify/ProductVariant'); + const response = await activity.complete; + + setResult(response); + setLoading(false); + }; + + return ( + + + {loading ? 'Creating...' : 'Launch Product Variant Creator'} + + {result?.code === 'ok' && ( + Product Variant created successfully! + )} + {result?.code === 'closed' && ( + Creation cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-article.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-article.js deleted file mode 100644 index 53c46d019b..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-article.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Article', { - value: 'gid://shopify/Article/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Article updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-article.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-article.jsx new file mode 100644 index 0000000000..1241f97b7a --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-article.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Article/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Article', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Article'} + + {result?.code === 'ok' && ( + Article updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-catalog.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-catalog.js deleted file mode 100644 index 548b582449..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-catalog.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Catalog', { - value: 'gid://shopify/Catalog/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Catalog updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-catalog.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-catalog.jsx new file mode 100644 index 0000000000..cd690e7c4f --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-catalog.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Catalog/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Catalog', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Catalog'} + + {result?.code === 'ok' && ( + Catalog updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-collection.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-collection.js deleted file mode 100644 index c09de9d423..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-collection.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Collection', { - value: 'gid://shopify/Collection/987654321', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Collection updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-collection.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-collection.jsx new file mode 100644 index 0000000000..87a95a4963 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-collection.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Collection/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Collection', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Collection'} + + {result?.code === 'ok' && ( + Collection updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-customer.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-customer.js deleted file mode 100644 index 7ce8196bf5..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-customer.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Customer', { - value: 'gid://shopify/Customer/456789123', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Customer updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-customer.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-customer.jsx new file mode 100644 index 0000000000..53592313eb --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-customer.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Customer/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Customer', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Customer'} + + {result?.code === 'ok' && ( + Customer updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-discount.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-discount.js deleted file mode 100644 index a415384c06..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-discount.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Discount', { - value: 'gid://shopify/Discount/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Discount updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-discount.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-discount.jsx new file mode 100644 index 0000000000..61f6ac0154 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-discount.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Discount/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Discount', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Discount'} + + {result?.code === 'ok' && ( + Discount updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-market.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-market.js deleted file mode 100644 index 49aa10403b..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-market.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Market', { - value: 'gid://shopify/Market/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Market updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-market.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-market.jsx new file mode 100644 index 0000000000..7ad40d4eb8 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-market.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Market/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Market', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Market'} + + {result?.code === 'ok' && ( + Market updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-menu.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-menu.js deleted file mode 100644 index aafa3d3a31..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-menu.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Menu', { - value: 'gid://shopify/Menu/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Menu updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-menu.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-menu.jsx new file mode 100644 index 0000000000..cdceed7923 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-menu.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Menu/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Menu', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Menu'} + + {result?.code === 'ok' && ( + Menu updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metafield-definition.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metafield-definition.js deleted file mode 100644 index 8f4818d62f..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metafield-definition.js +++ /dev/null @@ -1,12 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/MetafieldDefinition', { - value: 'gid://shopify/MetafieldDefinition/123456789', - data: {ownerType: 'product'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Metafield definition updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metafield-definition.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metafield-definition.jsx new file mode 100644 index 0000000000..0d5d077b8e --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metafield-definition.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/MetafieldDefinition/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/MetafieldDefinition', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Metafield Definition'} + + {result?.code === 'ok' && ( + Metafield Definition updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject-definition.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject-definition.js deleted file mode 100644 index 398947004c..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject-definition.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/MetaobjectDefinition', { - data: {type: 'my_metaobject_definition_type'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Metaobject definition updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject-definition.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject-definition.jsx new file mode 100644 index 0000000000..cf60fdf8b2 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject-definition.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/MetaobjectDefinition/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/MetaobjectDefinition', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Metaobject Definition'} + + {result?.code === 'ok' && ( + Metaobject Definition updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject.js deleted file mode 100644 index e6cb32faff..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject.js +++ /dev/null @@ -1,12 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Metaobject', { - value: 'gid://shopify/Metaobject/123456789', - data: {type: 'shopify--color-pattern'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Metaobject updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject.jsx new file mode 100644 index 0000000000..5c051fee31 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-metaobject.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Metaobject/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Metaobject', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Metaobject'} + + {result?.code === 'ok' && ( + Metaobject updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-page.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-page.js deleted file mode 100644 index 866c1dd353..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-page.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Page', { - value: 'gid://shopify/Page/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Page updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-page.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-page.jsx new file mode 100644 index 0000000000..3e4c98a853 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-page.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/Page/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/Page', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Page'} + + {result?.code === 'ok' && ( + Page updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-product.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-product.js deleted file mode 100644 index c9de08faa1..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-product.js +++ /dev/null @@ -1,11 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/Product', { - value: 'gid://shopify/Product/123456789', -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Product updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-product.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-product.jsx new file mode 100644 index 0000000000..96f5d0f798 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-product.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [editing, setEditing] = useState(false); + + const productId = data.selected[0]?.id || 'gid://shopify/Product/123456789'; + + const handleEdit = async () => { + setEditing(true); + + const activity = await shopify.intents.invoke('edit:shopify/Product', { + value: productId, + }); + + const response = await activity.complete; + setResult(response); + setEditing(false); + }; + + return ( + + Product: {productId} + + {editing ? 'Opening...' : 'Edit Product'} + + {result?.code === 'ok' && ( + Product updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-variant.js b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-variant.js deleted file mode 100644 index 9cba8ebadd..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-variant.js +++ /dev/null @@ -1,12 +0,0 @@ -const {intents} = useApi(TARGET); - -const activity = await intents.invoke('edit:shopify/ProductVariant', { - value: 'gid://shopify/ProductVariant/123456789', - data: {productId: 'gid://shopify/Product/123456789'}, -}); - -const response = await activity.complete; - -if (response.code === 'ok') { - console.log('Product variant updated:', response.data); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-variant.jsx b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-variant.jsx new file mode 100644 index 0000000000..7bce663124 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/examples/edit-variant.jsx @@ -0,0 +1,41 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const resourceId = data.selected[0]?.id || 'gid://shopify/ProductVariant/123456789'; + + const handleAction = async () => { + setLoading(true); + + const activity = await shopify.intents.invoke('edit:shopify/ProductVariant', { + value: resourceId, + }); + + const response = await activity.complete; + setResult(response); + setLoading(false); + }; + + return ( + + Editing: {resourceId} + + {loading ? 'Opening...' : 'Edit Product Variant'} + + {result?.code === 'ok' && ( + Product Variant updated! + )} + {result?.code === 'closed' && ( + Edit cancelled + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/intents.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/intents/intents.doc.ts index dd6ffcb6b2..65ee710230 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/intents.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/intents.doc.ts @@ -4,142 +4,177 @@ const data: ReferenceEntityTemplateSchema = { name: 'Intents API', overviewPreviewDescription: 'Orchestrate workflows and operations across Shopify resources', - description: `The Intents API provides a way to invoke existing admin workflows for creating, editing, and managing Shopify resources.`, + description: `The Intents API launches Shopify's native admin interfaces for creating and editing resources. When your extension calls an intent, merchants complete their changes using the standard admin UI, and your extension receives the result. This means you don't need to build custom forms. + +Use this API to build workflows like adding products to collections from bulk actions, creating multiple related resources in sequence (like a product, collection, and discount for a promotion), opening specific resources for editing from custom buttons, or launching discount creation with pre-selected types.`, isVisualComponent: true, category: 'Target APIs', subCategory: 'Utility APIs', thumbnail: 'intents.png', requires: - 'an Admin [block](/docs/api/admin-extensions/unstable/extension-targets#block-locations) or [action](/docs/api/admin-extensions/unstable/extension-targets#action-locations) extension.', + 'an Admin UI [block or action](/docs/api/admin-extensions/{API_VERSION}#building-your-extension) extension.', defaultExample: { + description: + 'Launch the article creation workflow from a button click. This example uses `shopify.intents.invoke()` to open the article editor, awaits the workflow completion, and displays success or cancellation feedback based on the response code.', image: 'intents.png', codeblock: { - title: 'Creating a collection', + title: 'Create a new article', tabs: [ { - code: './examples/create-collection.js', - language: 'js', + title: 'jsx', + code: './examples/create-article.jsx', + language: 'jsx', }, ], }, }, definitions: [ { - title: 'invoke', - description: `The \`invoke\` API is a function that accepts either a string query or an options object describing the intent to invoke and returns a Promise that resolves to an activity handle for the workflow. + title: 'invoke method', + description: `The \`invoke\` method launches a Shopify admin workflow for creating or editing resources. The method returns a promise that resolves to an activity handle you can await to get the workflow result. + +The method accepts either: +- **String query:** \`\${action}:\${type},\${value}\` with optional second parameter (\`IntentQueryOptions\`) +- **Object:** Properties for \`action\`, \`type\`, \`value\`, and \`data\` + +### IntentQueryOptions parameters + +Optional parameters for the \`invoke\` method when using the string query format: -## Intent Format +- **\`value\`** (\`string\`): The resource identifier for edit operations (for example, \`'gid://shopify/Product/123'\`). Required when editing existing resources. Omit for create operations. +- **\`data\`** (\`{ [key: string]: unknown }\`): Additional context required by specific resource types. For example, discounts require a type, variants require a product ID, and metaobjects require a definition type. -Intents are invoked using a string query format: \`\${action}:\${type},\${value}\` +### Supported resources -Where: -- \`action\` - The operation to perform (\`create\` or \`edit\`) -- \`type\` - The resource type (e.g., \`shopify/Product\`) -- \`value\` - The resource identifier (only for edit actions) +The following tables show which resource types you can create or edit, and what values you need to pass for \`value\` and \`data\` for each operation. -## Supported Resources +#### Article + +[Articles](/docs/api/admin-graphql/latest/objects/Article) are blog posts published on the Online Store. Use this to create or edit articles for merchant blogs. -### Article | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Article\` | — | — | | \`edit\` | \`shopify/Article\` | \`gid://shopify/Article/{id}\` | — | -### Catalog +#### Catalog + +[Catalogs](/docs/api/admin-graphql/latest/interfaces/Catalog) are product groupings that organize products for different markets or channels. Use this to create or edit catalogs for B2B or multi-market selling. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Catalog\` | — | — | | \`edit\` | \`shopify/Catalog\` | \`gid://shopify/Catalog/{id}\` | — | -### Collection +#### Collection + +[Collections](/docs/api/admin-graphql/latest/objects/Collection) are groups of products organized manually or by automated rules. Use this to create or edit product collections. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Collection\` | — | — | | \`edit\` | \`shopify/Collection\` | \`gid://shopify/Collection/{id}\` | — | -### Customer +#### Customer + +[Customers](/docs/api/admin-graphql/latest/objects/Customer) are profiles with contact information, order history, and metadata. Use this to create or edit customer accounts. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Customer\` | — | — | | \`edit\` | \`shopify/Customer\` | \`gid://shopify/Customer/{id}\` | — | -### Discount +#### Discount + +[Discounts](/docs/api/admin-graphql/latest/objects/DiscountNode) are price reductions applied to products, orders, or shipping. Use this to create or edit discount codes and automatic discounts. Creating discounts requires specifying a discount type. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Discount\` | — | \`{ type: 'amount-off-product' \\| 'amount-off-order' \\| 'buy-x-get-y' \\| 'free-shipping' }\` | | \`edit\` | \`shopify/Discount\` | \`gid://shopify/Discount/{id}\` | — | -### Market +#### Market + +[Markets](/docs/api/admin-graphql/latest/objects/Market) are geographic regions with customized pricing, languages, and domains. Use this to create or edit markets for international selling. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Market\` | — | — | | \`edit\` | \`shopify/Market\` | \`gid://shopify/Market/{id}\` | — | -### Menu +#### Menu + +[Menus](/docs/api/admin-graphql/latest/objects/Menu) are navigation structures for the Online Store. Use this to create or edit menu structures and links. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Menu\` | — | — | | \`edit\` | \`shopify/Menu\` | \`gid://shopify/Menu/{id}\` | — | -### Metafield Definition +#### Metafield definition + +[Metafield definitions](/docs/api/admin-graphql/latest/objects/MetafieldDefinition) are schemas that define custom data fields for resources. Use this to create or edit metafield definitions that merchants can use to add structured data to products, customers, and other resources. + | Action | Type | Value | Data | |--------|------|-------|------| -| \`create\` | \`shopify/MetafieldDefinition\` | — | { ownerType: 'Product' } | -| \`edit\` | \`shopify/MetafieldDefinition\` | \`gid://shopify/MetafieldDefinition/{id}\` | { ownerType: 'Product' } | +| \`create\` | \`shopify/MetafieldDefinition\` | — | \`{ ownerType: 'Product' }\` | +| \`edit\` | \`shopify/MetafieldDefinition\` | \`gid://shopify/MetafieldDefinition/{id}\` | \`{ ownerType: 'Product' }\` | + +#### Metaobject + +[Metaobjects](/docs/api/admin-graphql/latest/objects/Metaobject) are custom structured data entries based on metaobject definitions. Use this to create or edit metaobject instances that store complex custom data. Requires a definition type. -### Metaobject | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Metaobject\` | — | \`{ type: 'shopify--color-pattern' }\` | | \`edit\` | \`shopify/Metaobject\` | \`gid://shopify/Metaobject/{id}\` | \`{ type: 'shopify--color-pattern' }\` | -### Metaobject Definition +#### Metaobject definition + +[Metaobject definitions](/docs/api/admin-graphql/latest/objects/MetaobjectDefinition) are schemas that define the structure for metaobjects. Use this to create or edit metaobject definitions that determine the fields and data types for custom structured data. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/MetaobjectDefinition\` | — | — | -| \`edit\` | \`shopify/MetaobjectDefinition\` | — | { type: 'my_metaobject_definition_type' } | +| \`edit\` | \`shopify/MetaobjectDefinition\` | — | \`{ type: 'my_metaobject_definition_type' }\` | + +#### Page + +[Pages](/docs/api/admin-graphql/latest/objects/Page) are static content pages for the Online Store. Use this to create or edit pages like About Us, Contact, or custom informational pages. -### Page | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Page\` | — | — | | \`edit\` | \`shopify/Page\` | \`gid://shopify/Page/{id}\` | — | -### Product +#### Product + +[Products](/docs/api/admin-graphql/latest/objects/Product) are items sold in the store with pricing, inventory, and variants. Use this to create or edit products. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/Product\` | — | — | | \`edit\` | \`shopify/Product\` | \`gid://shopify/Product/{id}\` | — | -### Product Variant +#### Product variant + +[Product variants](/docs/api/admin-graphql/latest/objects/ProductVariant) are specific combinations of product options like size and color. Use this to create or edit product variants. Creating variants requires a parent product ID. + | Action | Type | Value | Data | |--------|------|-------|------| | \`create\` | \`shopify/ProductVariant\` | — | \`{ productId: 'gid://shopify/Product/{id}' }\` | | \`edit\` | \`shopify/ProductVariant\` | \`gid://shopify/ProductVariant/{id}\` | \`{ productId: 'gid://shopify/Product/{id}' }\` | -> **Note**: To determine whether to use the \`shopify/ProductVariant\` \`edit\` intent or the \`shopify/Product\` \`edit\` intent, query the [\`product.hasOnlyDefaultVariant\`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Product#field-Product.fields.hasOnlyDefaultVariant) field. If the product has only the default variant (\`hasOnlyDefaultVariant\` is \`true\`), use the \`shopify/Product\` \`edit\` intent.`, +> Note: +> When editing products with variants, query the [\`product.hasOnlyDefaultVariant\`](/docs/api/admin-graphql/latest/objects/Product#field-Product.fields.hasOnlyDefaultVariant) field first. If \`true\`, then use the \`shopify/Product\` edit intent. If \`false\`, then use the \`shopify/ProductVariant\` edit intent for specific variants.`, type: 'IntentInvokeApi', }, - { - title: 'IntentAction', - description: `Supported actions that can be performed on resources. -- \`create\`: Opens a creation workflow for a new resource -- \`edit\`: Opens an editing workflow for an existing resource (requires \`value\` parameter)`, - type: 'IntentAction', - }, - { - title: 'IntentType', - description: `Supported resource types that can be targeted by intents.`, - type: 'IntentType', - }, - { - title: 'IntentQueryOptions', - description: `Options for invoking intents when using the query string format.`, - type: 'IntentQueryOptions', - }, { title: 'IntentResponse', - description: `Response object returned when the intent workflow completes.`, + description: `The result returned when an intent workflow completes. Check the \`code\` property to determine the outcome: +- \`'ok'\` - The merchant completed the workflow successfully. +- \`'error'\` - The workflow failed due to validation or other errors. +- \`'closed'\` - The merchant cancelled without completing.`, type: 'IntentResponse', }, ], @@ -147,29 +182,18 @@ Where: description: 'Intents for each Shopify resource type', exampleGroups: [ { - title: 'Article', + title: '', examples: [ { description: - 'Create a new article. Opens the article creation workflow.', - codeblock: { - title: 'Create article', - tabs: [ - { - code: './examples/create-article.js', - language: 'js', - }, - ], - }, - }, - { - description: 'Edit an existing article. Requires an article GID.', + 'Open the article editor for a selected blog post. This example retrieves the article GID from extension context, passes it to the edit intent, and handles both successful updates and cancellations.', codeblock: { - title: 'Edit article', + title: 'Edit an existing article', tabs: [ { - code: './examples/edit-article.js', - language: 'js', + title: 'jsx', + code: './examples/edit-article.jsx', + language: 'jsx', }, ], }, @@ -177,29 +201,32 @@ Where: ], }, { - title: 'Catalog', + title: '', examples: [ { description: - 'Create a new catalog. Opens the catalog creation workflow.', + 'Launch the catalog creation workflow to set up B2B customer groups or market-specific product collections. This example invokes the create intent, manages loading state, and displays success or cancellation feedback.', codeblock: { - title: 'Create catalog', + title: 'Create a new catalog', tabs: [ { - code: './examples/create-catalog.js', - language: 'js', + title: 'jsx', + code: './examples/create-catalog.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing catalog. Requires a catalog GID.', + description: + 'Open the catalog editor to adjust product assignments or market settings. This example retrieves the catalog GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit catalog', + title: 'Edit an existing catalog', tabs: [ { - code: './examples/edit-catalog.js', - language: 'js', + title: 'jsx', + code: './examples/edit-catalog.jsx', + language: 'jsx', }, ], }, @@ -207,30 +234,32 @@ Where: ], }, { - title: 'Collection', + title: '', examples: [ { description: - 'Create a new collection. Opens the collection creation workflow.', + 'Launch the collection creation workflow for organizing products on the storefront. This example invokes the create intent, tracks loading state, and displays feedback when the workflow completes.', codeblock: { - title: 'Create collection', + title: 'Create a new collection', tabs: [ { - code: './examples/create-collection.js', - language: 'js', + title: 'jsx', + code: './examples/create-collection.jsx', + language: 'jsx', }, ], }, }, { description: - 'Edit an existing collection. Requires a collection GID.', + 'Open the collection editor to modify products or automation rules. This example retrieves the collection GID, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit collection', + title: 'Edit an existing collection', tabs: [ { - code: './examples/edit-collection.js', - language: 'js', + title: 'jsx', + code: './examples/edit-collection.jsx', + language: 'jsx', }, ], }, @@ -238,29 +267,32 @@ Where: ], }, { - title: 'Customer', + title: '', examples: [ { description: - 'Create a new customer. Opens the customer creation workflow.', + 'Launch the customer creation form to add a new profile with contact details and addresses. This example invokes the create intent, awaits completion, and displays feedback based on the result code.', codeblock: { - title: 'Create customer', + title: 'Create a new customer', tabs: [ { - code: './examples/create-customer.js', - language: 'js', + title: 'jsx', + code: './examples/create-customer.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing customer. Requires a customer GID.', + description: + 'Open the customer editor to update contact information or tags. This example retrieves the customer GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit customer', + title: 'Edit an existing customer', tabs: [ { - code: './examples/edit-customer.js', - language: 'js', + title: 'jsx', + code: './examples/edit-customer.jsx', + language: 'jsx', }, ], }, @@ -268,29 +300,32 @@ Where: ], }, { - title: 'Discount', + title: '', examples: [ { description: - 'Create a new discount. Opens the discount creation workflow. Requires a discount type.', + 'Launch the discount creation form for setting up promotional campaigns. This example invokes the create intent, manages loading state, and displays feedback on completion.', codeblock: { - title: 'Create discount', + title: 'Create a new discount', tabs: [ { - code: './examples/create-discount.js', - language: 'js', + title: 'jsx', + code: './examples/create-discount.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing discount. Requires a discount GID.', + description: + 'Open the discount editor to adjust values or extend active dates. This example retrieves the discount GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit discount', + title: 'Edit an existing discount', tabs: [ { - code: './examples/edit-discount.js', - language: 'js', + title: 'jsx', + code: './examples/edit-discount.jsx', + language: 'jsx', }, ], }, @@ -298,29 +333,32 @@ Where: ], }, { - title: 'Market', + title: '', examples: [ { description: - 'Create a new market. Opens the market creation workflow.', + 'Launch the market creation workflow for international selling with region-specific configurations. This example invokes the create intent, manages loading state, and displays feedback on completion.', codeblock: { - title: 'Create market', + title: 'Create a new market', tabs: [ { - code: './examples/create-market.js', - language: 'js', + title: 'jsx', + code: './examples/create-market.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing market. Requires a market GID.', + description: + 'Open the market editor to adjust geographic coverage or pricing strategies. This example retrieves the market GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit market', + title: 'Edit an existing market', tabs: [ { - code: './examples/edit-market.js', - language: 'js', + title: 'jsx', + code: './examples/edit-market.jsx', + language: 'jsx', }, ], }, @@ -328,28 +366,32 @@ Where: ], }, { - title: 'Menu', + title: '', examples: [ { - description: 'Create a new menu. Opens the menu creation workflow.', + description: + 'Launch the menu creation workflow for storefront navigation headers or footers. This example invokes the create intent, tracks loading state, and displays feedback on completion.', codeblock: { - title: 'Create menu', + title: 'Create a new menu', tabs: [ { - code: './examples/create-menu.js', - language: 'js', + title: 'jsx', + code: './examples/create-menu.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing menu. Requires a menu GID.', + description: + 'Open the menu editor to reorganize navigation structure or update links. This example retrieves the menu GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit menu', + title: 'Edit an existing menu', tabs: [ { - code: './examples/edit-menu.js', - language: 'js', + title: 'jsx', + code: './examples/edit-menu.jsx', + language: 'jsx', }, ], }, @@ -357,30 +399,32 @@ Where: ], }, { - title: 'Metafield Definition', + title: '', examples: [ { description: - 'Create a new metafield definition. Opens the metafield definition creation workflow.', + 'Launch the metafield definition creator to add custom data fields to products, orders, or customers. This example invokes the create intent, manages loading state, and displays feedback on completion.', codeblock: { - title: 'Create metafield definition', + title: 'Create a new metafield definition', tabs: [ { - code: './examples/create-metafield-definition.js', - language: 'js', + title: 'jsx', + code: './examples/create-metafield-definition.jsx', + language: 'jsx', }, ], }, }, { description: - 'Edit an existing metafield definition. Requires a metafield definition GID.', + 'Open the metafield definition editor to modify validation rules or field descriptions. This example retrieves the definition GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit metafield definition', + title: 'Edit an existing metafield definition', tabs: [ { - code: './examples/edit-metafield-definition.js', - language: 'js', + title: 'jsx', + code: './examples/edit-metafield-definition.jsx', + language: 'jsx', }, ], }, @@ -388,30 +432,32 @@ Where: ], }, { - title: 'Metaobject', + title: '', examples: [ { description: - 'Create a new metaobject. Opens the metaobject creation workflow. Requires a type.', + 'Launch the metaobject creator to add a new entry to a custom content type. This example invokes the create intent, tracks loading state, and displays feedback on completion.', codeblock: { - title: 'Create metaobject', + title: 'Create a new metaobject', tabs: [ { - code: './examples/create-metaobject.js', - language: 'js', + title: 'jsx', + code: './examples/create-metaobject.jsx', + language: 'jsx', }, ], }, }, { description: - 'Edit an existing metaobject. Requires a metaobject GID.', + 'Open the metaobject editor to modify field values or resource references. This example retrieves the metaobject GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit metaobject', + title: 'Edit an existing metaobject', tabs: [ { - code: './examples/edit-metaobject.js', - language: 'js', + title: 'jsx', + code: './examples/edit-metaobject.jsx', + language: 'jsx', }, ], }, @@ -419,30 +465,32 @@ Where: ], }, { - title: 'Metaobject Definition', + title: '', examples: [ { description: - 'Create a new metaobject definition. Opens the metaobject definition creation workflow.', + 'Launch the metaobject definition creator to build reusable content types with custom field schemas. This example invokes the create intent, manages loading state, and displays feedback on completion.', codeblock: { - title: 'Create metaobject definition', + title: 'Create a new metaobject definition', tabs: [ { - code: './examples/create-metaobject-definition.js', - language: 'js', + title: 'jsx', + code: './examples/create-metaobject-definition.jsx', + language: 'jsx', }, ], }, }, { description: - 'Edit an existing metaobject definition. Requires a metaobject definition GID.', + 'Open the metaobject definition editor to add fields or update validation rules. This example retrieves the definition GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit metaobject definition', + title: 'Edit an existing metaobject definition', tabs: [ { - code: './examples/edit-metaobject-definition.js', - language: 'js', + title: 'jsx', + code: './examples/edit-metaobject-definition.jsx', + language: 'jsx', }, ], }, @@ -450,28 +498,32 @@ Where: ], }, { - title: 'Page', + title: '', examples: [ { - description: 'Create a new page. Opens the page creation workflow.', + description: + 'Launch the page creator to add an informational page like About Us or Shipping Policy. This example invokes the create intent, manages loading state, and displays feedback on completion.', codeblock: { - title: 'Create page', + title: 'Create a new page', tabs: [ { - code: './examples/create-page.js', - language: 'js', + title: 'jsx', + code: './examples/create-page.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing page. Requires a page GID.', + description: + 'Open the page editor to update content or SEO metadata. This example retrieves the page GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit page', + title: 'Edit an existing page', tabs: [ { - code: './examples/edit-page.js', - language: 'js', + title: 'jsx', + code: './examples/edit-page.jsx', + language: 'jsx', }, ], }, @@ -479,29 +531,32 @@ Where: ], }, { - title: 'Product', + title: '', examples: [ { description: - 'Create a new product. Opens the product creation workflow.', + 'Launch the product creation workflow to add a new item to the store catalog. This example invokes the create intent, tracks loading state, and displays feedback on completion.', codeblock: { - title: 'Create product', + title: 'Create a new product', tabs: [ { - code: './examples/create-product.js', - language: 'js', + title: 'jsx', + code: './examples/create-product.jsx', + language: 'jsx', }, ], }, }, { - description: 'Edit an existing product. Requires a product GID.', + description: + 'Open the product editor to update details, pricing, or images. This example retrieves the product GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit product', + title: 'Edit an existing product', tabs: [ { - code: './examples/edit-product.js', - language: 'js', + title: 'jsx', + code: './examples/edit-product.jsx', + language: 'jsx', }, ], }, @@ -509,30 +564,32 @@ Where: ], }, { - title: 'Product Variant', + title: '', examples: [ { description: - 'Create a new product variant. Opens the variant creation workflow. Requires a product ID.', + 'Launch the variant creation workflow to add size, color, or material options to a product. This example invokes the create intent, manages loading state, and displays feedback on completion.', codeblock: { - title: 'Create variant', + title: 'Create a new variant', tabs: [ { - code: './examples/create-variant.js', - language: 'js', + title: 'jsx', + code: './examples/create-variant.jsx', + language: 'jsx', }, ], }, }, { description: - 'Edit an existing product variant. Requires a variant GID.', + 'Open the variant editor to modify pricing, SKU, or inventory levels. This example retrieves the variant GID from extension context, invokes the edit intent, and handles the completion response.', codeblock: { - title: 'Edit variant', + title: 'Edit an existing variant', tabs: [ { - code: './examples/edit-variant.js', - language: 'js', + title: 'jsx', + code: './examples/edit-variant.jsx', + language: 'jsx', }, ], }, @@ -542,6 +599,28 @@ Where: ], }, related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + "- **Parse `ErrorIntentResponse.issues` array for specific feedback:** When `code: 'error'`, the `issues` array contains structured validation errors with field paths and messages. Use this to show specific error feedback rather than generic error messages.\n" + + "- **Distinguish `closed` from `error`:** `code: 'closed'` means the merchant cancelled, while `code: 'error'` means validation or save failures. Handle these differently. Closed isn't an error state.\n" + + '- **Query `product.hasOnlyDefaultVariant` before editing:** If the value is `false`, use the `shopify/ProductVariant` edit intent instead of `shopify/Product` to edit specific variants.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Some resources require `data` for create operations. Discounts need `{ type: 'amount-off-product' }`, variants need `{ productId: 'gid://...' }`, and metaobjects need `{ type: 'definition-type' }`. Missing required data causes the intent to fail.\n" + + "- MetaobjectDefinition edit requires `{ data: { type: 'definition-type' }}` instead of passing the GID in `value`. It's the only resource with this pattern.\n" + + "- Intent workflows pause your extension until completion. You can't run other operations while an intent is open.\n" + + "- The workflow UI can't be customized. Field order, labels, and validation messages are controlled by Shopify and can't be modified.\n" + + "- Your extension only receives the final result. Intermediate workflow state and partial saves aren't communicated back to your extension.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/intents/intents.ts b/packages/ui-extensions/src/surfaces/admin/api/intents/intents.ts index b3e3027601..d0bcaadf3f 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/intents/intents.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/intents/intents.ts @@ -1,43 +1,42 @@ /** - * User dismissed or closed the workflow without completing it. + * The response returned when a merchant closes or cancels the workflow without completing it. Check for this response to handle cancellation gracefully in your extension. */ export interface ClosedIntentResponse { + /** Indicates the workflow was closed without completion. When `'closed'`, the merchant exited the workflow before finishing. */ code?: 'closed'; } /** - * Successful intent completion. + * The response returned when a merchant successfully completes the workflow. Use this to access the created or updated resource data. */ export interface SuccessIntentResponse { + /** Indicates successful completion. When `'ok'`, the merchant completed the workflow and the resource was created or updated. */ code?: 'ok'; + /** Additional data returned by the workflow, such as the created or updated resource information with IDs and properties. */ data?: {[key: string]: unknown}; } /** - * Failed intent completion. + * The response returned when the workflow fails due to validation errors or other issues. Use this to display error messages and help merchants fix problems. */ export interface ErrorIntentResponse { + /** Indicates the workflow failed. When `'error'`, the workflow encountered validation errors or other issues that prevented completion. */ code?: 'error'; + /** A general error message describing what went wrong. Use this to display feedback when specific field errors aren't available. */ message?: string; + /** Specific validation issues or field errors. Present when validation fails on particular fields, allowing you to show targeted error messages. */ issues?: { - /** - * The path to the field with the issue. - */ + /** The path to the field that has an error (for example, `['product', 'title']`). Use this to identify which field caused the validation failure. */ path?: string[]; - /** - * The error message for the issue. - */ + /** A description of what's wrong with this field. Display this to help merchants understand how to fix the error. */ message?: string; - /** - * A code identifier for the issue. - */ + /** A machine-readable error code for this issue. Use this for programmatic error handling or logging. */ code?: string; }[]; } /** - * Result of an intent activity. - * Discriminated union representing all possible completion outcomes. + * The result of an intent workflow. Check the `code` property to determine the outcome: `'ok'` for success, `'error'` for failure, or `'closed'` if the merchant cancelled. */ export type IntentResponse = | SuccessIntentResponse @@ -45,22 +44,22 @@ export type IntentResponse = | ClosedIntentResponse; /** - * Activity handle for tracking intent workflow progress. + * A handle for tracking an in-progress intent workflow. */ export interface IntentActivity { /** - * A Promise that resolves when the intent workflow completes, returning the response. + * A Promise that resolves when the workflow completes. Await this to get the outcome and handle success, failure, or cancellation appropriately. */ complete?: Promise; } /** - * The action to perform on a resource. + * The type of operation to perform: creating a new resource or editing an existing one. */ export type IntentAction = 'create' | 'edit'; /** - * Supported resource types that can be targeted by intents. + * The types of Shopify resources that support intent-based creation and editing workflows. */ export type IntentType = | 'shopify/Article' @@ -78,43 +77,39 @@ export type IntentType = | 'shopify/ProductVariant'; /** - * Options for invoking intents when using the query string format. + * Additional parameters for intent invocation when using the string query format. Use these options to provide resource IDs for editing or pass required context data for resource creation. */ export interface IntentQueryOptions { /** - * The resource identifier for edit actions (e.g., 'gid://shopify/Product/123'). + * The resource identifier for edit operations (for example, `'gid://shopify/Product/123'`). Required when editing existing resources. Omit this for create operations. */ value?: string; /** - * Additional data required for certain intent types. - * For example: - * - Discount creation requires { type: 'amount-off-product' | 'amount-off-order' | 'buy-x-get-y' | 'free-shipping' } - * - ProductVariant creation requires { productId: 'gid://shopify/Product/123' } - * - Metaobject creation requires { type: 'shopify--color-pattern' } + * Additional context data required by specific intent types. For example, discount creation requires a discount type, variant creation requires a parent product ID, and [metaobject](/docs/apps/build/custom-data/metaobjects) creation requires a definition type. */ data?: {[key: string]: unknown}; } /** - * Structured description of an intent to invoke. + * A structured intent specification defining what workflow to launch. Use this format when you prefer object syntax over string query format. */ export interface IntentQuery extends IntentQueryOptions { /** - * The operation to perform on the target resource. + * The operation to perform: `'create'` for new resources or `'edit'` for existing ones. */ action: IntentAction; /** - * The resource type (e.g., 'shopify/Product'). + * The type of resource to create or edit (for example, `'shopify/Product'`). */ type: IntentType; } /** - * The invoke API for triggering intent workflows. + * The [`invoke` method](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/intents-api#invoke-method) in the Intents API launches a Shopify admin workflow for creating or editing resources, such as products, customers, or discounts. It opens a native admin interface, waits for the merchant to complete the workflow, and returns the result including any created or updated resource data. * - * @param intent - Either a string query or structured object describing the intent - * @param options - Optional parameters when using string query format - * @returns A Promise resolving to an activity handle for tracking the workflow + * @param intent - Either a string query (for example, `'create:shopify/Product'`) or structured object describing the intent + * @param options - Optional parameters when using string query format, such as resource IDs for editing or additional context data + * @returns A Promise resolving to an activity handle for tracking the workflow and accessing the completion result * * @example * ```javascript @@ -139,15 +134,15 @@ export interface IntentInvokeApi { } /** - * Intent information provided to the receiver of an intent. + * The `Intents` object provides methods for launching standardized Shopify workflows to create or edit resources. Intents enable your extension to trigger native admin interfaces for products, customers, discounts, and other resources, then receive the results when merchants complete the workflow. */ export interface Intents { /** - * The URL that was used to launch the intent. + * The URL that launched the current intent workflow, if your extension was opened through an intent. Use this to determine how your extension was invoked and access any parameters passed in the URL. */ launchUrl?: string | URL; /** - * Invoke an intent workflow to create or edit Shopify resources. + * Launches an intent workflow for creating or editing Shopify resources. Returns a handle that resolves when the merchant completes, cancels, or encounters an error in the workflow. Use this to initiate resource creation or editing without building custom forms. */ invoke?: IntentInvokeApi; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/data.ts b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/data.ts index 1a82329fa6..2bc5e68554 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/data.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/data.ts @@ -1,21 +1,41 @@ import type {SupportedDefinitionType} from './metafields'; +/** + * A [metafield](/docs/apps/build/metafields) associated with an order routing rule. Use metafields to persist settings that control how your order routing function behaves, such as location preferences, routing criteria, or custom fulfillment rules. + */ interface Metafield { + /** The unique global identifier (GID) for this metafield. Present for existing metafields, absent for new ones. Use this ID to reference the metafield in GraphQL operations. */ id?: string | null; + /** The unique key identifying this metafield within its namespace. This key determines how you access the metafield value (for example, `'preferred_location'` or `'routing_priority'`). */ key: string; + /** The metafield value stored as a string. Parse this value according to the metafield type to use it in your settings UI. */ value?: string | null; + /** The namespace that organizes related metafields together. Use consistent namespaces to group related settings for your order routing rule. */ namespace?: string; + /** The metafield [definition type](/docs/apps/build/metafields/list-of-data-types) that specifies the value format and validation rules. Use this to determine how to parse and display the value. */ type?: SupportedDefinitionType; } +/** + * An order routing rule configuration that determines how orders are routed to fulfillment locations. Use this to access the rule's current settings and populate your configuration interface. + */ interface OrderRoutingRule { + /** The display label for the order routing rule shown to merchants in the admin. Use this to identify the rule in lists and settings pages. */ label: string; + /** A description explaining the rule's purpose and how it routes orders. Use this to help merchants understand what the rule does. */ description: string; + /** The unique global identifier (GID) for the order routing rule. Use this ID to associate configuration changes with the correct rule. */ id: string; + /** The priority order for rule evaluation when multiple rules exist. Lower numbers are evaluated first (for example, a rule with priority 1 runs before priority 2). Use this to understand the rule's position in the evaluation sequence. */ priority?: number; + /** An array of [metafields](/docs/apps/build/metafields) that store the routing rule's configuration values. Use these metafields to populate your settings UI with the current rule configuration. */ metafields: Metafield[]; } +/** + * The `data` object exposed to order routing rule extensions in the `admin.settings.order-routing-rule.render` target. Use this to access the current rule configuration and build your settings interface. + */ export interface Data { + /** The order routing rule being configured by the merchant. Use this to access the rule's properties and populate your settings UI with existing configuration values. */ rule: OrderRoutingRule; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/configure-location-priority.jsx b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/configure-location-priority.jsx new file mode 100644 index 0000000000..a9d64f11d3 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/configure-location-priority.jsx @@ -0,0 +1,46 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [preferred, setPreferred] = useState('gid://shopify/Location/123456789'); + const [fallback, setFallback] = useState('gid://shopify/Location/987654321'); + + const handleSave = () => { + shopify.applyMetafieldsChange([ + { + type: 'updateMetafield', + namespace: 'routing', + key: 'preferred_location', + value: preferred, + valueType: 'single_line_text_field', + }, + { + type: 'updateMetafield', + namespace: 'routing', + key: 'fallback_location', + value: fallback, + valueType: 'single_line_text_field', + }, + ]); + }; + + return ( + + setPreferred(value)} + /> + setFallback(value)} + /> + Save Location Priority + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/remove-deprecated-settings.jsx b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/remove-deprecated-settings.jsx new file mode 100644 index 0000000000..b674dea291 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/remove-deprecated-settings.jsx @@ -0,0 +1,35 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [removed, setRemoved] = useState(false); + + const handleRemove = () => { + const deprecatedKeys = ['old_setting', 'legacy_config']; + + const changes = deprecatedKeys.map((key) => ({ + type: 'removeMetafield', + namespace: 'routing', + key, + })); + + shopify.applyMetafieldsChange(changes); + setRemoved(true); + }; + + return ( + + + Rule priority: {data.rule.priority} + Current settings: {data.rule.metafields.length} + Remove Deprecated Settings + {removed && Deprecated settings removed} + + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/set-routing-criteria.jsx b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/set-routing-criteria.jsx new file mode 100644 index 0000000000..b3bc95c830 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/examples/set-routing-criteria.jsx @@ -0,0 +1,57 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [distance, setDistance] = useState('50'); + const [enableInventory, setEnableInventory] = useState(true); + + const handleSave = () => { + shopify.applyMetafieldsChange([ + { + type: 'updateMetafield', + namespace: 'routing', + key: 'max_distance_km', + value: distance, + valueType: 'number_integer', + }, + { + type: 'updateMetafield', + namespace: 'routing', + key: 'enable_inventory_check', + value: String(enableInventory), + valueType: 'boolean', + }, + { + type: 'updateMetafield', + namespace: 'routing', + key: 'excluded_zip_codes', + value: JSON.stringify(['10001', '90210']), + valueType: 'json', + }, + ]); + }; + + return ( + + + setDistance(value)} + /> + setEnableInventory(checked)} + > + Enable inventory check + + Save Routing Criteria + + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/metafields.ts b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/metafields.ts index 5f13c3552d..eafb820c23 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/metafields.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/metafields.ts @@ -43,25 +43,49 @@ const supportedDefinitionTypes = [ 'list.weight', ] as const; +/** + * A metafield update or creation operation. Use this to set or modify metafield values that store order routing rule configuration data. + */ interface MetafieldUpdateChange { + /** Identifies this as an update operation. Always set to `'updateMetafield'` for updates. */ type: 'updateMetafield'; + /** The unique key identifying the metafield within its namespace. Use descriptive keys that indicate the setting's purpose (for example, `'preferred_location'` or `'routing_priority'`). */ key: string; + /** The namespace that organizes related metafields. When omitted, a default namespace is assigned. Use consistent namespaces to group related settings. */ namespace?: string; + /** The metafield value to store. Can be a string or number depending on your configuration needs. */ value: string | number; + /** The [data type](/docs/apps/build/metafields/list-of-data-types) that defines how the value is formatted and validated. When omitted, preserves the existing type for updates or uses a default for new metafields. Choose a type that matches your value format. */ valueType?: SupportedDefinitionType; } +/** + * A metafield removal operation. Use this to delete metafields that are no longer needed for your order routing rule configuration. + */ interface MetafieldRemoveChange { + /** Identifies this as a removal operation. Always set to `'removeMetafield'` for deletions. */ type: 'removeMetafield'; + /** The unique key of the metafield to remove. Must match the key used when the metafield was created. */ key: string; + /** The namespace containing the metafield to remove. Required to ensure the correct metafield is targeted, as the same key can exist in different namespaces. */ namespace: string; } +/** + * One or more metafield change operations to apply to order routing rule settings. Can be a single change or an array of changes for batch operations. Use arrays to apply multiple changes at once. + */ type MetafieldsChange = | MetafieldUpdateChange | MetafieldRemoveChange | MetafieldUpdateChange[] | MetafieldRemoveChange[]; +/** + * The supported [metafield definition types](/docs/apps/build/metafields/list-of-data-types) for storing order routing rule configuration data. Use these types to specify how metafield values should be formatted, validated, and displayed. Types prefixed with `list.` store arrays of values, while other types store single values. + */ export type SupportedDefinitionType = (typeof supportedDefinitionTypes)[number]; + +/** + * A function that applies metafield changes to order routing rule settings. Call this function with one or more change operations to update or remove metafields in batch. Use batch operations to apply multiple configuration changes efficiently. + */ export type ApplyMetafieldsChange = (changes: MetafieldsChange[]) => void; diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.doc.ts index b23ccb869a..20319a6321 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.doc.ts @@ -3,19 +3,89 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Order Routing Rule API', description: - 'This API is available to extensions that render in the order routing settings using the `admin.settings.order-routing-rule.render` target.', + 'The Order Routing Rule API provides access to [order routing rule](/docs/apps/build/orders-fulfillment/order-routing-apps) configuration and settings management. Use this API to build custom configuration interfaces for order routing rules that determine fulfillment locations.', isVisualComponent: false, type: 'API', + requires: + 'the [`FunctionSettings`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/forms/functionsettings) component.', + defaultExample: { + description: + 'Set preferred and fallback fulfillment locations with [text field](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/forms/textfield) inputs. This example applies two metafield changes in a single batch operation to configure location priority for order routing.', + codeblock: { + title: 'Configure location priority', + tabs: [ + { + title: 'jsx', + code: './examples/configure-location-priority.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'OrderRoutingRuleApi', - description: '', + title: 'Properties', + description: + 'The `OrderRoutingRuleApi` object provides access to order routing rule data and configuration. Access the following properties on the `OrderRoutingRuleApi` object to interact with the current order routing rule context in the `admin.settings.order-routing-rule.render` target.', type: 'OrderRoutingRuleApi', }, ], + examples: { + description: 'Configure order routing rules', + examples: [ + { + description: + 'Batch remove outdated metafields from routing configuration. This example maps deprecated keys to removal operations, displays current rule stats, and shows a success banner after cleanup.', + codeblock: { + title: 'Remove deprecated settings', + tabs: [ + { + title: 'jsx', + code: './examples/remove-deprecated-settings.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Configure maximum distance, inventory checking, and excluded zip codes in one save. This example demonstrates using number fields, checkboxes, and JSON storage for complex routing criteria.', + codeblock: { + title: 'Set routing criteria', + tabs: [ + { + title: 'jsx', + code: './examples/set-routing-criteria.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Batch metafield changes for atomic updates:** `applyMetafieldsChange` accepts an array of change objects. Pass multiple changes in a single call to ensure all changes succeed or all fail together.\n' + + "- **Check operation result type:** `applyMetafieldsChange` returns `{ type: 'success' }` or `{ type: 'error', message: string }`. Errors don't throw, so always check the returned `type`.", + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Metafields have [size limits](/docs/apps/build/metafields/metafield-limits). Individual values can't exceed 256KB, and total metafield storage per rule is limited.\n" + + "- Rule priority is read-only. Evaluation order can't be modified through the settings interface. Merchants manage priority through the main rules interface.\n" + + '- Batch operations are all-or-nothing. If any metafield change in the array fails validation, the entire batch fails and no changes apply.\n' + + '- Metafield changes apply immediately. They persist right away without waiting for merchants to save the rule.', + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.ts b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.ts index d3ef37e0f8..7f0c28cf24 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/order-routing-rule/order-routing-rule.ts @@ -1,11 +1,16 @@ -import type {StandardApi} from '../standard/standard'; +import type {StandardRenderingExtensionApi} from '../standard/standard-rendering'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import {ApplyMetafieldsChange} from './metafields'; import {Data} from './data'; +/** + * The `OrderRoutingRuleApi` object provides methods for configuring order routing rules. Access the following properties on the `OrderRoutingRuleApi` object to manage rule settings and metafields. + */ export interface OrderRoutingRuleApi - extends StandardApi { + extends StandardRenderingExtensionApi { + /** Updates or removes [metafields](/docs/apps/build/metafields) that store order routing rule configuration. */ applyMetafieldsChange: ApplyMetafieldsChange; + /** The order routing rule being configured, including its metadata and associated [metafields](/docs/apps/build/metafields). */ data: Data; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/direct-api.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/direct-api.js deleted file mode 100644 index 2a99855671..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/direct-api.js +++ /dev/null @@ -1,73 +0,0 @@ -const r = await fetch('shopify:admin/api/graphql.json', { - method: 'POST', - body: JSON.stringify({ - query: ` - query GetOrders($first: Int!) { - orders(first: $first) { - edges { - node { - id - name - customer { - displayName - } - originalTotalPriceSet { - shopMoney { - amount - } - } - displayFulfillmentStatus - displayFinancialStatus - unpaid - } - } - } - } - `, - variables: {first: 10}, - }), -}); -const {data} = await r.json(); -const orderData = data.orders.edges; - -const selected = await picker({ - heading: 'Select orders', - multiple: true, - headers: [ - {title: 'Order'}, - {title: 'Customer'}, - {title: 'Total', type: 'number'}, - ], - items: orderData.map((order) => { - const { - id, - name, - customer, - originalTotalPriceSet: {shopMoney}, - displayFulfillmentStatus, - displayFinancialStatus, - } = order.node; - - return { - id, - heading: name, - data: [customer.displayName, `$${shopMoney.amount}`], - badges: [ - { - content: displayFulfillmentStatus, - tone: displayFulfillmentStatus === 'FULFILLED' ? '' : 'attention', - progress: - displayFulfillmentStatus === 'FULFILLED' - ? 'complete' - : 'incomplete', - }, - { - content: displayFinancialStatus, - tone: displayFinancialStatus === 'PENDING' ? 'warning' : '', - progress: - displayFinancialStatus === 'PENDING' ? 'incomplete' : 'complete', - }, - ], - }; - }), -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/direct-api.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/direct-api.jsx new file mode 100644 index 0000000000..a2d6fcfa58 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/direct-api.jsx @@ -0,0 +1,42 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const handlePick = async () => { + const r = await fetch('shopify:admin/api/graphql.json', { + method: 'POST', + body: JSON.stringify({ + query: `query GetOrders($first: Int!) { + orders(first: $first) { + edges { + node { + id + name + } + } + } + }`, + variables: {first: 10}, + }), + }); + + const {data} = await r.json(); + + await shopify.picker({ + heading: 'Select orders', + items: data.orders.edges.map((edge) => ({ + id: edge.node.id, + heading: edge.node.name, + })), + }); + }; + + return ( + + Open Order Picker + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/disabled.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/disabled.js deleted file mode 100644 index 7dca3584f2..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/disabled.js +++ /dev/null @@ -1,16 +0,0 @@ -const pickerInstance = await picker({ - heading: 'Disabled items', - items: [ - { - id: '1', - heading: 'Item 1', - disabled: true, - }, - { - id: '2', - heading: 'Item 2', - }, - ], -}); - -const selected = await pickerInstance.selected; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/disabled.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/disabled.jsx new file mode 100644 index 0000000000..7fe803c005 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/disabled.jsx @@ -0,0 +1,23 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const handlePick = async () => { + await shopify.picker({ + heading: 'Select items', + items: [ + {id: '1', heading: 'Available item'}, + {id: '2', heading: 'Disabled item', disabled: true}, + ], + }); + }; + + return ( + + Open Picker + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/minimal.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/minimal.js deleted file mode 100644 index 4e449e0dc6..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/minimal.js +++ /dev/null @@ -1,16 +0,0 @@ -const pickerInstance = await picker({ - heading: 'Select an item', - headers: [{title: 'Main heading'}], - items: [ - { - id: '1', - heading: 'Item 1', - }, - { - id: '2', - heading: 'Item 2', - }, - ], -}); - -const selected = await pickerInstance.selected; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/minimal.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/minimal.jsx new file mode 100644 index 0000000000..7826d4bd6f --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/minimal.jsx @@ -0,0 +1,37 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const pickerInstance = await shopify.picker({ + heading: 'Select an item', + headers: [{title: 'Main heading'}], + items: [ + { + id: '1', + heading: 'Item 1', + }, + { + id: '2', + heading: 'Item 2', + }, + ], + }); + + const result = await pickerInstance.selected; + setSelected(result); + }; + + return ( + + Open Picker + {selected && {selected.length} items selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-limit.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-limit.js deleted file mode 100644 index cffa94cfe0..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-limit.js +++ /dev/null @@ -1,21 +0,0 @@ -const pickerInstance = await picker({ - heading: 'Select items (up to 2)', - multiple: 2, - headers: [{title: 'Main heading'}], - items: [ - { - id: '1', - heading: 'Item 1', - }, - { - id: '2', - heading: 'Item 2', - }, - { - id: '3', - heading: 'Item 3', - }, - ], -}); - -const selected = await pickerInstance.selected; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-limit.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-limit.jsx new file mode 100644 index 0000000000..4dcb0ba861 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-limit.jsx @@ -0,0 +1,26 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const handlePick = async () => { + await shopify.picker({ + heading: 'Select items (up to 2)', + multiple: 2, + headers: [{title: 'Main heading'}], + items: [ + {id: '1', heading: 'Item 1'}, + {id: '2', heading: 'Item 2'}, + {id: '3', heading: 'Item 3'}, + ], + }); + }; + + return ( + + Open Picker + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-true.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-true.js deleted file mode 100644 index a0f785925b..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-true.js +++ /dev/null @@ -1,21 +0,0 @@ -const pickerInstance = await picker({ - heading: 'Select items', - multiple: true, - headers: [{title: 'Main heading'}], - items: [ - { - id: '1', - heading: 'Item 1', - }, - { - id: '2', - heading: 'Item 2', - }, - { - id: '3', - heading: 'Item 3', - }, - ], -}); - -const selected = await pickerInstance.selected; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-true.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-true.jsx new file mode 100644 index 0000000000..3db8b53b39 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/multiple-true.jsx @@ -0,0 +1,25 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const handlePick = async () => { + await shopify.picker({ + heading: 'Select items', + multiple: true, + items: [ + {id: '1', heading: 'Item 1'}, + {id: '2', heading: 'Item 2'}, + {id: '3', heading: 'Item 3'}, + ], + }); + }; + + return ( + + Open Picker + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/preselected.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/preselected.js deleted file mode 100644 index 090ec080db..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/preselected.js +++ /dev/null @@ -1,16 +0,0 @@ -const pickerInstance = await picker({ - heading: 'Preselected items', - items: [ - { - id: '1', - heading: 'Item 1', - selected: true, - }, - { - id: '2', - heading: 'Item 2', - }, - ], -}); - -const selected = await pickerInstance.selected; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/preselected.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/preselected.jsx new file mode 100644 index 0000000000..f5a2903a50 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/preselected.jsx @@ -0,0 +1,23 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const handlePick = async () => { + await shopify.picker({ + heading: 'Select items', + items: [ + {id: '1', heading: 'Item 1', selected: true}, + {id: '2', heading: 'Item 2'}, + ], + }); + }; + + return ( + + Open Picker + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/template-picker.js b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/template-picker.js deleted file mode 100644 index 052efde1a0..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/template-picker.js +++ /dev/null @@ -1,37 +0,0 @@ -const {picker} = useApi(TARGET); - -const pickerInstance = await picker({ - heading: 'Select a template', - multiple: false, - headers: [ - {title: 'Templates'}, - {title: 'Created by'}, - {title: 'Times used', type: 'number'}, - ], - items: [ - { - id: '1', - heading: 'Full width, 1 column', - data: ['Karine Ruby', '0'], - badges: [{content: 'Draft', tone: 'info'}, {content: 'Marketing'}], - }, - { - id: '2', - heading: 'Large graphic, 3 column', - data: ['Russell Winfield', '5'], - badges: [ - {content: 'Published', tone: 'success'}, - {content: 'New feature'}, - ], - selected: true, - }, - { - id: '3', - heading: 'Promo header, 2 column', - data: ['Russel Winfield', '10'], - badges: [{content: 'Published', tone: 'success'}], - }, - ], -}); - -const selected = await pickerInstance.selected; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/examples/template-picker.jsx b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/template-picker.jsx new file mode 100644 index 0000000000..703875cca4 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/examples/template-picker.jsx @@ -0,0 +1,58 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePickTemplate = async () => { + const pickerInstance = await shopify.picker({ + heading: 'Select a template', + multiple: false, + headers: [ + {title: 'Templates'}, + {title: 'Created by'}, + {title: 'Times used', type: 'number'}, + ], + items: [ + { + id: '1', + heading: 'Full width, 1 column', + data: ['Karine Ruby', '0'], + badges: [{content: 'Draft', tone: 'info'}, {content: 'Marketing'}], + }, + { + id: '2', + heading: 'Large graphic, 3 column', + data: ['Russell Winfield', '5'], + badges: [ + {content: 'Published', tone: 'success'}, + {content: 'New feature'}, + ], + selected: true, + }, + { + id: '3', + heading: 'Promo header, 2 column', + data: ['Russel Winfield', '10'], + badges: [{content: 'Published', tone: 'success'}], + }, + ], + }); + + const result = await pickerInstance.selected; + setSelected(result); + }; + + return ( + + Choose Template + {selected && selected.length > 0 && ( + Selected template: {selected[0]} + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/picker.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/picker/picker.doc.ts index 7dfef2a097..5f8119b2b3 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/picker.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/picker.doc.ts @@ -3,24 +3,27 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Picker API', overviewPreviewDescription: 'Opens a Picker in your app', - description: `The Picker API provides a search-based interface to help users find and select one or more resources that you provide, such as product reviews, email templates, or subscription options, and then returns the selected resource ids to your extension. + description: `The Picker API lets merchants search for and select items from your app-specific data, such as product reviews, email templates, or subscription options. Use this API to build custom selection dialogs with your own data structure, badges, and thumbnails. The picker returns the IDs of selected items. > Tip: -> If you are picking products, product variants, or collections, you should use the [Resource Picker](resource-picker) API instead.`, +> If you need to pick Shopify products, variants, or collections, use the [Resource Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) instead.`, isVisualComponent: true, category: 'Target APIs', subCategory: 'Utility APIs', thumbnail: 'picker.png', requires: - 'an Admin [block](/docs/api/admin-extensions/unstable/extension-targets#block-locations), [action](/docs/api/admin-extensions/unstable/extension-targets#action-locations), or [print](/docs/api/admin-extensions/unstable/extension-targets#print-locations) extension.', + 'an Admin UI [block, action, or print](/docs/api/admin-extensions/{API_VERSION}#building-your-extension) extension.', defaultExample: { + description: + 'Build a custom picker for email templates with multiple columns and status badges. This example shows defining column headers, populating items with searchable data fields, adding visual status indicators, and handling the selection promise. Use this pattern for app-specific resources like templates, product reviews, or subscription options where you need custom data structures beyond standard Shopify resources.', image: 'picker.png', codeblock: { - title: 'Picker', + title: 'Select email templates', tabs: [ { - code: './examples/template-picker.js', - language: 'js', + title: 'jsx', + code: './examples/template-picker.jsx', + language: 'jsx', }, ], }, @@ -28,85 +31,103 @@ const data: ReferenceEntityTemplateSchema = { definitions: [ { title: 'picker', - description: `The \`picker\` API is a function that accepts an options argument for configuration and returns a Promise that resolves to the picker instance once the picker modal is opened.`, + description: `The \`picker\` function opens a custom selection dialog with your app-specific data. It accepts configuration options to define the picker's heading, items, headers, and selection behavior. It returns a Promise that resolves to a \`Picker\` object with a \`selected\` property for accessing the merchant's selection.`, type: 'PickerApi', }, ], examples: { - description: 'Pickers with different options', + description: 'Examples that demonstrate how to use the Picker API.', examples: [ { description: - "Minimal required fields picker configuration.\n\nIf you don't provide the required options (`heading` and `items`), the picker will not open and an error will be logged.", + 'Disable specific picker items to prevent selection while keeping them visible for context. This example shows setting `disabled: true` on individual items to mark them as non-selectable. This is useful for showing all available options while preventing selection of incompatible resources, templates currently being edited by others, or deprecated features that require upgrades.', codeblock: { - title: 'Simple picker', + title: 'Disable specific items', tabs: [ { - code: './examples/minimal.js', - language: 'js', + title: 'jsx', + code: './examples/disabled.jsx', + language: 'jsx', }, ], }, }, { description: - 'Setting a specific number of selectable items. In this example, the user can select up to 2 items.', + 'Limit selection to a maximum number of items by setting `multiple: 2` in the picker options. This example shows restricting selection to exactly 2 items. Use this when your feature has hard constraints, such as A/B test variants needing exactly two options, comparison views with fixed slots, or integration mappings that support a specific connection count.', codeblock: { - title: 'Limited selectable items', + title: 'Limit selection count', tabs: [ { - code: './examples/multiple-limit.js', - language: 'js', + title: 'jsx', + code: './examples/multiple-limit.jsx', + language: 'jsx', }, ], }, }, { - description: 'Setting an unlimited number of selectable items.', + description: + 'Open the picker with items already selected by setting `selected: true` on individual items. This example shows pre-marking items as selected when the picker opens. Use this for edit workflows where you need to show what resources are already associated with a configuration, such as automation rule triggers or notification recipients. Merchants can modify the selection before confirming.', codeblock: { - title: 'Unlimited selectable items', + title: 'Preselect items', tabs: [ { - code: './examples/multiple-true.js', - language: 'js', + title: 'jsx', + code: './examples/preselected.jsx', + language: 'jsx', }, ], }, }, { description: - 'Providing preselected items in the picker. These will be selected when the picker opens but can be deselected by the user.', + 'Allow unlimited selection by setting `multiple: true` without a numeric limit. This example shows enabling flexible multi-selection where merchants control how many items to choose. This is useful for bulk operations, mass notification sending, export tools, or tag management where selection quantity depends on merchant needs without artificial constraints.', codeblock: { - title: 'Preselected items', + title: 'Select unlimited items', tabs: [ { - code: './examples/preselected.js', - language: 'js', + title: 'jsx', + code: './examples/multiple-true.jsx', + language: 'jsx', }, ], }, }, { description: - 'Providing disabled items in the picker. These will be disabled and cannot be selected by the user.', + "Populate the picker with data from the [GraphQL Admin API](/docs/api/admin-graphql). This example fetches order data when the button is clicked, maps results to picker items, and opens the picker with the returned data. Use this pattern for Shopify data that isn't available through the Resource Picker API, such as orders, draft orders, or fulfillments.", codeblock: { - title: 'Disabled items', + title: 'Use GraphQL data', tabs: [ { - code: './examples/disabled.js', - language: 'js', + title: 'jsx', + code: './examples/direct-api.jsx', + language: 'jsx', }, ], }, }, ], }, - related: [ + related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Handle undefined return on cancellation:** When merchants cancel or close the picker, it returns `undefined` rather than an empty array. Check for `undefined` explicitly to distinguish cancellation from empty selection.\n' + + "- **Disable items to prevent modification:** Use the `disabled` property on items combined with `initialSelectionIds` to create preselected items that merchants can't deselect.", + }, { - name: 'Resource Picker API', - subtitle: 'APIs', - url: 'resource-picker', - type: 'pickaxe-3', + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- The Picker API only supports app-specific data. It can't display Shopify resources like products or variants. Use [Resource Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) for Shopify resources.\n" + + "- Picker items don't support hierarchical or nested structures. All items appear in a flat list.\n" + + "- The picker can't be customized with additional filters, search operators, or sorting beyond what merchants type in the search field.", }, ], }; diff --git a/packages/ui-extensions/src/surfaces/admin/api/picker/picker.ts b/packages/ui-extensions/src/surfaces/admin/api/picker/picker.ts index 9c3ffc3f69..c53c9d58dd 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/picker/picker.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/picker/picker.ts @@ -1,82 +1,112 @@ +/** + * A handle returned when opening a picker dialog. Use this to access the merchant's selection after they confirm or cancel the picker. + */ interface Picker { /** - * A Promise that resolves with the selected item IDs when the user presses the "Select" button in the picker. + * A Promise that resolves with an array of selected item IDs when the merchant presses the **Select** button, or `undefined` if they cancel. Await this Promise to handle the selection result. */ selected: Promise; } +/** + * The configuration for a table column header in the picker. Each header creates a labeled column that displays corresponding data from items. + */ interface Header { /** - * The content to display in the table column header. + * The label text displayed at the top of the table column. Use clear, concise labels that describe the data in that column (for example, "Price", "Status", "Last Updated"). */ content?: string; /** - * The type of data to display in the column. The type is used to format the data in the column. - * If the type is 'number', the data in the column will be right-aligned, this should be used when referencing currency or numeric values. - * If the type is 'string', the data in the column will be left-aligned. + * The data type that controls column formatting and text alignment. Use `'number'` for currency, prices, or numeric values (displays right-aligned), or `'string'` for text content (displays left-aligned). * @defaultValue 'string' */ type?: 'string' | 'number'; } +/** + * The configuration options for the custom picker dialog. Define the picker's appearance, selection behavior, and data structure. + */ interface PickerOptions { /** - * The heading of the picker. This is displayed in the title of the picker modal. + * The heading displayed at the top of the picker modal. Use a clear, descriptive title that tells merchants what they're selecting. */ heading: string; /** - * The data headers for the picker. These are used to display the table headers in the picker modal. + * The selection mode for the picker. Pass `true` to allow unlimited selections, `false` for single-item selection only, or a number to set a maximum selection limit (for example, `3` allows up to three items). */ multiple?: boolean | number; /** - * The items to display in the picker. These are used to display the rows in the picker modal. + * The list of items that merchants can select from. Each item appears as a row in the picker table. */ items: Item[]; /** - * The data headers for the picker. These are used to display the table headers in the picker modal. + * The column headers for the picker table. Define headers to label and organize the data columns displayed for each item. The header order determines the column layout. */ headers?: Header[]; } +/** + * The visual tone for picker badges indicating status or importance. Use different tones to communicate urgency or state: `'info'` for neutral information, `'success'` for positive states, `'warning'` for caution, or `'critical'` for urgent issues. + */ export type Tone = 'info' | 'success' | 'warning' | 'critical'; + +/** + * The progress state for picker badges showing completion status. Use this to indicate how complete an item is: `'incomplete'` for not started, `'partiallyComplete'` for in progress, or `'complete'` for finished. + */ export type Progress = 'incomplete' | 'partiallyComplete' | 'complete'; + +/** + * A single data point that can appear in a picker table cell. Can be text, a number, or undefined if the cell should be empty. + */ type DataPoint = string | number | undefined; +/** + * A badge displayed next to an item in the picker to show status or progress. Use badges to highlight important item attributes or states that affect selection decisions. + */ interface PickerBadge { + /** The text content of the badge. Keep this short and descriptive (for example, "Draft", "Active", "Incomplete"). */ content: string; + /** The visual tone indicating status or importance. Choose a tone that matches the badge's meaning. */ tone?: Tone; + /** The progress indicator for the badge. Use this to show completion status for items that have progress states. */ progress?: Progress; } +/** + * An individual item that merchants can select in the picker. Each item appears as a row in the picker table. + */ interface Item { /** - * The unique identifier of the item. This will be returned by the picker if selected. + * The unique identifier for this item. This ID is returned in the selection array when the merchant selects this item. Use an ID that helps you identify the item in your system (for example, template IDs, review IDs, or custom option keys). */ id: string; /** - * The primary content to display in the first column of the row. + * The primary text displayed in the first column. This is typically the item's name or title and is the most prominent text in the row. */ heading: string; /** - * The additional content to display in the second and third columns of the row, if provided. + * Additional data displayed in subsequent columns after the heading. Each value appears in its own column, and the order must match the `headers` array. For example, if headers are `["Price", "Status"]`, then data would be `[19.99, "Active"]`. */ data?: DataPoint[]; /** - * Whether the item is disabled or not. If the item is disabled, it cannot be selected. + * Whether the item can be selected. When `true`, the item is disabled and can't be selected. Disabled items appear grayed out and merchants can't choose them. Use this for items that are unavailable or don't meet selection criteria. * @defaultValue false */ disabled?: boolean; /** - * Whether the item is selected or not when the picker is opened. The user may deselect the item if it is preselected. + * Whether the item is preselected when the picker opens. When `true`, the item appears selected by default. Merchants can still deselect preselected items. Use this to show current selections or suggest default choices. */ selected?: boolean; /** - * The badges to display in the first column of the row. These are used to display additional information about the item, such as progress of an action or tone indicating the status of that item. + * Status or context badges displayed next to the heading in the first column. Use badges to highlight item state, completion status, or other important attributes (for example, "Draft", "Published", "Incomplete"). */ badges?: PickerBadge[]; /** - * The thumbnail to display at the start of the row. This is used to display an image or icon for the item. + * A small preview image or icon displayed at the start of the row. Thumbnails help merchants visually identify items at a glance. Provide a URL to the image file. */ thumbnail?: {url: string}; } +/** + * The `picker` function opens a custom selection dialog with your app-specific data. It accepts configuration options to define the picker's heading, items, headers, and selection behavior. It returns a Promise that resolves to a `Picker` object with a `selected` property for accessing the merchant's selection. + */ export type PickerApi = (options: PickerOptions) => Promise; diff --git a/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/custom-product-labels.jsx b/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/custom-product-labels.jsx new file mode 100644 index 0000000000..022bff7264 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/custom-product-labels.jsx @@ -0,0 +1,44 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [additionalCount, setAdditionalCount] = useState(0); + + const handleSelectMore = async () => { + const additionalProducts = await shopify.resourcePicker({ + type: 'product', + multiple: 10, + action: 'add', + }); + + if (additionalProducts) { + setAdditionalCount(additionalProducts.length); + } + }; + + const handleGenerate = async () => { + const productIds = data.selected.map((item) => item.id); + + const response = await fetch('/api/generate-labels', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({productIds}), + }); + + const {printUrl} = await response.json(); + return printUrl; + }; + + return ( + + {data.selected.length} products selected + Add More Products + {additionalCount > 0 && +{additionalCount} additional} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/generate-packing-slip.jsx b/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/generate-packing-slip.jsx new file mode 100644 index 0000000000..4800b0ea4e --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/generate-packing-slip.jsx @@ -0,0 +1,28 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + + const handleGenerate = async () => { + const orderIds = data.selected.map((item) => item.id); + + const response = await fetch('/api/generate-packing-slip', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({orderIds}), + }); + + const {printUrl} = await response.json(); + return printUrl; + }; + + return ( + + Generating packing slip for {data.selected.length} orders + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/shipping-manifest.jsx b/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/shipping-manifest.jsx new file mode 100644 index 0000000000..c1d1f3d7c6 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/print-action/examples/shipping-manifest.jsx @@ -0,0 +1,58 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [orders, setOrders] = useState([]); + + useEffect(() => { + const fetchOrders = async () => { + const orderIds = data.selected.map((item) => item.id); + + const {data: ordersData} = await shopify.query( + `query GetOrders($ids: [ID!]!) { + nodes(ids: $ids) { + ... on Order { + id + name + shippingAddress { + address1 + city + country + } + } + } + }`, + {variables: {ids: orderIds}}, + ); + + setOrders(ordersData.nodes); + }; + + fetchOrders(); + }, [data]); + + const handleGenerate = async () => { + const response = await fetch('/api/generate-shipping-manifest', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({orders}), + }); + + const {printUrl} = await response.json(); + return printUrl; + }; + + return ( + + Shipping manifest for {orders.length} orders + {orders.map((order) => ( + {order.name} + ))} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.doc.ts index 4d3a4cd84b..52cd867b1d 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.doc.ts @@ -3,19 +3,89 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Print Action Extension API', description: - 'This API is available to all print action extension types. Note that the [`AdminPrintAction`](/docs/api/admin-extensions/polaris-web-components/other/adminprintaction) component is required to build admin print action extensions.', + 'The Print Action Extension API lets you [build print action extensions](/docs/apps/build/admin/actions-blocks/build-admin-print-action) that generate custom printable documents for orders, products, and other resources. Use this API to create branded labels, packing slips, custom invoices, or specialty documents.', isVisualComponent: false, type: 'API', + requires: + 'the [`AdminPrintAction`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/settings-and-templates/adminprintaction) component.', + defaultExample: { + description: + "Generate a packing slip PDF for selected orders by calling your app's backend service. This example shows extracting order IDs from the selected resources, making an API call to your backend to generate the PDF, and returning the printable URL to display the document.", + codeblock: { + title: 'Generate packing slip', + tabs: [ + { + title: 'jsx', + code: './examples/generate-packing-slip.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'PrintActionExtensionApi', - description: '', + title: 'Properties', + description: + 'The `PrintActionExtensionApi` object provides properties for print action extensions that generate custom printable documents. Access the following properties on the `PrintActionExtensionApi` object to access selected resources and display picker dialogs for print configuration.', type: 'PrintActionExtensionApi', }, ], + examples: { + description: 'Generate custom printable documents', + examples: [ + { + description: + 'Generate product labels with an option to add additional products beyond the initial selection. This example demonstrates using the [resource picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) within a print action and tracking the additional product count.', + codeblock: { + title: 'Generate custom product labels', + tabs: [ + { + title: 'jsx', + code: './examples/custom-product-labels.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Query order details using the [GraphQL Admin API](/docs/api/admin-graphql/) and then generate a shipping manifest PDF. This example shows fetching order data in `useEffect`, displaying the order list, and passing the data to your print service.', + codeblock: { + title: 'Generate shipping manifest', + tabs: [ + { + title: 'jsx', + code: './examples/shipping-manifest.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Core APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Use `@media print` CSS for print-optimized styling:** Apply print-specific styles with `@media print` queries to control page breaks, hide UI elements, and optimize for paper output. The print preview shows the screen styles until printing.\n' + + '- **Set document MIME type correctly:** Return `Content-Type: application/pdf` for PDFs, `image/png` for images, or `text/html` for HTML documents. Incorrect MIME types may cause browser download instead of preview.\n' + + '- **Test `window.print()` behavior:** If generating HTML, test that `window.print()` works correctly. Some CSS frameworks or scripts may interfere with browser print dialogs.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Print action extensions must return a URL string. You can't render the print UI directly within the extension or control the print preview appearance.\n" + + '- URLs must be publicly accessible with CORS headers allowing the Shopify admin origin. Authentication tokens in URLs can expire while merchants have the preview open.\n' + + "- Extensions don't have access to printer settings. You can't configure print options like page orientation, margins, or paper size. Merchants control these through browser print dialogs.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.ts b/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.ts index c3f393afc8..1e8e8358c4 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/print-action/print-action.ts @@ -1,24 +1,15 @@ -import type {StandardApi} from '../standard/standard'; +import type {StandardRenderingExtensionApi} from '../standard/standard-rendering'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import type {Data} from '../shared'; -import type {ResourcePickerApi} from '../resource-picker/resource-picker'; -import type {PickerApi} from '../picker/picker'; +/** + * The `PrintActionExtensionApi` object provides methods for print action extensions that generate custom printable documents. Access the following properties on the `PrintActionExtensionApi` object to access selected resources and display picker dialogs for print configuration. + */ export interface PrintActionExtensionApi< ExtensionTarget extends AnyExtensionTarget, -> extends StandardApi { +> extends StandardRenderingExtensionApi { /** - * Information about the currently viewed or selected items. + * An array of currently viewed or selected resource identifiers. Use this to access the IDs of items to include in the print document, such as selected orders or products. */ data: Data; - - /** - * Renders the [Resource Picker](resource-picker), allowing users to select a resource for the extension to use as part of its flow. - */ - resourcePicker: ResourcePickerApi; - - /** - * Renders a custom [Picker](picker) dialog allowing users to select values from a list. - */ - picker: PickerApi; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/create-variant-component.jsx b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/create-variant-component.jsx new file mode 100644 index 0000000000..27b3880ebd --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/create-variant-component.jsx @@ -0,0 +1,56 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [created, setCreated] = useState(null); + const [productId, setProductId] = useState(null); + + useEffect(() => { + const getParentProduct = async () => { + const variantId = data.selected[0]?.id; + + const {data: parentData} = await shopify.query( + `query GetParentProduct($id: ID!) { + productVariant(id: $id) { + product { + id + } + } + }`, + {variables: {id: variantId}}, + ); + + setProductId(parentData.productVariant.product.id); + }; + + getParentProduct(); + }, [data]); + + const handleCreate = async () => { + const activity = await shopify.intents.invoke('create:shopify/ProductVariant', { + data: {productId}, + }); + + const response = await activity.complete; + + if (response.code === 'ok') { + setCreated(true); + } + }; + + return ( + + {productId && ( + <> + Create Component Variant + {created && Component variant created} + + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/load-bundle-config.jsx b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/load-bundle-config.jsx new file mode 100644 index 0000000000..8826ce8201 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/load-bundle-config.jsx @@ -0,0 +1,47 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [components, setComponents] = useState([]); + + useEffect(() => { + const loadConfig = async () => { + const productId = data.selected[0]?.id; + + const {data: bundleData} = await shopify.query( + `query GetBundleComponents($id: ID!) { + product(id: $id) { + id + title + metafield(namespace: "bundle", key: "components") { + value + } + } + }`, + {variables: {id: productId}}, + ); + + const comps = bundleData.product.metafield + ? JSON.parse(bundleData.product.metafield.value) + : []; + + setComponents(comps); + }; + + loadConfig(); + }, [data]); + + return ( + + {components.length} components configured + {components.map((comp, i) => ( + Component {i + 1} + ))} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/load-variant-bundle-config.jsx b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/load-variant-bundle-config.jsx new file mode 100644 index 0000000000..7cfb1de001 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/load-variant-bundle-config.jsx @@ -0,0 +1,54 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [components, setComponents] = useState([]); + const [variantInfo, setVariantInfo] = useState(null); + + useEffect(() => { + const loadConfig = async () => { + const variantId = data.selected[0]?.id; + + const {data: variantData} = await shopify.query( + `query GetVariantBundleComponents($id: ID!) { + productVariant(id: $id) { + id + displayName + sku + metafield(namespace: "bundle", key: "variant_components") { + value + } + } + }`, + {variables: {id: variantId}}, + ); + + setVariantInfo(variantData.productVariant); + + const comps = variantData.productVariant.metafield + ? JSON.parse(variantData.productVariant.metafield.value) + : []; + + setComponents(comps); + }; + + loadConfig(); + }, [data]); + + return ( + + {variantInfo && ( + <> + {variantInfo.displayName} + SKU: {variantInfo.sku} + {components.length} variant components configured + + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/navigate-to-component.jsx b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/navigate-to-component.jsx new file mode 100644 index 0000000000..27080459f4 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/navigate-to-component.jsx @@ -0,0 +1,27 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [created, setCreated] = useState(null); + + const handleCreate = async () => { + const activity = await shopify.intents.invoke('create:shopify/Product'); + const response = await activity.complete; + + if (response.code === 'ok') { + const newProductId = response.data?.product?.id; + setCreated(newProductId); + } + }; + + return ( + + Create New Component Product + {created && Component created: {created}} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/select-bundle-components.jsx b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/select-bundle-components.jsx new file mode 100644 index 0000000000..da57cdc1be --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/select-bundle-components.jsx @@ -0,0 +1,50 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [selected, setSelected] = useState([]); + + const parentProductId = data.selected[0]?.id; + + const handleSelectComponents = async () => { + const componentProducts = await shopify.resourcePicker({ + type: 'product', + multiple: 5, + action: 'select', + filter: { + draft: false, + archived: false, + }, + }); + + if (componentProducts) { + setSelected(componentProducts); + + await fetch('/api/bundles/configure', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + bundleProductId: parentProductId, + components: componentProducts.map((p) => ({ + productId: p.id, + quantity: 1, + })), + }), + }); + } + }; + + return ( + + Select Components + {selected.length > 0 && ( + {selected.length} components selected + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/select-variant-components.jsx b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/select-variant-components.jsx new file mode 100644 index 0000000000..31f241f1ac --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/examples/select-variant-components.jsx @@ -0,0 +1,50 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [selected, setSelected] = useState([]); + + const parentVariantId = data.selected[0]?.id; + + const handleSelectComponents = async () => { + const componentVariants = await shopify.resourcePicker({ + type: 'variant', + multiple: 5, + action: 'select', + filter: { + draft: false, + archived: false, + }, + }); + + if (componentVariants) { + setSelected(componentVariants); + + await fetch('/api/bundles/configure-variant', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + bundleVariantId: parentVariantId, + componentVariants: componentVariants.map((v) => ({ + variantId: v.id, + quantity: 1, + })), + }), + }); + } + }; + + return ( + + Select Variant Components + {selected.length > 0 && ( + {selected.length} variant components selected + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.doc.ts index 6f4274b4b2..3067e28d6c 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.doc.ts @@ -3,19 +3,89 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Product Details Configuration API', description: - 'This API is available to product configuration extensions that render on the product details page. See the [tutorial](/docs/apps/selling-strategies/bundles/product-config) for more information.', + 'The Product Details Configuration API lets you [build product configuration extensions for bundles](/docs/apps/build/product-merchandising/bundles/product-configuration-extension/add-merchant-config-ui) that define product relationships and manage [bundle](/docs/apps/build/product-merchandising/bundles) compositions. Use this API to build configuration interfaces for bundle and component product experiences.', isVisualComponent: false, type: 'API', + requires: + 'the [`AdminBlock`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/settings-and-templates/adminblock) component.', + defaultExample: { + description: + 'Open the product [resource picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) to select up to five components for a [bundle](/docs/apps/build/product-merchandising/bundles). This example filters out draft and archived products, saves the bundle configuration to your backend, and tracks the selection count.', + codeblock: { + title: 'Select bundle components', + tabs: [ + { + title: 'jsx', + code: './examples/select-bundle-components.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'ProductDetailsConfigurationApi', - description: '', + title: 'Properties', + description: + 'The `ProductDetailsConfigurationApi` object provides access to product configuration data. Access the following properties on the `ProductDetailsConfigurationApi` object to interact with the current product context, navigate within the admin, and select resources in the `admin.product-details.configuration.render` target.', type: 'ProductDetailsConfigurationApi', }, ], + examples: { + description: 'Configure product bundles', + examples: [ + { + description: + "Query a product's bundle metafield and parse the JSON components array. This example fetches bundle data in `useEffect`, parses the stored configuration, and displays the component count.", + codeblock: { + title: 'Load bundle configuration', + tabs: [ + { + title: 'jsx', + code: './examples/load-bundle-config.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Launch the product creation workflow using [intents](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/intents-api) and capture the new product ID. This example invokes the `create` intent, waits for completion, and displays the created product ID from the response data.', + codeblock: { + title: 'Create bundle component', + tabs: [ + { + title: 'jsx', + code: './examples/navigate-to-component.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Design for products with multiple variants:** Products in `api.data.selected` may have multiple variants. Design your bundle configuration to either apply to all variants or allow variant-level configuration.\n' + + '- **Use the Resource Picker to select components:** Use the [Resource Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) to let merchants select component products for bundle configurations.\n' + + '- **Implement cart transforms to enforce bundles:** Configuration only defines relationships in admin. Use Shopify Functions [cart transforms](/docs/api/functions/latest/cart-transform) to actually bundle products at checkout based on your saved configuration.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Configuration extensions only render in the admin. They don't affect storefront display or checkout behavior. You must implement storefront and checkout logic separately.\n" + + "- Bundles aren't enforced automatically. Saving configuration doesn't automatically create bundles. You must implement [cart transforms](/docs/api/functions/latest/cart-transform) or other mechanisms to enforce bundling at purchase time.\n" + + "- Your extension can't directly modify product properties. The API is read-only for product data. Use GraphQL mutations like [`productUpdate`](/docs/api/admin-graphql/latest/mutations/productUpdate) to update products if needed.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.ts b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.ts index b4f6eb6a54..c2bf5f47dc 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-details-configuration.ts @@ -2,51 +2,81 @@ import type {BlockExtensionApi} from '../block/block'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import type {Data} from '../shared'; -/* @deprecated */ +/** + * A product configuration. + * @deprecated + */ interface Product { + /** The product's unique global identifier (GID). */ id: string; + /** The product's display name shown to merchants and customers. */ title: string; + /** The URL-friendly unique identifier used in product URLs (for example, `'blue-t-shirt'`). */ handle: string; + /** The publication status indicating whether the product is active (published), archived (discontinued), or draft (unpublished). */ status: 'ACTIVE' | 'ARCHIVED' | 'DRAFT'; + /** The total number of variants this product has. */ totalVariants: number; + /** The total available inventory summed across all variants and locations. */ totalInventory: number; + /** Whether the product has only the default variant with no custom options. When `true`, the product has no size, color, or other option variations. */ hasOnlyDefaultVariant: boolean; + /** The URL to view this product on the online store. Use this to create "View in store" links. */ onlineStoreUrl?: string; + /** Product options that define how variants differ (for example, Size, Color, Material). Each option has an ID, name, position, and array of possible values. */ options: { id: string; name: string; position: number; values: string[]; }[]; + /** The product category or type used for organization (for example, "T-Shirt", "Shoes"). */ productType: string; + /** The standardized product category taxonomy. Use this for product classification in search and organization. */ productCategory?: string; + /** An array of component products that make up this bundle. Each component represents a product included in the bundle configuration. */ productComponents: ProductComponent[]; } -/* @deprecated */ +/** + * A component product that is part of a bundle. Represents an individual product included in a bundle configuration. + * @deprecated + */ export interface ProductComponent { + /** The component product's unique global identifier (GID). */ id: string; + /** The product's display name. Use this to show which product is included in the bundle. */ title: string; + /** The featured image displayed for this component product with ID, URL, and alt text properties. Use this for showing component previews in bundle configuration interfaces. */ featuredImage?: { id?: string | null; url?: string | null; altText?: string | null; } | null; + /** The total number of variants this component product has. Use this to determine if variant selection is needed for this component. */ totalVariants: number; + /** The admin URL for this component product. Use this to create links to the product's details page in the admin. */ productUrl: string; + /** The count of variants from this product that are used as bundle components. Use this to understand how many variants are configured in bundles. */ componentVariantsCount: number; + /** The count of variants from this product that aren't used in any bundles. Use this to identify available variants for adding to bundle configurations. */ nonComponentVariantsCount: number; } +/** + * The `ProductDetailsConfigurationApi` object provides methods for configuring product bundles and relationships. Access the following properties on the `ProductDetailsConfigurationApi` object to build product configuration interfaces. + */ export interface ProductDetailsConfigurationApi< ExtensionTarget extends AnyExtensionTarget, > extends BlockExtensionApi { + /** Product configuration data including the current product, selected items, and app URLs. Use this to access the product being configured and build your configuration interface. */ data: Data & { - /* - @deprecated - The product currently being viewed in the admin. - */ + /** + * The product currently being viewed in the admin. + * @deprecated + */ product: Product; + /** URLs for launching and navigating to your app, including the launch URL and base application URL. Use these to create links or redirect merchants to your app. */ app: { launchUrl: string; applicationUrl: string; diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.doc.ts index 679bcd79ca..647e3000c3 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.doc.ts @@ -3,19 +3,89 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Product Variant Details Configuration API', description: - 'This API is available to product configuration extensions that render on the product variant details page. See the [tutorial](/docs/apps/selling-strategies/bundles/product-config) for more information.', + 'The Product Variant Details Configuration API lets you [build product configuration extensions for bundles](/docs/apps/build/product-merchandising/bundles/product-configuration-extension/add-merchant-config-ui) that define variant relationships and manage [bundle](/docs/apps/build/product-merchandising/bundles) compositions. Use this API to build configuration interfaces for bundle and component product experiences at the variant level.', isVisualComponent: false, type: 'API', + requires: + 'the [`AdminBlock`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/settings-and-templates/adminblock) component.', + defaultExample: { + description: + 'Use the product variant [resource picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) to select component variants for a [bundle](/docs/apps/build/product-merchandising/bundles). This example picks product variants, tracks selections, and posts the product variant IDs to configure the bundle.', + codeblock: { + title: 'Select product variant components', + tabs: [ + { + title: 'jsx', + code: './examples/select-variant-components.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'ProductVariantDetailsConfigurationApi', - description: '', + title: 'Properties', + description: + 'The `ProductVariantDetailsConfigurationApi` object provides access to product variant configuration data. Access the following properties on the `ProductVariantDetailsConfigurationApi` object to interact with the current product variant context, navigate within the admin, and select resources in the `admin.product-variant-details.configuration.render` target.', type: 'ProductVariantDetailsConfigurationApi', }, ], + examples: { + description: 'Configure product variant-level bundles', + examples: [ + { + description: + 'Query the parent product ID then launch the variant creation workflow. This example fetches the parent product using GraphQL, passes it as context data to the variant intent, and shows success feedback.', + codeblock: { + title: 'Create component variant', + tabs: [ + { + title: 'jsx', + code: './examples/create-variant-component.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Fetch variant bundle data including SKU and display name from metafields. This example queries variant-specific details, parses the component configuration, and displays variant information in the UI.', + codeblock: { + title: 'Load variant bundle configuration', + tabs: [ + { + title: 'jsx', + code: './examples/load-variant-bundle-config.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Store configuration keyed by variant GID:** Save bundle relationships in metafields on the variant or in your app database using the variant GID as the key for precise variant-level configuration.\n' + + '- **Use `type: "variant"` in Resource Picker for precision:** When selecting component variants, use `type: "variant"` in the [Resource Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) for precise variant selection rather than product-level selection.\n' + + '- **Implement cart transforms to enforce bundles:** Configuration only defines relationships. Use Shopify Functions [cart transforms](/docs/api/functions/latest/cart-transform) to enforce variant-level bundling at checkout based on saved configuration.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Configuration extensions only render in the admin. They don't affect storefront or checkout behavior. You must implement separate logic for storefront bundle display and checkout enforcement.\n" + + "- Bundles aren't enforced automatically. Configuration doesn't automatically create bundles. You must implement [cart transforms](/docs/api/functions/latest/cart-transform) to enforce bundling when variants are added to cart.\n" + + "- Your extension can't directly modify variant properties. The API is read-only for variant data. Use GraphQL mutations like [`productVariantsBulkUpdate`](/docs/api/admin-graphql/latest/mutations/productVariantsBulkUpdate) if you need to update variants.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.ts b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.ts index ecec2bba32..25ef0d7ba9 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/product-configuration/product-variant-details-configuration.ts @@ -2,52 +2,82 @@ import type {BlockExtensionApi} from '../block/block'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import type {Data} from '../shared'; -/* @deprecated */ +/** + * A product variant configuration. + * @deprecated + */ interface ProductVariant { + /** The variant's unique global identifier (GID). */ id: string; + /** The Stock Keeping Unit (SKU) identifier for inventory tracking. */ sku: string; + /** The barcode, UPC, or ISBN number for the variant. */ barcode: string; + /** The display name showing only the variant's option values (for example, "Medium / Blue"). */ title: string; + /** A human-readable display name that combines the product title with the variant's option values (for example, "T-Shirt - Medium / Blue"). */ displayName: string; + /** The current selling price for this variant. */ price: string; + /** The original price before any discounts or markdowns. */ compareAtPrice: string; + /** Whether this variant is subject to taxes. When `true`, applicable taxes are calculated at checkout. */ taxable: boolean; + /** The harmonized system (HS) tax code for international shipping and customs. */ taxCode: string; + /** The physical weight of the variant as a number. */ weight: number; + /** The option values that define this specific variant with name and value pairs (for example, Size: Large, Color: Blue). */ selectedOptions: { name: string; value: string; }[]; + /** An array of component variants that make up this bundle variant. Each component represents a product variant included in the bundle. */ productVariantComponents: ProductVariantComponent[]; } -/* @deprecated */ +/** + * A component variant that is part of a product bundle. Represents an individual product variant included in a bundle configuration. + * @deprecated + */ export interface ProductVariantComponent { + /** The component variant's unique global identifier (GID). */ id: string; + /** A human-readable display name that combines the product title with the variant's option values (for example, "T-Shirt - Medium / Blue"). */ displayName: string; + /** The display name showing only the variant's option values (for example, "Medium / Blue"). */ title: string; + /** The Stock Keeping Unit (SKU) identifier for this component variant. */ sku?: string; + /** The image displayed for this component variant with ID, URL, and alt text properties. Use this for showing component previews in bundle configuration interfaces. */ image?: { id?: string | null; url?: string | null; altText?: string | null; } | null; + /** The admin URL for this product variant. Use this to create links to the variant's details page in the admin. */ productVariantUrl: string; + /** The option values that define this specific component variant with name and value pairs (for example, Size: Large, Color: Blue). */ selectedOptions: { name: string; value: string; }[]; } +/** + * The `ProductVariantDetailsConfigurationApi` object provides methods for configuring product variant bundles and relationships. Access the following properties on the `ProductVariantDetailsConfigurationApi` object to build variant configuration interfaces. + */ export interface ProductVariantDetailsConfigurationApi< ExtensionTarget extends AnyExtensionTarget, > extends BlockExtensionApi { + /** Product variant configuration data including the current variant, selected items, and app URLs. Use this to access the variant being configured and build your configuration interface. */ data: Data & { - /* - @deprecated - The product variant currently being viewed in the admin. - */ + /** + * The product variant currently being viewed in the admin. + * @deprecated + */ variant: ProductVariant; + /** URLs for launching and navigating to your app, including the launch URL and base application URL. Use these to create links or redirect merchants to your app. */ app: { launchUrl: string; applicationUrl: string; diff --git a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.doc.ts index b9780d7343..fa4e7dff1d 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.doc.ts @@ -3,19 +3,88 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Purchase Options Card Configuration API', description: - 'This API is available to action extensions that render in the purchase options card on product and product variant details pages when selling plan groups are present.', + 'The Purchase Options Card Configuration API provides access to purchase option selection data for products with [subscription](/docs/apps/build/purchase-options/subscriptions) and [selling plan](/docs/apps/build/purchase-options/subscriptions/selling-plans) configurations. Use this API to build action extensions that interact with selected [purchase options](/docs/apps/build/purchase-options) on product and product variant details pages.', isVisualComponent: false, type: 'API', + requires: + 'the [`AdminAction`](/docs/api/admin-extensions/{API_VERSION}/polaris-web-components/settings-and-templates/adminaction) component.', + defaultExample: { + description: + 'Update a subscription by sending product and selling plan IDs to your backend. This example checks for selling plan presence, posts the update request, and shows a success banner before auto-closing the modal.', + codeblock: { + title: 'Manage a subscription', + tabs: [ + { + title: 'jsx', + code: './purchase-options-card/examples/manage-subscription.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'PurchaseOptionsCardConfigurationApi', - description: '', + title: 'Properties', + description: + 'The `PurchaseOptionsCardConfigurationApi` object provides access to selected purchase option data. Access the following properties on the `PurchaseOptionsCardConfigurationApi` object to interact with currently selected products and selling plans in the `admin.product-purchase-option.action.render` and `admin.product-variant-purchase-option.action.render` targets.', type: 'PurchaseOptionsCardConfigurationApi', }, ], + examples: { + description: 'Work with purchase options and selling plans', + examples: [ + { + description: + 'Show a confirmation dialog before removing a product from a selling plan. This example demonstrates two-step confirmation with cancel option and success feedback after removal.', + codeblock: { + title: 'Remove from selling plan', + tabs: [ + { + title: 'jsx', + code: './purchase-options-card/examples/remove-from-plan.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Fetch selling plan name and options using the [GraphQL Admin API](/docs/api/admin-graphql) to validate the configuration. This example queries plan details, stores them in state, displays the information, and auto-closes after two seconds.', + codeblock: { + title: 'Validate selling plan', + tabs: [ + { + title: 'jsx', + code: './purchase-options-card/examples/validate-selling-plan.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Contextual APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Handle operations based on selling plan selection:** Items in `api.data.selected` have an optional `sellingPlanId` property. When present, perform subscription-specific operations. When absent, treat it as a one-time purchase.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + '- The action only appears when selling plan groups exist on the product or variant. The action is hidden for products without subscription options, even if your extension is installed.\n' + + '- Items in `api.data.selected` have an optional `sellingPlanId` property. When present, the merchant selected a specific selling plan. When absent, they selected the product/variant without a specific plan.\n' + + "- Your extension can't modify selling plan configurations. The API is read-only for selling plan data. Use GraphQL mutations to update selling plans if needed.\n" + + '- Selection data only includes IDs. You must query GraphQL for full product, variant, and selling plan details like billing policy and pricing adjustments. Selling plan group data is also unavailable. Your extension only receives individual selling plan IDs but not the parent selling plan group structure.', + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.ts b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.ts index 9c68ed39d3..77a9a8955b 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card-action.ts @@ -1,10 +1,20 @@ import {ActionExtensionApi} from './action/action'; import type {ExtensionTarget as AnyExtensionTarget} from '../extension-targets'; +/** + * The `PurchaseOptionsCardConfigurationApi` object provides methods for action extensions that interact with purchase options and selling plans. Access the following properties on the `PurchaseOptionsCardConfigurationApi` object to work with selected products and their associated subscription configurations. + */ export interface PurchaseOptionsCardConfigurationApi< ExtensionTarget extends AnyExtensionTarget, > extends ActionExtensionApi { + /** Selected purchase option data including product and selling plan identifiers. */ data: { - selected: {id: string; sellingPlanId?: string}[]; + /** Array of selected items with their product IDs and optional selling plan IDs for subscription configurations. */ + selected: { + /** The product or variant identifier. */ + id: string; + /** The associated selling plan identifier, if a subscription option is selected. */ + sellingPlanId?: string; + }[]; }; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/manage-subscription.jsx b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/manage-subscription.jsx new file mode 100644 index 0000000000..77e1d271e7 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/manage-subscription.jsx @@ -0,0 +1,45 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [updated, setUpdated] = useState(false); + + const {id: productId, sellingPlanId} = data.selected[0]; + + const handleUpdate = async () => { + if (!sellingPlanId) { + console.error('No selling plan selected'); + shopify.close(); + return; + } + + const response = await fetch('/api/subscriptions/update', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + productId, + sellingPlanId, + action: 'modify', + }), + }); + + if (response.ok) { + setUpdated(true); + setTimeout(() => shopify.close(), 1500); + } + }; + + return ( + + Product: {productId} + Selling Plan: {sellingPlanId} + Update Subscription + {updated && Subscription updated!} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/remove-from-plan.jsx b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/remove-from-plan.jsx new file mode 100644 index 0000000000..5937c8fc28 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/remove-from-plan.jsx @@ -0,0 +1,44 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [confirming, setConfirming] = useState(false); + const [removed, setRemoved] = useState(false); + + const {id: productId, sellingPlanId} = data.selected[0]; + + const handleRemove = async () => { + setConfirming(false); + + await fetch('/api/selling-plans/remove-product', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({productId, sellingPlanId}), + }); + + setRemoved(true); + setTimeout(() => shopify.close(), 1500); + }; + + return ( + + {!confirming ? ( + setConfirming(true)}> + Remove Product + + ) : ( + <> + Are you sure you want to remove this product? + Confirm Remove + setConfirming(false)}>Cancel + + )} + {removed && Product removed from plan} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/validate-selling-plan.jsx b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/validate-selling-plan.jsx new file mode 100644 index 0000000000..62d91ea1eb --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/purchase-options-card/examples/validate-selling-plan.jsx @@ -0,0 +1,47 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {data} = shopify; + const [plan, setPlan] = useState(null); + + const {sellingPlanId} = data.selected[0]; + + const handleValidate = async () => { + const {data: planData} = await shopify.query( + `query GetSellingPlan($id: ID!) { + sellingPlanGroup(id: $id) { + sellingPlans(first: 1) { + edges { + node { + id + name + options + } + } + } + } + }`, + {variables: {id: sellingPlanId}}, + ); + + setPlan(planData.sellingPlanGroup.sellingPlans.edges[0]?.node); + setTimeout(() => shopify.close(), 2000); + }; + + return ( + + Check Plan Details + {plan && ( + <> + Plan: {plan.name} + Options: {plan.options.length} + + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/action.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/action.js deleted file mode 100644 index 46ea7da059..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/action.js +++ /dev/null @@ -1,4 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - action: 'select', -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/action.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/action.jsx new file mode 100644 index 0000000000..4632c91c4c --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/action.jsx @@ -0,0 +1,25 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({ + type: 'product', + action: 'add', + }); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/collection-picker.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/collection-picker.js deleted file mode 100644 index d348c124f0..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/collection-picker.js +++ /dev/null @@ -1 +0,0 @@ -const selected = await resourcePicker({type: 'collection'}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/collection-picker.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/collection-picker.jsx new file mode 100644 index 0000000000..e7597582b2 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/collection-picker.jsx @@ -0,0 +1,22 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({type: 'collection'}); + setSelected(result); + }; + + return ( + + Select Collections + {selected && {selected.length} collections selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filter-query.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filter-query.js deleted file mode 100644 index 828a319d8e..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filter-query.js +++ /dev/null @@ -1,6 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - filter: { - query: 'Sweater', - }, -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filter-query.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filter-query.jsx new file mode 100644 index 0000000000..f676066a62 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filter-query.jsx @@ -0,0 +1,22 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({type: 'product'}); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filters.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filters.js deleted file mode 100644 index ae65369776..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filters.js +++ /dev/null @@ -1,9 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - filter: { - hidden: true, - variants: false, - draft: false, - archived: false, - }, -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filters.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filters.jsx new file mode 100644 index 0000000000..390562af10 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/filters.jsx @@ -0,0 +1,27 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({ + type: 'product', + filter: { + published_status: 'published', + }, + }); + setSelected(result); + }; + + return ( + + Select Published Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-limited.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-limited.js deleted file mode 100644 index 1378d3895f..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-limited.js +++ /dev/null @@ -1,4 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - multiple: 5, -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-limited.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-limited.jsx new file mode 100644 index 0000000000..b68ed60f06 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-limited.jsx @@ -0,0 +1,25 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({ + type: 'product', + multiple: 5, + }); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-unlimited.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-unlimited.js deleted file mode 100644 index 7251cfabe8..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-unlimited.js +++ /dev/null @@ -1,4 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - multiple: true, -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-unlimited.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-unlimited.jsx new file mode 100644 index 0000000000..fe44f19f67 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/multiple-unlimited.jsx @@ -0,0 +1,25 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({ + type: 'product', + multiple: true, + }); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-picker.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-picker.js deleted file mode 100644 index 259647375f..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-picker.js +++ /dev/null @@ -1,3 +0,0 @@ -const {resourcePicker} = useApi(TARGET); - -const selected = await resourcePicker({type: 'product'}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-picker.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-picker.jsx new file mode 100644 index 0000000000..f676066a62 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-picker.jsx @@ -0,0 +1,22 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({type: 'product'}); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-variant-picker.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-variant-picker.js deleted file mode 100644 index d2b1c39d1c..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-variant-picker.js +++ /dev/null @@ -1 +0,0 @@ -const selected = await resourcePicker({type: 'variant'}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-variant-picker.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-variant-picker.jsx new file mode 100644 index 0000000000..3a0f401cd6 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/product-variant-picker.jsx @@ -0,0 +1,22 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({type: 'variant'}); + setSelected(result); + }; + + return ( + + Select Variants + {selected && {selected.length} variants selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/query.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/query.js deleted file mode 100644 index 1185c26f24..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/query.js +++ /dev/null @@ -1,4 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - query: 'Sweater', -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/query.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/query.jsx new file mode 100644 index 0000000000..344d1f0a7d --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/query.jsx @@ -0,0 +1,25 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({ + type: 'product', + query: 'shirt', + }); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection-ids.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection-ids.js deleted file mode 100644 index f9ef7cbea2..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection-ids.js +++ /dev/null @@ -1,16 +0,0 @@ -const selected = await resourcePicker({ - type: 'product', - selectionIds: [ - { - id: 'gid://shopify/Product/12345', - variants: [ - { - id: 'gid://shopify/ProductVariant/1', - }, - ], - }, - { - id: 'gid://shopify/Product/67890', - }, - ], -}); diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection-ids.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection-ids.jsx new file mode 100644 index 0000000000..48530b67b5 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection-ids.jsx @@ -0,0 +1,28 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({ + type: 'product', + selectionIds: [ + 'gid://shopify/Product/123', + 'gid://shopify/Product/456', + ], + }); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection.js b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection.js deleted file mode 100644 index d5fae19182..0000000000 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection.js +++ /dev/null @@ -1,7 +0,0 @@ -const selected = await resourcePicker({type: 'product'}); - -if (selected) { - console.log(selected); -} else { - console.log('Picker was cancelled by the user'); -} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection.jsx b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection.jsx new file mode 100644 index 0000000000..f676066a62 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/examples/selection.jsx @@ -0,0 +1,22 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [selected, setSelected] = useState(null); + + const handlePick = async () => { + const result = await shopify.resourcePicker({type: 'product'}); + setSelected(result); + }; + + return ( + + Select Products + {selected && {selected.length} products selected} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.doc.ts index 959f75638c..32b97fe693 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.doc.ts @@ -3,136 +3,144 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Resource Picker API', overviewPreviewDescription: 'Opens a Resource Picker in your app', - description: `The Resource Picker API provides a search-based interface to help users find and select one or more products, collections, or product variants, and then returns the selected resources to your app. Both the app and the user must have the necessary permissions to access the resources selected. + description: `The Resource Picker API lets merchants search for and select products, collections, or product variants. Use this API when your extension needs merchants to choose Shopify resources to work with. The resource picker returns detailed resource information including IDs, titles, images, and metadata. > Tip: -> If you are picking app resources such as product reviews, email templates, or subscription options, you should use the [Picker](picker) API instead. +> If you need to pick app-specific resources like product reviews, email templates, or subscription options, use the [Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/picker-api) instead. `, isVisualComponent: true, category: 'Target APIs', subCategory: 'Utility APIs', thumbnail: 'resource-picker.png', requires: - 'an Admin [block](/docs/api/admin-extensions/unstable/extension-targets#block-locations), [action](/docs/api/admin-extensions/unstable/extension-targets#action-locations), or [print](/docs/api/admin-extensions/unstable/extension-targets#print-locations) extension.', + 'an Admin UI [block, action, or print](/docs/api/admin-extensions/{API_VERSION}#building-your-extension) extension.', defaultExample: { + description: + 'Open the product resource picker to select items from the store catalog. This example invokes `shopify.resourcePicker` with `type: "product"`, handles the async selection, and displays the count of selected products. When merchants confirm their selection, the resource picker returns an array of product objects with GIDs, titles, and handles for use in your extension.', image: 'resource-picker.png', codeblock: { - title: 'Product picker', + title: 'Select products', tabs: [ { - code: './examples/product-picker.js', - language: 'js', + title: 'jsx', + code: './examples/product-picker.jsx', + language: 'jsx', }, ], }, }, examples: { - description: 'Resource Pickers with different options', + description: + 'Examples that demonstrate how to use the Resource Picker API.', examples: [ { - description: 'Alternate resources', + description: + 'Filter the picker to show only published products using the `filter` option with `published_status: "published"`. This example shows restricting the picker to live, customer-visible products. Use this for promotional campaigns, product recommendations, or any feature that should only work with active inventory, preventing accidental selection of draft or archived products.', codeblock: { - title: 'Alternate resources', + title: 'Filter to published products', tabs: [ { - title: 'Collection picker', - code: './examples/collection-picker.js', - language: 'js', - }, - { - title: 'Product variant picker', - code: './examples/product-variant-picker.js', - language: 'js', + title: 'jsx', + code: './examples/filters.jsx', + language: 'jsx', }, ], }, }, { - description: 'Preselected resources', + description: + 'Limit selection to a maximum of five products by setting `multiple: 5`. This example shows restricting how many products merchants can choose. This is useful for bundle builders with item limits, featured product sections with fixed display slots, or promotional campaigns with maximum product counts. The resource picker automatically prevents selection beyond the limit.', codeblock: { - title: 'Product picker with preselected resources', + title: 'Limit selection count', tabs: [ { - code: './examples/selection-ids.js', - language: 'js', + title: 'jsx', + code: './examples/multiple-limited.jsx', + language: 'jsx', }, ], }, }, { - description: 'Action verb', + description: + 'Open the resource picker with products already selected by passing GIDs to the `selectionIds` option. This example shows pre-populating the resource picker with current selections for edit workflows. Use this for showing what products are already in a bundle, collection, or promotional set. Merchants can see current selections and modify them by adding or removing products before confirming.', codeblock: { - title: 'Product picker with action verb', + title: 'Preselect products', tabs: [ { - code: './examples/action.js', - language: 'js', + title: 'jsx', + code: './examples/selection-ids.jsx', + language: 'jsx', }, ], }, }, { - description: 'Multiple selection', + description: + 'Select collections instead of individual products by setting `type: "collection"`. This example shows switching the resource picker to collection mode for choosing product groupings. This is useful for homepage featured collection carousels, navigation menu builders, bulk collection operations, or promotional campaigns targeting entire product categories rather than individual items.', codeblock: { - title: 'Product picker with multiple selection', + title: 'Select collections', tabs: [ { - title: 'Unlimited selectable items', - code: './examples/multiple-unlimited.js', - language: 'js', - }, - { - title: 'Maximum selectable items', - code: './examples/multiple-limited.js', - language: 'js', + title: 'jsx', + code: './examples/collection-picker.jsx', + language: 'jsx', }, ], }, }, { - description: 'Filters', + description: + 'Allow unlimited product selection by setting `multiple: true` without a numeric limit. This example shows enabling flexible multi-selection where merchants control the quantity. Use this for mass product taggers, bulk inventory tools, category managers, or export utilities where selection count depends on merchant needs without artificial constraints.', codeblock: { - title: 'Product picker with filters', + title: 'Select unlimited products', tabs: [ { - code: './examples/filters.js', - language: 'js', + title: 'jsx', + code: './examples/multiple-unlimited.jsx', + language: 'jsx', }, ], }, }, { - description: 'Filter query', + description: + 'Select specific product variants instead of entire products by setting `type: "variant"`. This example shows switching to variant-level selection for choosing individual SKUs. Use this for inventory transfer tools, variant-specific promotions, wholesale pricing sheets, or shipment builders where you need granular control over size, color, and individual SKU tracking.', codeblock: { - title: 'Product picker with a custom filter query', + title: 'Select product variants', tabs: [ { - code: './examples/filter-query.js', - language: 'js', + title: 'jsx', + code: './examples/product-variant-picker.jsx', + language: 'jsx', }, ], }, }, { - description: 'Selection', + description: + 'Customize the resource picker button text by setting the `action` option to "add" or "select". This example shows changing the action verb to provide workflow context. "Add" suggests appending to an existing list, while "select" implies choosing for a specific purpose or replacing selections. This subtle language difference improves clarity for different workflow contexts.', codeblock: { - title: 'Product picker using returned selection payload', + title: 'Set action verb', tabs: [ { - code: './examples/selection.js', - language: 'js', + title: 'jsx', + code: './examples/action.jsx', + language: 'jsx', }, ], }, }, { - description: 'Initial query', + description: + 'Start the resource picker with a pre-filled search query by passing the `query` option. This example shows initializing the picker with a search term already entered. This is helpful when you know what merchants are likely looking for, such as products from a specific vendor, tag, or product type. Merchants can modify the query, but starting with relevant results saves time in large catalogs.', codeblock: { - title: 'Product picker with initial query provided', + title: 'Start with search query', tabs: [ { - code: './examples/query.js', - language: 'js', + title: 'jsx', + code: './examples/query.jsx', + language: 'jsx', }, ], }, @@ -141,22 +149,35 @@ const data: ReferenceEntityTemplateSchema = { }, definitions: [ { - title: 'Resource Picker Options', - description: `The \`Resource Picker\` accepts a variety of options to customize the picker's behavior.`, + title: 'Properties', + description: `The \`ResourcePickerOptions\` object defines how the resource picker behaves, including which resource type to display, selection limits, filters, and preselected items. Access the following properties on the \`ResourcePickerOptions\` object to configure the resource picker's appearance and functionality.`, type: 'ResourcePickerOptions', }, { - title: 'Resource Picker Return Payload', - description: `The \`Resource Picker\` returns a Promise with an array of the selected resources. The object type in the array varies based on the provided \`type\` option.\n\nIf the picker is cancelled, the Promise resolves to \`undefined\``, + title: 'ResourcePicker return payload', + description: `The resource picker returns an array of selected resources when the merchant confirms their selection, or \`undefined\` if they cancel. The resource structure in the array varies based on the \`type\` option: products include variants and images, collections include rule sets, and variants include pricing and inventory data.`, type: 'SelectPayload', }, ], - related: [ + related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + "- **Filter query runs server-side:** The `query` property in filters isn't visible to merchants and runs as a GraphQL search query. Use it to programmatically restrict results (for example, `vendor:Acme`) without exposing the filter logic.\n" + + '- **Handle undefined return on cancellation:** When merchants close the picker without selecting, it returns `undefined` rather than an empty array. Check for `undefined` explicitly.', + }, { - name: 'Picker API', - subtitle: 'APIs', - url: 'picker', - type: 'pickaxe-3', + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + "- Only products, variants, and collections are supported. Other resource types like customers, orders, or locations can't be selected. Use the [Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/picker-api) for custom resources.\n" + + "- Product selection with `multiple: false` doesn't prevent multi-variant selection from the same product. Merchants can select multiple variants from a single product even when `multiple: false`.\n" + + "- Filter options are limited to predefined fields (`hidden`, `variants`, `draft`, `archived`, `query`). Custom filter criteria beyond these aren't supported.\n" + + '- Returned data structure varies by resource type. Products include a `variants` array, variants include `price` and `inventoryQuantity`, and collections include `ruleSet`.', }, ], }; diff --git a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.ts b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.ts index 8d5cba3555..17f5f3c4fa 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/resource-picker/resource-picker.ts @@ -1,240 +1,400 @@ +/** + * A monetary value represented as a string (for example, `"19.99"` or `"0.00"`). The format always includes the decimal point and cents, even for whole dollar amounts. Use this type for prices, costs, and other currency values. + */ type Money = string; +/** + * The sort order that determines how products appear in a collection. This controls the default product arrangement that customers see when viewing the collection on the storefront. + */ enum CollectionSortOrder { + /** Products arranged in the custom order set by the merchant. Use this when merchants have manually organized products for specific merchandising. */ Manual = 'MANUAL', + /** Products sorted by sales volume, with best-sellers first. Use this to highlight popular products. */ BestSelling = 'BEST_SELLING', + /** Products sorted alphabetically by title from A to Z. Use this for easy browsing of product names. */ AlphaAsc = 'ALPHA_ASC', + /** Products sorted alphabetically by title from Z to A. Use this for reverse alphabetical ordering. */ AlphaDesc = 'ALPHA_DESC', + /** Products sorted by price from highest to lowest. Use this to show premium or expensive items first. */ PriceDesc = 'PRICE_DESC', + /** Products sorted by price from lowest to highest. Use this to show affordable options first. */ PriceAsc = 'PRICE_ASC', + /** Products sorted by creation date with newest products first. Use this to highlight recently added items. */ CreatedDesc = 'CREATED_DESC', + /** Products sorted by creation date with oldest products first. Use this for chronological ordering. */ Created = 'CREATED', + /** Products sorted by search relevance based on query terms. Use this when the collection is filtered by search. */ MostRelevant = 'MOST_RELEVANT', } +/** + * The types of fulfillment services that can handle order fulfillment. This determines how products are delivered to customers. + */ enum FulfillmentServiceType { + /** Digital gift card fulfillment with automatic delivery. No physical shipping required. */ GiftCard = 'GIFT_CARD', + /** Manual fulfillment handled directly by the merchant. The merchant packs and ships orders themselves. */ Manual = 'MANUAL', + /** Third-party fulfillment service that handles warehousing and shipping (for example, ShipBob, Amazon FBA, or custom 3PL providers). */ ThirdParty = 'THIRD_PARTY', } +/** + * The unit of measurement for product weight. Use this with the weight value to calculate shipping costs or display product specifications. + */ enum WeightUnit { + /** Weight measured in kilograms (kg). Commonly used in metric system countries. */ Kilograms = 'KILOGRAMS', + /** Weight measured in grams (g). Used for lightweight items in metric system. */ Grams = 'GRAMS', + /** Weight measured in pounds (lb). Commonly used in the United States. */ Pounds = 'POUNDS', + /** Weight measured in ounces (oz). Used for lightweight items in imperial system. */ Ounces = 'OUNCES', } +/** + * The inventory policy that determines whether customers can purchase a variant when it's out of stock. Use this to control checkout behavior for low or zero inventory items. + */ enum ProductVariantInventoryPolicy { + /** Prevents purchases when inventory reaches zero. Customers can't add out-of-stock variants to their cart. Use this to avoid overselling. */ Deny = 'DENY', + /** Allows purchases even when inventory is zero or negative. Customers can continue buying out-of-stock variants. Use this for backorders or made-to-order products. */ Continue = 'CONTINUE', } +/** + * The system responsible for tracking inventory levels for a variant. This determines where stock counts are managed and updated. + */ enum ProductVariantInventoryManagement { + /** Inventory tracked and managed by Shopify. Stock levels update through Shopify admin or API. Use this for standard inventory management. */ Shopify = 'SHOPIFY', + /** Inventory not tracked. The variant is always considered in stock. Use this for services, digital goods, or custom products with unlimited availability. */ NotManaged = 'NOT_MANAGED', + /** Inventory tracked by an external fulfillment service. The third-party system manages stock levels. Use this when a 3PL or fulfillment app controls inventory. */ FulfillmentService = 'FULFILLMENT_SERVICE', } +/** + * The publication status indicating a product's availability state. Use this to filter products or determine which products customers can see and purchase. + */ enum ProductStatus { + /** The product is published and available for sale on active sales channels. Customers can view and purchase this product. */ Active = 'ACTIVE', + /** The product is archived and no longer available. Archived products don't appear on storefronts and can't be purchased. Use this for discontinued items. */ Archived = 'ARCHIVED', + /** The product is an unpublished draft not visible to customers. Draft products are still being prepared or reviewed. */ Draft = 'DRAFT', } +/** + * An image associated with a product, variant, or collection. Use image data to display thumbnails, galleries, or product previews in your extension. + */ interface ResourceImage { + /** The unique identifier for the image file. Use this ID for image-related GraphQL operations. */ id: string; + /** Alternative text describing the image for screen readers and accessibility. This text appears when images fail to load. Use descriptive alt text to make your extension accessible. */ altText?: string; + /** The full URL to the original image file. Use this URL to display the image in your extension UI. */ originalSrc: string; } +/** + * The base resource structure with a unique identifier. + */ interface Resource { - /** in GraphQL id format, ie 'gid://shopify/Product/1' */ + /** The resource identifier in GraphQL global ID format (for example, `gid://shopify/Product/123`). */ id: string; } +/** + * A resource structure that can optionally include associated variants. Use this type for specifying preselected items in the resource picker when you need to include variant selections. + */ interface BaseResource extends Resource { + /** An array of variant resources to preselect along with the main resource. Only applicable when the main resource is a product that has variants you want to preselect. */ variants?: Resource[]; } +/** + * A single rule that defines product inclusion criteria for an automated collection. Rules filter products based on their attributes to automatically populate a collection. + */ interface CollectionRule { + /** The product field to evaluate (for example, `'title'`, `'tag'`, `'vendor'`, or `'product_type'`). This determines which product attribute the rule checks. */ column: string; + /** The value to compare against. For example, if checking tags, this might be `'summer'` or `'featured'`. The product attribute must match this condition value according to the relation. */ condition: string; + /** The comparison operator that determines how the field is matched (for example, `'equals'`, `'contains'`, `'starts_with'`, `'ends_with'`, `'not_equals'`). This defines the matching logic between the column and condition. */ relation: string; } +/** + * A set of rules that determine which products are automatically included in a collection. Use this to understand how an automated collection populates itself with products. + */ interface RuleSet { + /** The logical operator for combining multiple rules. When `true`, products are included if they match ANY rule (OR logic). When `false`, products must match ALL rules (AND logic). Use this to understand the collection's filtering strategy. */ appliedDisjunctively: boolean; + /** An array of rules that define product inclusion criteria. Each rule checks a different product attribute. Products are added to the collection based on how these rules are combined (see `appliedDisjunctively`). */ rules: CollectionRule[]; } +/** + * A collection resource selected from the resource picker. Collections are groups of products organized by manual curation or automated rules. Use collection data to access product groupings, organizational information, and collection metadata. + */ interface Collection extends Resource { + /** The number of sales channels where this collection can be published. Use this to understand the collection's potential reach across different storefronts. */ availablePublicationCount: number; + /** The collection description as plain text without HTML formatting. Use this when you need the description without markup. */ description: string; + /** The collection description formatted as HTML. Use this to display rich text descriptions with formatting and styling in your UI. */ descriptionHtml: string; + /** The URL-friendly unique identifier used in collection URLs (for example, `'summer-collection'` in `/collections/summer-collection`). Use this to generate collection links or match URL paths. */ handle: string; + /** The collection's unique global identifier (GID). Use this ID for collection-related GraphQL operations. */ id: string; + /** The featured image displayed for the collection. Use this for collection thumbnails, headers, or preview images. */ image?: ResourceImage | null; + /** The count of products automatically added to the collection based on automation rules. Use this to understand how many products match the collection's criteria. */ productsAutomaticallySortedCount: number; + /** The total number of products in the collection, including both manually added and automatically included products. Use this to show collection size or for pagination. */ productsCount: number; + /** The count of products manually added by the merchant. Use this to understand how much manual curation the collection has. */ productsManuallySortedCount: number; + /** The number of sales channels where the collection is currently published. Use this to check the collection's actual visibility across storefronts. */ publicationCount: number; + /** The automation rules that determine which products are automatically included. Present only for automated (smart) collections. When `null`, the collection is manually curated. Use this to understand the collection's filtering logic. */ ruleSet?: RuleSet | null; + /** Search engine optimization metadata for the collection. Use this to understand how the collection appears in search engine results. */ seo: { + /** The SEO meta description that appears in search engine result snippets. This summarizes the collection for search engines. */ description?: string | null; + /** The SEO page title that appears in browser tabs and search results. When `null`, the collection's regular title is used. */ title?: string | null; }; + /** The default sort order that determines how products are arranged in the collection. This controls the product sequence customers see on the storefront. */ sortOrder: CollectionSortOrder; + /** The Storefront API identifier for this collection. Use this ID when making Storefront API queries for this collection. */ storefrontId: string; + /** The theme template suffix for using custom theme templates (for example, `'featured'` to use `collection.featured.liquid`). When `null`, uses the default collection template. */ templateSuffix?: string | null; + /** The collection's display name shown to merchants and customers. Use this as the primary collection identifier in lists and displays. */ title: string; + /** ISO 8601 timestamp when the collection was last updated. Use this to track changes, sync with external systems, or show freshness indicators. */ updatedAt: string; } +/** + * A product variant resource selected from the resource picker. Product variants represent specific combinations of product options (for example, a t-shirt in size Medium and color Blue). Use variant data to access pricing, inventory, and option-specific information. + */ interface ProductVariant extends Resource { + /** Whether the variant is currently available for purchase. When `false`, the variant can't be added to orders even if inventory exists. Use this to check if customers can buy this variant. */ availableForSale: boolean; + /** The barcode, UPC, or ISBN number for the variant. Use this to scan products, integrate with inventory systems, or match physical products to Shopify data. */ barcode?: string | null; + /** The original price before any discounts or markdowns. When present, indicates the variant is on sale and can be displayed as a "compare at" or "was" price to show savings. */ compareAtPrice?: Money | null; + /** ISO 8601 timestamp when the variant was first created. Use this to track when products were added to the catalog or sort variants by creation date. */ createdAt: string; + /** A human-readable display name that combines the product title with the variant's option values (for example, "T-Shirt - Medium / Blue"). Use this for displaying the complete variant identity in lists or UI. */ displayName: string; + /** The fulfillment service responsible for handling orders of this variant. Use this to determine how the product is fulfilled (manual, third-party, or gift card). */ fulfillmentService?: { + /** The unique identifier for the fulfillment service. */ id: string; + /** Whether this service tracks and manages inventory levels. When `true`, the service controls stock counts. */ inventoryManagement: boolean; + /** Whether fulfillment is product-based (fulfills specific products) or location-based (fulfills from specific warehouses). */ productBased: boolean; + /** The display name of the fulfillment service (for example, "Amazon Fulfillment" or "Manual"). */ serviceName: string; + /** The type of fulfillment service determining how orders are processed. */ type: FulfillmentServiceType; }; + /** The image displayed for this variant. When present, shows variant-specific imagery instead of the product's default image. Use this for displaying variant previews. */ image?: ResourceImage | null; + /** A reference to the inventory item that tracks stock levels for this variant. Use this ID for inventory-related GraphQL operations. */ inventoryItem: {id: string}; + /** How inventory is tracked for this variant. Determines whether Shopify, a fulfillment service, or no system tracks stock levels. */ inventoryManagement: ProductVariantInventoryManagement; + /** Whether to allow purchases when inventory is unavailable. When set to continue, customers can buy out-of-stock variants. When set to deny, purchases are blocked when stock is zero. */ inventoryPolicy: ProductVariantInventoryPolicy; + /** The total available inventory quantity summed across all locations. Use this for at-a-glance stock checks, though individual location quantities may vary. */ inventoryQuantity?: number | null; + /** The display position of this variant in the product's variant list. Lower numbers appear first. Use this to maintain variant ordering in your UI. */ position: number; + /** The current selling price for this variant. This is the amount customers pay before any checkout-level discounts. */ price: Money; + /** The parent product that this variant belongs to. Contains partial product data for accessing product-level information without a separate query. */ product: Partial; + /** Whether this variant requires physical shipping. When `false`, the variant is digital or doesn't need delivery (for example, gift cards, digital downloads, or services). */ requiresShipping: boolean; + /** The option values that define this specific variant (for example, `[{value: "Large"}, {value: "Blue"}]` for size and color). The array order matches the product's option definitions. */ selectedOptions: {value?: string | null}[]; + /** The Stock Keeping Unit (SKU) identifier for inventory tracking and order management. Use this to match variants with warehouse systems, barcodes, or fulfillment data. */ sku?: string | null; + /** Whether this variant is taxable. When `true`, applicable taxes are calculated at checkout based on the customer's location and tax rules. */ taxable: boolean; + /** The display name showing only the variant's option values (for example, "Medium / Blue" without the product title). Use this when the product name is already displayed. */ title: string; + /** The physical weight of the variant. Use this for calculating shipping costs or determining fulfillment requirements. */ weight?: number | null; + /** The unit of measurement for the weight value (kilograms, grams, pounds, or ounces). */ weightUnit: WeightUnit; + /** ISO 8601 timestamp when the variant was last updated. Use this to track changes or sync with external systems. */ updatedAt: string; } +/** + * A product resource selected from the resource picker. Products are the items sold in a Shopify store and can have multiple variants representing different options (like size or color). Use product data to access titles, descriptions, images, pricing, and variant information. + */ interface Product extends Resource { + /** The number of sales channels where this product can be published. Use this to understand the product's potential reach across different storefronts. */ availablePublicationCount: number; + /** ISO 8601 timestamp when the product was first created in the catalog. Use this to track product age or sort by creation date. */ createdAt: string; + /** The product description formatted as HTML. Use this to display rich text descriptions with formatting, links, and styling in your UI. */ descriptionHtml: string; + /** The URL-friendly unique identifier used in product URLs (for example, `'blue-t-shirt'` in `/products/blue-t-shirt`). Use this to generate product links or match URL paths. */ handle: string; + /** Whether the product has only the default variant with no custom options. When `true`, the product has no size, color, or other option variations. Use this to simplify UI for single-variant products. */ hasOnlyDefaultVariant: boolean; + /** An array of images associated with the product. The first image typically serves as the main product image. Use these for displaying product galleries or thumbnails. */ images: ResourceImage[]; + /** Product options that define how variants differ (for example, Size, Color, Material). Each option can have multiple values that combine to create unique variants. */ options: { + /** The unique identifier for this option. */ id: string; + /** The display name for this option (for example, "Size", "Color", "Material"). Use this as a label when showing variant choices. */ name: string; + /** The display order position. Lower numbers appear first when showing options to merchants or customers. */ position: number; + /** An array of available values for this option (for example, `["Small", "Medium", "Large"]` for a Size option). Use these to build variant selection interfaces. */ values: string[]; }[]; + /** The product category or type used for organization and filtering (for example, "T-Shirt", "Shoes", "Electronics"). Use this to group similar products or filter catalogs. */ productType: string; + /** ISO 8601 timestamp when the product was first published to a sales channel. When `null`, the product has never been published. Use this to identify draft products or track publishing history. */ publishedAt?: string | null; + /** An array of tags for categorizing and filtering products (for example, `["summer", "sale", "featured"]`). Use tags for custom organization, filtering, or search functionality. */ tags: string[]; + /** The theme template suffix for using custom theme templates (for example, `'alternate'` to use `product.alternate.liquid`). When `null`, uses the default product template. */ templateSuffix?: string | null; + /** The product's display name shown to merchants and customers. Use this as the primary product identifier in lists and displays. */ title: string; + /** The total available inventory summed across all variants and locations. Use this for at-a-glance stock checks, though individual variant quantities may vary. */ totalInventory: number; + /** The total number of variants this product has. Use this to determine if the product has multiple options or only the default variant. */ totalVariants: number; + /** Whether inventory levels are tracked for this product. When `false`, the product is always considered in stock regardless of quantity. */ tracksInventory: boolean; + /** An array of all product variants. Each variant represents a unique combination of option values. Contains partial variant data for accessing variant-level information. */ variants: Partial[]; + /** The brand or manufacturer name. Use this for filtering by brand, displaying brand information, or organizing products by vendor. */ vendor: string; + /** ISO 8601 timestamp when the product was last updated. Use this to track changes, sync with external systems, or show freshness indicators. */ updatedAt: string; + /** The publication status indicating whether the product is active (published), archived (discontinued), or draft (unpublished). Use this to filter products by availability state. */ status: ProductStatus; } +/** + * Map of resource type identifiers to their corresponding resource interfaces. + */ interface ResourceTypes { product: Product; variant: ProductVariant; collection: Collection; } +/** + * Extracts the resource interface for a given resource type. + */ type ResourceSelection = ResourceTypes[Type]; +/** + * Wraps a type with a deprecated selection property for backward compatibility. + */ type WithSelection = T & { /** * @private - * @deprecated + * @deprecated Use the outer array directly instead. */ selection: T; }; +/** + * The payload returned when resources are selected from the picker. + */ type SelectPayload = WithSelection< ResourceSelection[] >; +/** + * Filter options that control which resources appear in the resource picker. Use filters to restrict the available resources based on publication status, resource type, or custom search criteria. + */ interface Filters { /** - * Whether to show hidden resources, referring to products that are not published on any sales channels. + * Whether to include products that aren't published on any sales channels. When `false`, only products published to at least one sales channel appear in the picker. Use this to ensure merchants only select products that customers can purchase. * @defaultValue true */ hidden?: boolean; /** - * Whether to show product variants. Only applies to the Product resource type picker. + * Whether to show product variants in the picker. When `false`, merchants can only select products, not individual variants. Only applies when `type` is `'product'`. Use this to simplify selection when you only need product-level data. * @defaultValue true */ variants?: boolean; /** - * Whether to show [draft products](https://help.shopify.com/en/manual/products/details?shpxid=70af7d87-E0F2-4973-8B09-B972AAF0ADFD#product-availability). - * Only applies to the Product resource type picker. - * Setting this to undefined will show a badge on draft products. + * Whether to include draft products in the picker. When `false`, draft products are hidden. When `undefined`, draft products appear with a draft badge. Only applies when `type` is `'product'`. Use this to prevent selecting products that aren't ready for use. * @defaultValue true */ draft?: boolean | undefined; /** - * Whether to show [archived products](https://help.shopify.com/en/manual/products/details?shpxid=70af7d87-E0F2-4973-8B09-B972AAF0ADFD#product-availability). - * Only applies to the Product resource type picker. - * Setting this to undefined will show a badge on draft products. + * Whether to include archived products in the picker. When `false`, archived products are hidden. When `undefined`, archived products appear with an archived badge. Only applies when `type` is `'product'`. Use this to prevent selecting discontinued products. * @defaultValue true */ archived?: boolean | undefined; /** - * GraphQL initial search query for filtering resources available in the picker. - * See [search syntax](https://shopify.dev/docs/api/usage/search-syntax) for more information. - * This is not displayed in the search bar when the picker is opened. + * A GraphQL search query that filters the available resources without showing the query in the picker's search bar. Merchants won't see or edit this filter. See [search syntax](https://shopify.dev/docs/api/usage/search-syntax) for the query format. Use this to programmatically restrict resources based on attributes like tags, vendor, or product type (for example, `"tag:featured"` or `"vendor:Acme"`). */ query?: string; } +/** + * The `ResourcePickerOptions` object defines how the resource picker behaves, including which resource type to display, selection limits, filters, and preselected items. Access the following properties on the `ResourcePickerOptions` object to configure the resource picker's appearance and functionality. + */ export interface ResourcePickerOptions { /** - * The type of resource you want to pick. + * The type of Shopify resource to select: `'product'` for products, `'variant'` for specific product variants, or `'collection'` for collections. This determines what appears in the picker and what data structure is returned. */ type: 'product' | 'variant' | 'collection'; /** - * The action verb appears in the title and as the primary action of the Resource Picker. + * The action verb that appears in the picker's title and primary button. Use `'add'` for actions that add new items (for example, "Add products") or `'select'` for choosing existing items (for example, "Select products"). This helps merchants understand the picker's purpose. * @defaultValue 'add' */ action?: 'add' | 'select'; /** - * Filters for what resource to show. + * Filtering options that control which resources appear in the picker. Use filters to restrict resources by publication status, include or exclude variants, or apply custom search criteria. This helps merchants find relevant items faster. */ filter?: Filters; /** - * Whether to allow selecting multiple items of a specific type or not. If a number is provided, then limit the selections to a maximum of that number of items. When type is Product, the user may still select multiple variants of a single product, even if multiple is false. + * The selection mode for the picker. Pass `true` to allow unlimited selections, `false` for single-item selection only, or a number to set a maximum selection limit (for example, `5` allows up to five items). When `type` is `'product'`, merchants can still select multiple variants from a single product even if `multiple` is `false`. * @defaultValue false */ multiple?: boolean | number; /** - * GraphQL initial search query for filtering resources available in the picker. See [search syntax](https://shopify.dev/docs/api/usage/search-syntax) for more information. - * This is displayed in the search bar when the picker is opened and can be edited by users. - * For most use cases, you should use the `filter.query` option instead which doesn't show the query in the UI. + * An initial search query that appears in the picker's search bar when it opens. Merchants can see and edit this query. See [search syntax](https://shopify.dev/docs/api/usage/search-syntax) for the query format. For most use cases, use `filter.query` instead, which filters results without exposing the query to merchants. * @defaultValue '' */ query?: string; /** - * Resources that should be preselected when the picker is opened. + * Resources that should be preselected when the picker opens. Pass an array of resource objects with IDs (and optional variant IDs) to show which items are already selected. Merchants can deselect these preselected items. Use this to show current selections or default choices. * @defaultValue [] */ selectionIds?: BaseResource[]; } +/** + * Opens the resource picker modal for selecting products, variants, or collections. Returns the selected resources when the user confirms their selection, or undefined if they cancel. + */ export type ResourcePickerApi = ( options: ResourcePickerOptions, ) => Promise | undefined>; diff --git a/packages/ui-extensions/src/surfaces/admin/api/shared.ts b/packages/ui-extensions/src/surfaces/admin/api/shared.ts index b4c1a6d1c3..f3f6b18a8a 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/shared.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/shared.ts @@ -1,10 +1,16 @@ +/** + * The `Data` object provides access to currently viewed or selected resources in the admin context. + */ export interface Data { /** - * Information about the currently viewed or selected items. + * An array of currently viewed or selected resource identifiers. Use this to access the IDs of items in the current context, such as selected products in an index page or the product being viewed on a details page. The available IDs depend on the extension target and user interactions. */ selected: {id: string}[]; } +/** + * The supported [metafield definition types](/docs/apps/build/metafields/list-of-data-types) for storing extension configuration data. Use these types to specify how metafield values should be formatted, validated, and displayed. Types prefixed with `list.` store arrays of values, while other types store single values. Choose a type that matches your data format (for example, use `'number_integer'` for whole numbers, `'single_line_text_field'` for short text, or `'json'` for complex structured data). + */ export type SupportedDefinitionType = | 'boolean' | 'collection_reference' diff --git a/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/bulk-selection-check.jsx b/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/bulk-selection-check.jsx new file mode 100644 index 0000000000..b69f75fa7f --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/bulk-selection-check.jsx @@ -0,0 +1,8 @@ +export default () => { + const {data} = shopify; + + const selectedCount = data.selected.length; + const isWithinLimit = selectedCount > 0 && selectedCount <= 50; + + return {display: isWithinLimit}; +}; diff --git a/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/check-order-status.jsx b/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/check-order-status.jsx new file mode 100644 index 0000000000..b2bc415366 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/check-order-status.jsx @@ -0,0 +1,7 @@ +export default () => { + const {data} = shopify; + + const selectedCount = data.selected.length; + + return {display: selectedCount === 1}; +}; diff --git a/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/check-product-tag.jsx b/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/check-product-tag.jsx new file mode 100644 index 0000000000..23c4ceb70b --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/should-render/examples/check-product-tag.jsx @@ -0,0 +1,7 @@ +export default () => { + const {data} = shopify; + + const hasSelection = data.selected.length > 0; + + return {display: hasSelection}; +}; diff --git a/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.doc.ts index 8401bf068c..08e60ea77d 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.doc.ts @@ -3,19 +3,86 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Should Render API', description: - 'This API controls the render state of an admin action extension. Learn more in the admin extensions tutorial.', + 'The Should Render API lets you [conditionally show or hide admin action extensions](/docs/apps/build/admin/actions-blocks/hide-extensions) dynamically. Use this API to control action visibility based on resource state, user permissions, or business logic.', isVisualComponent: false, type: 'API', + defaultExample: { + description: + 'Return `true` to show the action extension only when items are selected. This simple check prevents the action extension from appearing on empty pages or when no resources are chosen.', + codeblock: { + title: 'Check when items are selected', + tabs: [ + { + title: 'jsx', + code: './examples/check-product-tag.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'ShouldRenderApi', - description: '', + title: 'Properties', + description: + 'The `ShouldRenderApi` object provides properties for controlling action extension visibility. Access the following properties on the `ShouldRenderApi` object to determine whether an associated action should appear based on the current context.', type: 'ShouldRenderApi', }, ], + examples: { + description: 'Conditionally show or hide action extensions', + examples: [ + { + description: + 'Check if exactly one item is selected before showing the action extension. This pattern ensures action extensions that operate on individual resources only appear when appropriate.', + codeblock: { + title: 'Require one item to be selected', + tabs: [ + { + title: 'jsx', + code: './examples/check-order-status.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Validate selection count is between 1 and 50 before showing bulk actions. This example prevents the action extension from appearing when nothing is selected or when too many items would overload the operation.', + codeblock: { + title: 'Validate selection count', + tabs: [ + { + title: 'jsx', + code: './examples/bulk-selection-check.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Utility APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Keep evaluation under ~50ms:** Slow `shouldRender` functions delay page rendering for all merchants. Profile your logic and optimize for speed.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + '- The function must return an object with a `display` property. Returning a plain boolean like `true` instead of `{ display: true }` fails.\n' + + "- No asynchronous operations are supported. Async functions, promises, fetch calls, and timers won't work.\n" + + "- Your extension can't access external data sources. Evaluation is limited to data available in `api.data.selected` and in-memory state.\n" + + "- No re-evaluation occurs after initial render. If conditions change after page load, the action visibility doesn't update dynamically.", + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.ts b/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.ts index 6b3f2a8b0d..c82c06d497 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/should-render/should-render.ts @@ -2,14 +2,21 @@ import type {StandardApi} from '../standard/standard'; import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; import type {Data} from '../shared'; +/** + * The output returned by `should-render` extensions to control visibility. + */ export interface ShouldRenderOutput { + /** Whether to display the associated action extension. Return `true` to show the action, `false` to hide it. */ display: boolean; } +/** + * The `ShouldRenderApi` object provides methods for controlling action extension visibility. Access the following properties on the `ShouldRenderApi` object to determine whether an associated action should appear based on the current context. + */ export interface ShouldRenderApi extends StandardApi { /** - * Information about the currently viewed or selected items. + * An array of currently viewed or selected resource identifiers. Use this data to determine whether the action extension should appear based on the current context. */ data: Data; } diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/examples/authenticate-backend-request.jsx b/packages/ui-extensions/src/surfaces/admin/api/standard/examples/authenticate-backend-request.jsx new file mode 100644 index 0000000000..7170a930e9 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/examples/authenticate-backend-request.jsx @@ -0,0 +1,40 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + + const handleFetch = async () => { + setLoading(true); + + const token = await shopify.auth.idToken(); + + const response = await fetch('https://my-app.com/api/products', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + setProducts(data); + setLoading(false); + }; + + return ( + + + {loading ? 'Loading...' : 'Fetch from Backend'} + + {products.length > 0 && ( + {products.length} products loaded + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/examples/persist-settings.jsx b/packages/ui-extensions/src/surfaces/admin/api/standard/examples/persist-settings.jsx new file mode 100644 index 0000000000..df28609870 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/examples/persist-settings.jsx @@ -0,0 +1,43 @@ +import {render} from 'preact'; +import {useState, useEffect} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [preferences, setPreferences] = useState(null); + + useEffect(() => { + const loadPreferences = async () => { + const prefs = await shopify.storage.get('userPreferences'); + setPreferences(prefs); + }; + + loadPreferences(); + }, []); + + const handleSave = async () => { + await shopify.storage.set('userPreferences', { + theme: 'dark', + notifications: true, + defaultView: 'grid', + }); + + const updated = await shopify.storage.get('userPreferences'); + setPreferences(updated); + }; + + return ( + + Save Preferences + {preferences && ( + + Theme: {preferences.theme} + Notifications: {String(preferences.notifications)} + View: {preferences.defaultView} + + )} + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/examples/query-and-mutate.jsx b/packages/ui-extensions/src/surfaces/admin/api/standard/examples/query-and-mutate.jsx new file mode 100644 index 0000000000..63efff7e62 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/examples/query-and-mutate.jsx @@ -0,0 +1,75 @@ +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const [products, setProducts] = useState([]); + const [updated, setUpdated] = useState(false); + + const handleQuery = async () => { + const {data: productsData} = await shopify.query( + `query GetProducts { + products(first: 10) { + edges { + node { + id + title + totalInventory + } + } + } + }`, + ); + + setProducts(productsData.products.edges); + }; + + const handleUpdate = async () => { + const productId = products[0]?.node.id; + + if (!productId) return; + + const {data: updateData} = await shopify.query( + `mutation UpdateProduct($id: ID!, $input: ProductInput!) { + productUpdate(id: $id, product: $input) { + product { + id + tags + } + userErrors { + field + message + } + } + }`, + { + variables: { + id: productId, + input: {tags: ['processed', 'reviewed']}, + }, + }, + ); + + if (updateData.productUpdate.product) { + setUpdated(true); + } + }; + + return ( + + + Query Products + {products.length > 0 && ( + <> + {products.length} products found + Update First Product + + )} + {updated && Product tags updated!} + + + ); +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/standard-rendering.ts b/packages/ui-extensions/src/surfaces/admin/api/standard/standard-rendering.ts new file mode 100644 index 0000000000..ce7f703332 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/standard-rendering.ts @@ -0,0 +1,18 @@ +import type {PickerApi} from '../picker/picker'; +import type {ResourcePickerApi} from '../resource-picker/resource-picker'; +import type {StandardApi} from './standard'; +import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets'; + +export interface StandardRenderingExtensionApi< + ExtensionTarget extends AnyExtensionTarget, +> extends StandardApi { + /** + * Opens the [resource picker](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/resource-picker-api) modal for selecting products, variants, or collections. Returns the selected resources when the user confirms their selection, or undefined if they cancel. + */ + resourcePicker: ResourcePickerApi; + + /** + * Opens a custom selection dialog with your app-specific data. Use the [Picker API](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/picker-api) to define the picker's heading, items, headers, and selection behavior. Returns a Promise that resolves to a `Picker` object with a `selected` property for accessing the merchant's selection. + */ + picker: PickerApi; +} diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/standard.doc.ts b/packages/ui-extensions/src/surfaces/admin/api/standard/standard.doc.ts index 3daa8d5632..6de36d153b 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/standard/standard.doc.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/standard.doc.ts @@ -2,19 +2,90 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; const data: ReferenceEntityTemplateSchema = { name: 'Standard API', - description: 'This API is available to all extension types.', + description: + 'The Standard API provides core functionality available to all Admin UI extension types. Use this API to authenticate with your app backend, query the [GraphQL Admin API](/docs/api/admin-graphql), translate content, handle navigation [intents](/docs/api/admin-extensions/{API_VERSION}/target-apis/utility-apis/intents-api), and persist data in browser storage.', isVisualComponent: false, type: 'API', + defaultExample: { + description: + 'Retrieve an authentication token and use it to fetch data from your app backend. This example gets the ID token, adds it to request headers, and displays loading states while fetching.', + codeblock: { + title: 'Authenticate backend requests', + tabs: [ + { + title: 'jsx', + code: './examples/authenticate-backend-request.jsx', + language: 'jsx', + }, + ], + }, + }, definitions: [ { - title: 'StandardApi', - description: '', + title: 'Properties', + description: + 'The `StandardApi` object provides core properties available to all extension targets. Access the following properties on the `StandardApi` object to authenticate users, query the [GraphQL Admin API](/docs/api/admin-graphql), translate content, handle intents, and persist data.', type: 'StandardApi', }, ], + examples: { + description: 'Essential patterns for all extensions', + examples: [ + { + description: + 'Query products using the [GraphQL Admin API](/docs/api/admin-graphql/), then update the first product with new tags. This example demonstrates chaining a query and mutation, handling the response data, and showing success feedback.', + codeblock: { + title: 'Query and mutate product data', + tabs: [ + { + title: 'jsx', + code: './examples/query-and-mutate.jsx', + language: 'jsx', + }, + ], + }, + }, + { + description: + 'Save and retrieve user preferences from browser storage. This example loads saved preferences on mount, displays current values, and lets merchants update settings that persist across sessions.', + codeblock: { + title: 'Persist settings', + tabs: [ + { + title: 'jsx', + code: './examples/persist-settings.jsx', + language: 'jsx', + }, + ], + }, + }, + ], + }, category: 'Target APIs', subCategory: 'Core APIs', related: [], + subSections: [ + { + type: 'Generic', + anchorLink: 'best-practices', + title: 'Best practices', + sectionContent: + '- **Handle GraphQL partial data:** Check both `errors` and `data` in query responses. GraphQL returns partial data with errors when some fields fail but others succeed.\n' + + '- **Catch `StorageExceededError` exceptions:** `storage.set()` and `storage.setMany()` throw `StorageExceededError` when you exceed storage limits. Catch these errors and handle quota failures gracefully.\n' + + '- **Use `storage.setMany()` for batch updates:** When updating multiple related values, use `setMany()` with an array of entries for efficient batch operations.\n' + + '- **Batch GraphQL queries:** Combine multiple queries in a single GraphQL request using aliases to reduce roundtrips and improve performance under rate limits.', + }, + { + type: 'Generic', + anchorLink: 'limitations', + title: 'Limitations', + sectionContent: + '- Storage is scoped per extension. Data saved by one extension is inaccessible to other extensions, even from the same app.\n' + + "- Storage values are serialized with `JSON.stringify`, so functions, symbols, and circular references aren't supported.\n" + + "- GraphQL queries share [rate limits](/docs/api/usage/limits) with your app's overall Admin API usage and are subject to the shop's installed [access scopes](/docs/api/usage/access-scopes).\n" + + '- ID tokens from `auth.idToken()` are short-lived JWTs. Call `auth.idToken()` on each request instead of caching tokens.', + }, + ], }; export default data; diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/standard.ts b/packages/ui-extensions/src/surfaces/admin/api/standard/standard.ts index 97534598e4..8cc5f7b85b 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/standard/standard.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/standard.ts @@ -7,57 +7,67 @@ import type {Intents} from '../intents/intents'; export type {Intents} from '../intents/intents'; /** - * GraphQL error returned by the Shopify Admin API. + * The GraphQL error returned by the [GraphQL Admin API](/docs/api/admin-graphql). */ export interface GraphQLError { + /** + * A human-readable error message describing what went wrong with the GraphQL query. Use this to understand the cause of the error and how to fix your query. + */ message: string; + /** + * The location in the GraphQL query where the error occurred. Provides the line number and column position to help identify the exact source of the error in your query string. + */ locations: { + /** The line number in the GraphQL query where the error occurred. */ line: number; + /** The column position in the GraphQL query where the error occurred. */ column: string; }; } +/** + * The `Auth` object provides authentication methods for secure communication with your app backend. + */ interface Auth { /** - * Retrieves a Shopify OpenID Connect ID token for the current user. + * Retrieves a [Shopify OpenID Connect ID token](/docs/api/app-home/apis/id-token) for the current user. Use this token to authenticate requests to your app backend and verify the user's identity. The token is a signed JWT that contains user information and can be validated using Shopify's public keys. Returns `null` if the token can't be retrieved. */ idToken: () => Promise; } /** - * The following APIs are provided to all extension targets. + * The `StandardApi` object provides core methods available to all extension targets. Access the following properties on the `StandardApi` object to authenticate users, query the [GraphQL Admin API](/docs/api/admin-graphql), translate content, handle intents, and persist data. */ export interface StandardApi { /** - * The identifier of the running extension target. + * The identifier of the running extension target. Use this to determine which target your extension is rendering in and conditionally adjust functionality or UI based on the extension context. */ extension: { target: ExtensionTarget; }; /** - * Provides methods for authenticating calls to an app backend. + * Provides methods for authenticating calls to your app backend. Use the `idToken()` method to retrieve a signed JWT token that verifies the current user's identity for secure server-side operations. */ auth: Auth; /** - * Utilities for translating content according to the current localization of the admin. - * More info - https://shopify.dev/docs/apps/checkout/best-practices/localizing-ui-extensions + * Utilities for translating content according to the current localization of the admin. Use these methods to provide translated strings that match the merchant's language preferences, ensuring your extension is accessible to a global audience. */ i18n: I18n; /** - * Provides information to the receiver of an intent. + * Provides information to the receiver of an intent. Use this to access data passed from other extensions or parts of the admin when your extension is launched through intent-based navigation. */ intents: Intents; /** - * Provides methods for setting, getting, and clearing browser data from the extension + * Provides methods for persisting data in browser storage that is scoped to your extension. Use this to store user preferences, cache data, maintain state across sessions, or save temporary working data. Storage is persistent across page reloads and isolated per extension. */ storage: Storage; /** - * Used to query the Admin GraphQL API + * Executes GraphQL queries against the [GraphQL Admin API](/docs/api/admin-graphql). Use this to fetch shop data, manage resources, or perform mutations. Queries are automatically authenticated with the current user's permissions. Optionally specify GraphQL variables and API version for your query. */ query: ( query: string, diff --git a/packages/ui-extensions/src/surfaces/admin/api/standard/storage.ts b/packages/ui-extensions/src/surfaces/admin/api/standard/storage.ts index 086a5eee67..d713a98b46 100644 --- a/packages/ui-extensions/src/surfaces/admin/api/standard/storage.ts +++ b/packages/ui-extensions/src/surfaces/admin/api/standard/storage.ts @@ -2,13 +2,12 @@ export interface Storage< BaseStorageTypes extends Record = Record, > { /** - * Sets the value of a key in the storage. + * Sets the value of a key in the storage. Use this to save individual data items like user preferences, form state, or cached values. The value is serialized using `JSON.stringify`, so it can be any primitive type, object, or array that JSON supports. * - * @param key - The key to set the value for. - * @param value - The value to set for the key. - * Can be any primitive type supported by `JSON.stringify`. + * @param key - The key to set the value for. Use descriptive keys to organize your stored data. + * @param value - The value to set for the key. Can be any primitive type supported by `JSON.stringify`. * - * Rejects with a `StorageExceededError` if the extension exceeds its allotted storage limit. + * @throws {StorageExceededError} Rejects with a `StorageExceededError` if the extension exceeds its allotted storage limit. */ set< StorageTypes extends BaseStorageTypes = BaseStorageTypes, @@ -19,26 +18,21 @@ export interface Storage< ): Promise; /** - * Sets multiple key-value pairs in the storage at once. + * Sets multiple key-value pairs in the storage at once. Use this for efficient batch operations when you need to save multiple related values together, such as form data or configuration settings. * - * If the operation fails, no changes are made to storage. + * @param entries - An object containing key-value pairs to store. Values can be any primitive type supported by `JSON.stringify`. * - * @param entries - An object containing key-value pairs to store. - * Values can be any primitive type supported by `JSON.stringify`. - * - * Rejects with a `StorageExceededError` if the extension exceeds its allotted storage limit. + * @throws {StorageExceededError} Rejects with a `StorageExceededError` if the extension exceeds its allotted storage limit. */ setMany( entries: Partial, ): Promise; /** - * Gets the value of a key in the storage. + * Gets the value of a key in the storage. Use this to retrieve previously saved data when your extension loads or when you need to access stored values. The value is automatically deserialized from JSON to its original type. * * @param key - The key to get the value for. - * @returns The value of the key. - * - * If no value for the key exists, the resolved value is undefined. + * @returns The value of the key, or `undefined` if no value exists for the key. */ get< StorageTypes extends BaseStorageTypes = BaseStorageTypes, @@ -48,12 +42,10 @@ export interface Storage< ): Promise; /** - * Gets the values of multiple keys in the storage at once. + * Gets the values of multiple keys in the storage at once. Use this to efficiently retrieve related data in a single operation, reducing overhead when loading multiple stored values. The returned array is in the same order as the provided keys, with `undefined` values for keys that don't exist in storage. * * @param keys - An array of keys to get the values for. - * @returns An object containing key-value pairs for the requested keys. - * - * The returned array is in the same order as `keys`, with `undefined` values for keys that do not exist. + * @returns An array containing values for the requested keys, in the same order as the input keys. */ getMany< StorageTypes extends BaseStorageTypes = BaseStorageTypes, @@ -63,15 +55,15 @@ export interface Storage< ): Promise<(StorageTypes[Keys] | undefined)[]>; /** - * Clears the storage. + * Clears all data from the storage. Use this to reset your extension's storage, such as when implementing a logout flow, clearing cached data, or resetting to defaults. This operation removes all stored key-value pairs. */ clear(): Promise; /** - * Deletes a key from the storage. + * Deletes a specific key from the storage. Use this to remove individual data items that are no longer needed, freeing up storage space and maintaining data hygiene. * * @param key - The key to delete. - * @returns A promise that resolves to `true` if the key was deleted, or `false` if the key did not exist. + * @returns A promise that resolves to `true` if the key existed and was deleted, or `false` if the key did not exist. */ delete< StorageTypes extends BaseStorageTypes = BaseStorageTypes, @@ -81,11 +73,10 @@ export interface Storage< ): Promise; /** - * Deletes multiple keys from the storage at once. + * Deletes multiple keys from the storage at once. Use this to efficiently remove several related data items in a single operation, such as clearing expired cache entries or removing a group of related settings. * * @param keys - An array of keys to delete. - * @returns A promise that resolves to an object with `keys` keys, and boolean values, - * which are `true` if the key was deleted, or `false` if the key did not exist. + * @returns A promise that resolves to an object mapping each key to a boolean value: `true` if the key existed and was deleted, or `false` if the key did not exist. */ deleteMany< StorageTypes extends BaseStorageTypes = BaseStorageTypes, @@ -95,9 +86,9 @@ export interface Storage< ): Promise>; /** - * Gets all the keys and values in the storage. + * Gets all the keys and values in the storage. Use this to iterate over all stored data, useful for debugging, data migration, or displaying all stored settings. The returned iterator provides entries as `[key, value]` tuples. * - * @returns An iterator containing all the keys and values in the storage. + * @returns A promise that resolves to an iterator containing all key-value pairs in the storage. */ entries< StorageTypes extends BaseStorageTypes = BaseStorageTypes, @@ -105,6 +96,9 @@ export interface Storage< >(): Promise>; } +/** + * Error thrown when storage operations exceed the available storage quota. This can occur during `set()` or `setMany()` operations when the total stored data size exceeds the limit allocated to your extension. + */ export interface StorageExceededError extends Error { name: 'StorageExceededError'; } diff --git a/packages/ui-extensions/src/surfaces/admin/components/ActionExtensionComponents.ts b/packages/ui-extensions/src/surfaces/admin/components/ActionExtensionComponents.ts index 587316c2fc..006973eef8 100644 --- a/packages/ui-extensions/src/surfaces/admin/components/ActionExtensionComponents.ts +++ b/packages/ui-extensions/src/surfaces/admin/components/ActionExtensionComponents.ts @@ -1,5 +1,8 @@ import {StandardComponents} from './StandardComponents'; +/** + * The components available for building action extensions. Includes all standard components plus the `AdminAction` component required for action extension setup. + */ export type ActionExtensionComponents = StandardComponents | 'AdminAction'; export default ActionExtensionComponents; diff --git a/packages/ui-extensions/src/surfaces/admin/components/BlockExtensionComponents.ts b/packages/ui-extensions/src/surfaces/admin/components/BlockExtensionComponents.ts index 512573102c..b82316783f 100644 --- a/packages/ui-extensions/src/surfaces/admin/components/BlockExtensionComponents.ts +++ b/packages/ui-extensions/src/surfaces/admin/components/BlockExtensionComponents.ts @@ -1,5 +1,8 @@ import {StandardComponents} from './StandardComponents'; +/** + * The components available for building block extensions. Includes all standard components plus the `AdminBlock` component required for block extension setup and the `Form` component for creating forms. + */ export type BlockExtensionComponents = | StandardComponents | 'AdminBlock' diff --git a/packages/ui-extensions/src/surfaces/admin/components/FormExtensionComponents.ts b/packages/ui-extensions/src/surfaces/admin/components/FormExtensionComponents.ts index 6850eca740..fbc4ca6db2 100644 --- a/packages/ui-extensions/src/surfaces/admin/components/FormExtensionComponents.ts +++ b/packages/ui-extensions/src/surfaces/admin/components/FormExtensionComponents.ts @@ -1,5 +1,8 @@ import {StandardComponents} from './StandardComponents'; +/** + * The components available for building form-based extensions. Includes all standard components plus the `Form` component for creating structured data collection interfaces with submission handling and validation. + */ export type FormExtensionComponents = StandardComponents | 'Form'; export default FormExtensionComponents; diff --git a/packages/ui-extensions/src/surfaces/admin/components/FunctionSettingsComponents.ts b/packages/ui-extensions/src/surfaces/admin/components/FunctionSettingsComponents.ts index 835ff43e42..56f23a5cc1 100644 --- a/packages/ui-extensions/src/surfaces/admin/components/FunctionSettingsComponents.ts +++ b/packages/ui-extensions/src/surfaces/admin/components/FunctionSettingsComponents.ts @@ -1,5 +1,8 @@ import {FormExtensionComponents} from './FormExtensionComponents'; +/** + * The components available for building function settings extensions. Includes all form components plus the `FunctionSettings` component required for function settings configuration. + */ export type FunctionSettingsComponents = | FormExtensionComponents | 'FunctionSettings'; diff --git a/packages/ui-extensions/src/surfaces/admin/components/PrintActionExtensionComponents.ts b/packages/ui-extensions/src/surfaces/admin/components/PrintActionExtensionComponents.ts index 0d224e1072..9db1896aea 100644 --- a/packages/ui-extensions/src/surfaces/admin/components/PrintActionExtensionComponents.ts +++ b/packages/ui-extensions/src/surfaces/admin/components/PrintActionExtensionComponents.ts @@ -1,5 +1,8 @@ import {StandardComponents} from './StandardComponents'; +/** + * The components available for building print action extensions. Includes all standard components plus the `AdminPrintAction` component required for print action setup. + */ export type PrintActionExtensionComponents = | StandardComponents | 'AdminPrintAction'; diff --git a/packages/ui-extensions/src/surfaces/admin/components/StandardComponents.ts b/packages/ui-extensions/src/surfaces/admin/components/StandardComponents.ts index c20a67230e..227d35842a 100644 --- a/packages/ui-extensions/src/surfaces/admin/components/StandardComponents.ts +++ b/packages/ui-extensions/src/surfaces/admin/components/StandardComponents.ts @@ -1,3 +1,6 @@ +/** + * The standard set of UI components available in most admin extensions. These components provide the building blocks for creating extension interfaces including layout elements, form inputs, data display, navigation, and interactive controls. Use these components to build consistent, accessible UIs that match the Shopify admin design system. + */ export type StandardComponents = | 'Avatar' | 'Badge' diff --git a/packages/ui-extensions/src/surfaces/admin/extension-targets.ts b/packages/ui-extensions/src/surfaces/admin/extension-targets.ts index 5dc42d6046..fddb8e1b15 100644 --- a/packages/ui-extensions/src/surfaces/admin/extension-targets.ts +++ b/packages/ui-extensions/src/surfaces/admin/extension-targets.ts @@ -23,9 +23,12 @@ import type {ActionExtensionComponents} from './components/ActionExtensionCompon import type {PrintActionExtensionComponents} from './components/PrintActionExtensionComponents'; import type {FunctionSettingsComponents} from './components/FunctionSettingsComponents'; +/** + * Maps extension target identifiers to their corresponding extension types. Each target represents a specific location or context in the Shopify admin where extensions can render or execute. Use these targets to define where your extension appears and what capabilities it has access to. + */ export interface ExtensionTargets { /** - * Renders a [`CustomerSegmentTemplate`](/docs/api/admin-extensions/components/customersegmenttemplate) in the [customer segment editor](https://help.shopify.com/en/manual/customers/customer-segmentation/customer-segments). + * A runnable target that provides [customer segment templates](/docs/apps/build/marketing-analytics/customer-segments/build-a-template-extension) in the [customer segment editor](https://help.shopify.com/manual/customers/customer-segmentation/create-customer-segments). Use this target to provide pre-built segment templates that merchants can use as starting points for creating targeted customer groups based on custom criteria. */ 'admin.customers.segmentation-templates.data': RunnableExtension< CustomerSegmentTemplateApi<'admin.customers.segmentation-templates.data'>, @@ -34,9 +37,7 @@ export interface ExtensionTargets { // Blocks /** - * Renders an admin block in the product details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the product details page. Use this to show product-specific information, tools, or actions directly on the product page. */ 'admin.product-details.block.render': RenderExtension< BlockExtensionApi<'admin.product-details.block.render'>, @@ -44,9 +45,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the order details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the order details page. Use this to show order-specific information, fulfillment tools, or custom order actions. */ 'admin.order-details.block.render': RenderExtension< BlockExtensionApi<'admin.order-details.block.render'>, @@ -54,9 +53,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the discount details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A function settings target that appears when merchants create or edit a discount powered by your discount function, allowing them to configure function-specific settings. Use this to build custom configuration interfaces for discount function parameters. */ 'admin.discount-details.function-settings.render': RenderExtension< DiscountFunctionSettingsApi<'admin.discount-details.function-settings.render'>, @@ -64,9 +61,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the customer details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the customer details page. Use this to show customer-specific information, loyalty data, or custom customer actions. */ 'admin.customer-details.block.render': RenderExtension< BlockExtensionApi<'admin.customer-details.block.render'>, @@ -74,9 +69,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the collection details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the collection details page. Use this to show collection analytics, bulk product operations, or collection-specific tools. */ 'admin.collection-details.block.render': RenderExtension< BlockExtensionApi<'admin.collection-details.block.render'>, @@ -84,9 +77,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the draft order details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the draft order details page. Use this to show custom pricing calculations, special order handling tools, or order-specific information. */ 'admin.draft-order-details.block.render': RenderExtension< BlockExtensionApi<'admin.draft-order-details.block.render'>, @@ -94,9 +85,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the abandoned checkout details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the abandoned checkout details page. Use this to show cart recovery tools, abandonment analysis, or customer re-engagement options. */ 'admin.abandoned-checkout-details.block.render': RenderExtension< BlockExtensionApi<'admin.abandoned-checkout-details.block.render'>, @@ -104,9 +93,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the catalog details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the catalog details page. Use this to show catalog-specific settings, market information, or synchronization tools. */ 'admin.catalog-details.block.render': RenderExtension< BlockExtensionApi<'admin.catalog-details.block.render'>, @@ -114,9 +101,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the company details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the company details page. Use this to show B2B customer information, credit limits, or company-specific data. */ 'admin.company-details.block.render': RenderExtension< BlockExtensionApi<'admin.company-details.block.render'>, @@ -124,9 +109,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the company location details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the company location details page. Use this to show location-specific information, shipping preferences, or location management tools. */ 'admin.company-location-details.block.render': RenderExtension< BlockExtensionApi<'admin.company-location-details.block.render'>, @@ -134,9 +117,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the gift card details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the gift card details page. Use this to show gift card balance tracking, usage history, or custom gift card metadata. */ 'admin.gift-card-details.block.render': RenderExtension< BlockExtensionApi<'admin.gift-card-details.block.render'>, @@ -144,9 +125,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the product variant details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the product variant details page. Use this to show variant-specific data, inventory tools, or variant configuration options. */ 'admin.product-variant-details.block.render': RenderExtension< BlockExtensionApi<'admin.product-variant-details.block.render'>, @@ -154,9 +133,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin block in the product details page. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A block target that displays inline content within the product details page. Use this to show product-specific tools or reorderable content sections. */ 'admin.product-details.reorder.render': RenderExtension< BlockExtensionApi<'admin.product-details.reorder.render'>, @@ -165,9 +142,7 @@ export interface ExtensionTargets { // Actions /** - * Renders an admin action extension in the product details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the product details page. Use this to create workflows for processing products, syncing data, or integrating with external systems. */ 'admin.product-details.action.render': RenderExtension< ActionExtensionApi<'admin.product-details.action.render'>, @@ -175,9 +150,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the catalog details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the catalog details page. Use this to create workflows for catalog management, market synchronization, or data exports. */ 'admin.catalog-details.action.render': RenderExtension< ActionExtensionApi<'admin.catalog-details.action.render'>, @@ -185,9 +158,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the company details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the company details page. Use this to create workflows for B2B customer management, credit operations, or company data synchronization. */ 'admin.company-details.action.render': RenderExtension< ActionExtensionApi<'admin.company-details.action.render'>, @@ -195,9 +166,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the gift card details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the gift card details page. Use this to create workflows for gift card processing, balance adjustments, or custom gift card operations. */ 'admin.gift-card-details.action.render': RenderExtension< ActionExtensionApi<'admin.gift-card-details.action.render'>, @@ -205,9 +174,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the order details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the order details page. Use this to create workflows for order processing, fulfillment operations, or external system integrations. */ 'admin.order-details.action.render': RenderExtension< ActionExtensionApi<'admin.order-details.action.render'>, @@ -215,9 +182,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the customer details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the customer details page. Use this to create workflows for customer data management, loyalty operations, or CRM integrations. */ 'admin.customer-details.action.render': RenderExtension< ActionExtensionApi<'admin.customer-details.action.render'>, @@ -225,9 +190,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the customer segment details page. Open this extension from the "Use segment" button. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears from the **Use segment** button on the customer segment details page. Use this to create workflows for marketing campaigns, email operations, or segment-based actions. */ 'admin.customer-segment-details.action.render': RenderExtension< ActionExtensionApi<'admin.customer-segment-details.action.render'>, @@ -235,9 +198,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the product index page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the product index page. Use this to create workflows for product management, catalog operations, or inventory synchronization. */ 'admin.product-index.action.render': RenderExtension< ActionExtensionApi<'admin.product-index.action.render'>, @@ -245,9 +206,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the order index page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the order index page. Use this to create workflows for order management, reporting, or fulfillment operations. */ 'admin.order-index.action.render': RenderExtension< ActionExtensionApi<'admin.order-index.action.render'>, @@ -255,9 +214,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the customer index page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the customer index page. Use this to create workflows for customer management, marketing operations, or bulk data processing. */ 'admin.customer-index.action.render': RenderExtension< ActionExtensionApi<'admin.customer-index.action.render'>, @@ -265,9 +222,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the discount index page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the discount index page. Use this to create workflows for discount management, promotional operations, or bulk discount updates. */ 'admin.discount-index.action.render': RenderExtension< ActionExtensionApi<'admin.discount-index.action.render'>, @@ -275,9 +230,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the collection details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the collection details page. Use this to create workflows for collection management, product operations, or merchandising tools. */ 'admin.collection-details.action.render': RenderExtension< ActionExtensionApi<'admin.collection-details.action.render'>, @@ -285,9 +238,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the collection index page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the collection index page. Use this to create workflows for collection management, bulk operations, or catalog organization. */ 'admin.collection-index.action.render': RenderExtension< ActionExtensionApi<'admin.collection-index.action.render'>, @@ -295,9 +246,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the abandoned checkout page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the abandoned checkout details page. Use this to create workflows for cart recovery, customer engagement, or checkout analysis. */ 'admin.abandoned-checkout-details.action.render': RenderExtension< ActionExtensionApi<'admin.abandoned-checkout-details.action.render'>, @@ -305,9 +254,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the product variant details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the product variant details page. Use this to create workflows for variant management, inventory operations, or data synchronization. */ 'admin.product-variant-details.action.render': RenderExtension< ActionExtensionApi<'admin.product-variant-details.action.render'>, @@ -315,9 +262,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the draft order details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the draft order details page. Use this to create workflows for draft order processing, custom pricing, or order preparation tools. */ 'admin.draft-order-details.action.render': RenderExtension< ActionExtensionApi<'admin.draft-order-details.action.render'>, @@ -325,9 +270,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the draft orders page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the draft order index page. Use this to create workflows for draft order management, bulk operations, or order conversion tools. */ 'admin.draft-order-index.action.render': RenderExtension< ActionExtensionApi<'admin.draft-order-index.action.render'>, @@ -335,9 +278,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the discount details page. Open this extension from the "More Actions" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the discount details page. Use this to create workflows for discount management, promotion analysis, or discount synchronization. */ 'admin.discount-details.action.render': RenderExtension< ActionExtensionApi<'admin.discount-details.action.render'>, @@ -345,10 +286,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the order fulfilled card. Open this extension from the "3-dot" menu inside the order fulfilled card. - * Note: This extension will only be visible on orders which were fulfilled by your app. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the actions menu inside the order fulfilled card, visible only on orders fulfilled by your app. Use this to create workflows for fulfillment operations, tracking updates, or post-fulfillment actions. */ 'admin.order-fulfilled-card.action.render': RenderExtension< ActionExtensionApi<'admin.order-fulfilled-card.action.render'>, @@ -358,9 +296,7 @@ export interface ExtensionTargets { // Bulk Actions /** - * Renders an admin action extension in the product index page when multiple resources are selected. Open this extension from the "More Actions" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the product index page when multiple products are selected. Use this to create workflows for bulk product operations, batch updates, or mass data processing. */ 'admin.product-index.selection-action.render': RenderExtension< ActionExtensionApi<'admin.product-index.selection-action.render'>, @@ -368,9 +304,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the order index page when multiple resources are selected. Open this extension from the "More Actions" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the order index page when multiple orders are selected. Use this to create workflows for bulk order operations, batch fulfillment, or mass order processing. */ 'admin.order-index.selection-action.render': RenderExtension< ActionExtensionApi<'admin.order-index.selection-action.render'>, @@ -378,9 +312,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the customer index page when multiple resources are selected. Open this extension from the "More Actions" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the customer index page when multiple customers are selected. Use this to create workflows for bulk customer operations, mass email campaigns, or batch data updates. */ 'admin.customer-index.selection-action.render': RenderExtension< ActionExtensionApi<'admin.customer-index.selection-action.render'>, @@ -388,9 +320,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the draft order page when multiple resources are selected. Open this extension from the "3-dot" menu. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **More actions** menu on the draft order index page when multiple draft orders are selected. Use this to create workflows for bulk draft order operations, batch conversions, or mass order processing. */ 'admin.draft-order-index.selection-action.render': RenderExtension< ActionExtensionApi<'admin.draft-order-index.selection-action.render'>, @@ -398,9 +328,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the product details page when a selling plan group is present. Open this extension from the "Purchase Options card". - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **Purchase Options** card on the product details page when a selling plan group is present. Use this to create workflows for subscription management, selling plan configuration, or purchase option operations. */ 'admin.product-purchase-option.action.render': RenderExtension< PurchaseOptionsCardConfigurationApi<'admin.product-purchase-option.action.render'>, @@ -408,9 +336,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin action extension in the product variant details page when a selling plan group is present. Open this extension from the "Purchase Options card". - * - * See the [list of available components](/docs/api/admin-extensions/components). + * An action target that appears in the **Purchase Options** card on the product variant details page when a selling plan group is present. Use this to create workflows for variant-specific subscription management, selling plan configuration, or purchase option operations. */ 'admin.product-variant-purchase-option.action.render': RenderExtension< PurchaseOptionsCardConfigurationApi<'admin.product-variant-purchase-option.action.render'>, @@ -420,9 +346,7 @@ export interface ExtensionTargets { // Print actions and bulk print actions /** - * Renders an admin print action extension in the order index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A print action target that appears in the **Print** menu on the order details page. Use this to generate custom documents such as packing slips, shipping labels, or invoices. */ 'admin.order-details.print-action.render': RenderExtension< PrintActionExtensionApi<'admin.order-details.print-action.render'>, @@ -430,9 +354,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin print action extension in the product index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A print action target that appears in the **Print** menu on the product details page. Use this to generate custom documents such as product labels, barcode sheets, or specification sheets. */ 'admin.product-details.print-action.render': RenderExtension< PrintActionExtensionApi<'admin.product-details.print-action.render'>, @@ -440,9 +362,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin print action extension in the order index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A print action target that appears in the **Print** menu on the order index page when multiple orders are selected. Use this to generate batch documents such as combined packing slips, shipping manifests, or bulk invoices. */ 'admin.order-index.selection-print-action.render': RenderExtension< PrintActionExtensionApi<'admin.order-index.selection-print-action.render'>, @@ -450,9 +370,7 @@ export interface ExtensionTargets { >; /** - * Renders an admin print action extension in the product index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A print action target that appears in the **Print** menu on the product index page when multiple products are selected. Use this to generate batch documents such as combined product labels, barcode sheets, or catalog pages. */ 'admin.product-index.selection-print-action.render': RenderExtension< PrintActionExtensionApi<'admin.product-index.selection-print-action.render'>, @@ -462,9 +380,7 @@ export interface ExtensionTargets { // Other /** - * Renders Product Configuration on product details and product variant details - * - * See the [tutorial](/docs/apps/selling-strategies/bundles/product-config) for more information + * A configuration target that renders product configuration settings for [product bundles](/docs/apps/build/product-merchandising/bundles/product-configuration-extension/add-merchant-config-ui) and customizable products on the product details page. Use this to define bundle component selections, customization options, or product configuration rules. */ 'admin.product-details.configuration.render': RenderExtension< ProductDetailsConfigurationApi<'admin.product-details.configuration.render'>, @@ -472,9 +388,7 @@ export interface ExtensionTargets { >; /** - * Renders Product Configuration on product details and product variant details - * - * See the [tutorial](/docs/apps/selling-strategies/bundles/product-config) for more information + * A configuration target that renders product variant configuration settings for [product bundles](/docs/apps/build/product-merchandising/bundles/product-configuration-extension/add-merchant-config-ui) and customizable products on the product variant details page. Use this to define variant-specific bundle components, customization options, or configuration rules. */ 'admin.product-variant-details.configuration.render': RenderExtension< ProductVariantDetailsConfigurationApi<'admin.product-variant-details.configuration.render'>, @@ -482,23 +396,23 @@ export interface ExtensionTargets { >; /** - * Renders Order Routing Rule Configuration on order routing settings. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A function settings target that renders within order routing settings, allowing merchants to configure order routing rule functions. + * @private */ 'admin.settings.internal-order-routing-rule.render': RenderExtension< OrderRoutingRuleApi<'admin.settings.internal-order-routing-rule.render'>, FunctionSettingsComponents >; + /** + * A function settings target that renders within order routing settings, allowing merchants to configure order routing rule functions. Use this to build custom configuration interfaces for order routing function parameters. + */ 'admin.settings.order-routing-rule.render': RenderExtension< OrderRoutingRuleApi<'admin.settings.order-routing-rule.render'>, FunctionSettingsComponents >; /** - * Renders Validation Settings within a given validation's add and edit views. - * - * See the [list of available components](/docs/api/admin-extensions/components). + * A function settings target that renders within a validation's add and edit views, allowing merchants to configure validation function settings. Use this to build custom configuration interfaces for validation function parameters and rules. */ 'admin.settings.validation.render': RenderExtension< ValidationSettingsApi<'admin.settings.validation.render'>, @@ -507,7 +421,7 @@ export interface ExtensionTargets { // Admin action shouldRender targets /** - * Controls the render state of an admin action extension in the product details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the product details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on product properties, user permissions, or external data. */ 'admin.product-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.product-details.action.should-render'>, @@ -515,7 +429,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the catalog details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the catalog details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on catalog properties, user permissions, or external data. */ 'admin.catalog-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.catalog-details.action.should-render'>, @@ -523,7 +437,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the company details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the company details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on company properties, user permissions, or external data. */ 'admin.company-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.company-details.action.should-render'>, @@ -531,7 +445,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the gift card details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the gift card details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on gift card properties, user permissions, or external data. */ 'admin.gift-card-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.gift-card-details.action.should-render'>, @@ -539,7 +453,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the order details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the order details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on order properties, fulfillment status, or external data. */ 'admin.order-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.order-details.action.should-render'>, @@ -547,7 +461,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the customer details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the customer details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on customer properties, user permissions, or external data. */ 'admin.customer-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.customer-details.action.should-render'>, @@ -555,7 +469,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the customer segment details page. Open this extension from the "Use segment" button. + * A non-rendering target that controls whether the customer segment details action appears from the **Use segment** button. Use this to conditionally show or hide your action based on segment properties, user permissions, or external data. */ 'admin.customer-segment-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.customer-segment-details.action.should-render'>, @@ -563,7 +477,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the product index page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the product index action appears in the **More actions** menu. Use this to conditionally show or hide your action based on user permissions, store configuration, or external data. */ 'admin.product-index.action.should-render': RunnableExtension< ShouldRenderApi<'admin.product-index.action.should-render'>, @@ -571,7 +485,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the order index page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the order index action appears in the **More actions** menu. Use this to conditionally show or hide your action based on user permissions, store configuration, or external data. */ 'admin.order-index.action.should-render': RunnableExtension< ShouldRenderApi<'admin.order-index.action.should-render'>, @@ -579,7 +493,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the customer index page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the customer index action appears in the **More actions** menu. Use this to conditionally show or hide your action based on user permissions, store configuration, or external data. */ 'admin.customer-index.action.should-render': RunnableExtension< ShouldRenderApi<'admin.customer-index.action.should-render'>, @@ -587,7 +501,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the discount index page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the discount index action appears in the **More actions** menu. Use this to conditionally show or hide your action based on user permissions, store configuration, or external data. */ 'admin.discount-index.action.should-render': RunnableExtension< ShouldRenderApi<'admin.discount-index.action.should-render'>, @@ -595,7 +509,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the collection details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the collection details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on collection properties, user permissions, or external data. */ 'admin.collection-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.collection-details.action.should-render'>, @@ -603,7 +517,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the collection index page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the collection index action appears in the **More actions** menu. Use this to conditionally show or hide your action based on user permissions, store configuration, or external data. */ 'admin.collection-index.action.should-render': RunnableExtension< ShouldRenderApi<'admin.collection-index.action.should-render'>, @@ -611,7 +525,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the abandoned checkout page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the abandoned checkout details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on checkout properties, user permissions, or external data. */ 'admin.abandoned-checkout-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.abandoned-checkout-details.action.should-render'>, @@ -619,7 +533,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the product variant details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the product variant details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on variant properties, user permissions, or external data. */ 'admin.product-variant-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.product-variant-details.action.should-render'>, @@ -627,7 +541,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the draft order details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the draft order details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on draft order properties, user permissions, or external data. */ 'admin.draft-order-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.draft-order-details.action.should-render'>, @@ -635,7 +549,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the draft orders page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the draft order index action appears in the **More actions** menu. Use this to conditionally show or hide your action based on user permissions, store configuration, or external data. */ 'admin.draft-order-index.action.should-render': RunnableExtension< ShouldRenderApi<'admin.draft-order-index.action.should-render'>, @@ -643,7 +557,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the discount details page. Open this extension from the "More Actions" menu. + * A non-rendering target that controls whether the discount details action appears in the **More actions** menu. Use this to conditionally show or hide your action based on discount properties, user permissions, or external data. */ 'admin.discount-details.action.should-render': RunnableExtension< ShouldRenderApi<'admin.discount-details.action.should-render'>, @@ -651,8 +565,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the order fulfilled card. Open this extension from the "3-dot" menu inside the order fulfilled card. - * Note: This extension will only be visible on orders which were fulfilled by your app. + * A non-rendering target that controls whether the order fulfilled card action appears in the actions menu. Use this to conditionally show or hide your action based on fulfillment properties, user permissions, or external data. */ 'admin.order-fulfilled-card.action.should-render': RunnableExtension< ShouldRenderApi<'admin.order-fulfilled-card.action.should-render'>, @@ -662,7 +575,7 @@ export interface ExtensionTargets { // Admin bulk action shouldRender targets /** - * Controls the render state of an admin action extension in the product index page when multiple resources are selected. Open this extension from the "More Actions" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the product index selection action appears in the **More actions** menu. Use this to conditionally show or hide your bulk action based on selection criteria, user permissions, or external data. */ 'admin.product-index.selection-action.should-render': RunnableExtension< ShouldRenderApi<'admin.product-index.selection-action.should-render'>, @@ -670,7 +583,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the order index page when multiple resources are selected. Open this extension from the "More Actions" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the order index selection action appears in the **More actions** menu. Use this to conditionally show or hide your bulk action based on selection criteria, user permissions, or external data. */ 'admin.order-index.selection-action.should-render': RunnableExtension< ShouldRenderApi<'admin.order-index.selection-action.should-render'>, @@ -678,7 +591,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the customer index page when multiple resources are selected. Open this extension from the "More Actions" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the customer index selection action appears in the **More actions** menu. Use this to conditionally show or hide your bulk action based on selection criteria, user permissions, or external data. */ 'admin.customer-index.selection-action.should-render': RunnableExtension< ShouldRenderApi<'admin.customer-index.selection-action.should-render'>, @@ -686,7 +599,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin action extension in the draft order page when multiple resources are selected. Open this extension from the "3-dot" menu. + * A non-rendering target that controls whether the draft order index selection action appears in the **More actions** menu. Use this to conditionally show or hide your bulk action based on selection criteria, user permissions, or external data. */ 'admin.draft-order-index.selection-action.should-render': RunnableExtension< ShouldRenderApi<'admin.draft-order-index.selection-action.should-render'>, @@ -696,7 +609,7 @@ export interface ExtensionTargets { // Admin print action and bulk print action shouldRender targets /** - * Controls the render state of an admin print action extension in the order index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the order details print action appears in the **Print** menu. Use this to conditionally show or hide your print action based on order properties, user permissions, or external data. */ 'admin.order-details.print-action.should-render': RunnableExtension< ShouldRenderApi<'admin.order-details.print-action.should-render'>, @@ -704,7 +617,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin print action extension in the product index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the product details print action appears in the **Print** menu. Use this to conditionally show or hide your print action based on product properties, user permissions, or external data. */ 'admin.product-details.print-action.should-render': RunnableExtension< ShouldRenderApi<'admin.product-details.print-action.should-render'>, @@ -712,7 +625,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin print action extension in the order index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the order index selection print action appears in the **Print** menu. Use this to conditionally show or hide your bulk print action based on selection criteria, user permissions, or external data. */ 'admin.order-index.selection-print-action.should-render': RunnableExtension< ShouldRenderApi<'admin.order-index.selection-print-action.should-render'>, @@ -720,7 +633,7 @@ export interface ExtensionTargets { >; /** - * Controls the render state of an admin print action extension in the product index page when multiple resources are selected. Open this extension from the "Print" menu of the resource list. The resource ids are available to this extension at runtime. + * A non-rendering target that controls whether the product index selection print action appears in the **Print** menu. Use this to conditionally show or hide your bulk print action based on selection criteria, user permissions, or external data. */ 'admin.product-index.selection-print-action.should-render': RunnableExtension< ShouldRenderApi<'admin.product-index.selection-print-action.should-render'>, @@ -728,7 +641,7 @@ export interface ExtensionTargets { >; /** - * Provides tools data for the admin tools functionality. + * A runnable target that enables your app to expose data to [Sidekick](/docs/apps/build/sidekick/build-app-data). Use this target to register tools that Sidekick can invoke to search your app's data and answer merchant questions. */ 'admin.app.tools.data': RunnableExtension< StandardApi<'admin.app.tools.data'>, @@ -736,4 +649,7 @@ export interface ExtensionTargets { >; } +/** + * A string literal union of all valid extension target identifiers. Use this type to specify where your Admin UI extension should render, such as `admin.product-details.block.render` for a block on product details pages or `admin.order-details.action.render` for an action on order details pages. The target determines the extension's location, available APIs, and UI components. + */ export type ExtensionTarget = keyof ExtensionTargets; diff --git a/packages/ui-extensions/src/surfaces/checkout/api/address-autocomplete/standard.ts b/packages/ui-extensions/src/surfaces/checkout/api/address-autocomplete/standard.ts index a627d211a2..08d2a89262 100644 --- a/packages/ui-extensions/src/surfaces/checkout/api/address-autocomplete/standard.ts +++ b/packages/ui-extensions/src/surfaces/checkout/api/address-autocomplete/standard.ts @@ -102,6 +102,11 @@ export interface AddressAutocompleteStandardApi< * * Once the order is created, you can query these metafields using the * [GraphQL Admin API](https://shopify.dev/docs/admin-api/graphql/reference/orders/order#metafield-2021-01) + * + * > Caution: + * `metafields` is deprecated. Use `appMetafields` with cart metafields instead. + * + * @deprecated Use `appMetafields` with cart metafields instead. */ metafields: Metafield[]; diff --git a/packages/ui-extensions/src/surfaces/checkout/api/checkout/checkout.ts b/packages/ui-extensions/src/surfaces/checkout/api/checkout/checkout.ts index 5470dce200..b5d675f7df 100644 --- a/packages/ui-extensions/src/surfaces/checkout/api/checkout/checkout.ts +++ b/packages/ui-extensions/src/surfaces/checkout/api/checkout/checkout.ts @@ -352,7 +352,11 @@ export interface GiftCardChangeResultError { message: string; } -/** Removes a metafield. */ +/** + * Removes a metafield. This change type is deprecated and will be removed in a future API version. Use `MetafieldRemoveCartChange` instead. + * + * @deprecated - Consumers should use cart metafields instead. + */ export interface MetafieldRemoveChange { /** * The type of the `MetafieldRemoveChange` API. @@ -391,6 +395,9 @@ export interface MetafieldRemoveCartChange { /** * Updates a metafield. If a metafield with the * provided key and namespace does not already exist, it gets created. + * This change type is deprecated and will be removed in a future API version. Use `MetafieldUpdateCartChange` instead. + * + * @deprecated - Consumers should use cart metafields instead. */ export interface MetafieldUpdateChange { /** @@ -544,6 +551,8 @@ export interface CheckoutApi { * through the [`attributes`](https://shopify.dev/docs/api/checkout-ui-extensions/apis/attributes#standardapi-propertydetail-attributes) property. * * > Note: This method will return an error if the [cart instruction](https://shopify.dev/docs/api/checkout-ui-extensions/apis/cart-instructions#standardapi-propertydetail-instructions) `attributes.canUpdateAttributes` is false, or the buyer is using an accelerated checkout method, such as Apple Pay or Google Pay. + * + * @deprecated - Consumers should use cart metafields instead. */ applyAttributeChange(change: AttributeChange): Promise; @@ -589,6 +598,10 @@ export interface CheckoutApi { * successful, this mutation results in an update to the value retrieved * through the [`metafields`](https://shopify.dev/docs/api/checkout-ui-extensions/apis/metafields#standardapi-propertydetail-metafields) property. * + * Cart metafields will be copied to order metafields at order creation time if there is a matching order metafield definition with the [`cart to order copyable`](https://shopify.dev/docs/apps/build/metafields/use-metafield-capabilities#cart-to-order-copyable) capability enabled. + * + * > Caution: `MetafieldRemoveChange` and `MetafieldUpdateChange` are deprecated. Use cart metafields with `MetafieldRemoveCartChange` and `MetafieldUpdateCartChange` instead. If `MetafieldUpdateChange` writes a metafield with the same namespace and key as a cart metafield that’s configured to copy, the cart metafield won’t be copied. + * * > Note: This method will return an error if the [cart instruction](https://shopify.dev/docs/api/checkout-ui-extensions/apis/cart-instructions#standardapi-propertydetail-instructions) `metafields.canSetCartMetafields` is false, or the buyer is using an accelerated checkout method, such as Apple Pay or Google Pay. */ applyMetafieldChange(change: MetafieldChange): Promise; diff --git a/packages/ui-extensions/src/surfaces/checkout/api/standard/standard.ts b/packages/ui-extensions/src/surfaces/checkout/api/standard/standard.ts index 9a9493756d..558bfe90f3 100644 --- a/packages/ui-extensions/src/surfaces/checkout/api/standard/standard.ts +++ b/packages/ui-extensions/src/surfaces/checkout/api/standard/standard.ts @@ -641,8 +641,10 @@ export interface StandardApi { * Once the order is created, you can query these metafields using the * [GraphQL Admin API](https://shopify.dev/docs/admin-api/graphql/reference/orders/order#metafield-2021-01) * - * > Tip: - * > Cart metafields are only available on carts created via the Storefront API version `2023-04` or later. + * > Caution: + * `metafields` is deprecated. Use `appMetafields` with cart metafields instead. + * + * @deprecated Use `appMetafields` with cart metafields instead. */ metafields: SubscribableSignalLike; diff --git a/packages/ui-extensions/src/surfaces/checkout/preact/metafield.ts b/packages/ui-extensions/src/surfaces/checkout/preact/metafield.ts index 5839fe7972..9c24d36b02 100644 --- a/packages/ui-extensions/src/surfaces/checkout/preact/metafield.ts +++ b/packages/ui-extensions/src/surfaces/checkout/preact/metafield.ts @@ -11,6 +11,7 @@ interface MetafieldFilter { /** * Returns a single filtered `Metafield` or `undefined`. * @arg {MetafieldFilter} - filter the list of returned metafields to a single metafield + * @deprecated `useMetafield` is deprecated. Use `useAppMetafields` with cart metafields instead. */ export function useMetafield(filters: MetafieldFilter): Metafield | undefined { const {namespace, key} = filters; diff --git a/packages/ui-extensions/src/surfaces/checkout/preact/metafields.ts b/packages/ui-extensions/src/surfaces/checkout/preact/metafields.ts index cacb399560..d3fb3aa7ae 100644 --- a/packages/ui-extensions/src/surfaces/checkout/preact/metafields.ts +++ b/packages/ui-extensions/src/surfaces/checkout/preact/metafields.ts @@ -20,6 +20,7 @@ interface MetafieldsFilters { * Returns the current array of `metafields` applied to the checkout. * You can optionally filter the list. * @arg {MetafieldsFilters} - filter the list of returned metafields + * @deprecated `useMetafields` is deprecated. Use `useAppMetafields` with cart metafields instead. */ export function useMetafields< Target extends RenderExtensionTarget = RenderExtensionTarget, diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/api/pin-pad-api.ts b/packages/ui-extensions/src/surfaces/point-of-sale/api/pin-pad-api.ts index 66f2bbac53..457a80571d 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/api/pin-pad-api.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/api/pin-pad-api.ts @@ -4,7 +4,7 @@ export interface PinPadApiContent { /** * Shows a PIN pad to the user in a modal dialog. The `onSubmit` function is called when the PIN is submitted and should validate the PIN, returning `'accept'` or `'reject'`. * - * • **When accepted**: Modal dismisses and triggers the `onDismissed` callback—perform any post-validation navigation in this callback rather than in `onSubmit`. + * • **When accepted**: The modal dismisses and triggers the `onDismissed` callback—perform any post-validation navigation in this callback rather than in `onSubmit`. * * • **When rejected**: Displays the optional `errorMessage` and keeps the modal open. * diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/components.d.ts b/packages/ui-extensions/src/surfaces/point-of-sale/components.d.ts index 875369a4c3..9180958e2e 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/components.d.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/components.d.ts @@ -15,38 +15,38 @@ export type ComponentChildren = any; export type StringChildren = string; export interface GlobalProps { - /** - * A unique identifier for the element. - */ - id?: string; + /** + * A unique identifier for the element. + */ + id?: string; } export interface ActionSlots { - /** - * The primary action to perform, provided as a button or link type element. - */ - primaryAction?: ComponentChildren; - /** - * The secondary actions to perform, provided as button or link type elements. - */ - secondaryActions?: ComponentChildren; + /** + * The primary action to perform, provided as a button or link type element. + */ + primaryAction?: ComponentChildren; + /** + * The secondary actions to perform, provided as button or link type elements. + */ + secondaryActions?: ComponentChildren; } export interface BaseOverlayProps { - /** - * Callback fired after the overlay is shown. - */ - onShow?: (event: Event) => void; - /** - * Callback fired when the overlay is shown **after** any animations to show the overlay have finished. - */ - onAfterShow?: (event: Event) => void; - /** - * Callback fired after the overlay is hidden. - */ - onHide?: (event: Event) => void; - /** - * Callback fired when the overlay is hidden **after** any animations to hide the overlay have finished. - */ - onAfterHide?: (event: Event) => void; + /** + * Callback fired after the overlay is shown. + */ + onShow?: (event: Event) => void; + /** + * Callback fired when the overlay is shown **after** any animations to show the overlay have finished. + */ + onAfterShow?: (event: Event) => void; + /** + * Callback fired after the overlay is hidden. + */ + onHide?: (event: Event) => void; + /** + * Callback fired when the overlay is hidden **after** any animations to hide the overlay have finished. + */ + onAfterHide?: (event: Event) => void; } /** * Shared interfaces for web component methods. @@ -57,49 +57,62 @@ export interface BaseOverlayProps { * - Consumers expect these methods to be consistently available on all instances */ export interface BaseOverlayMethods { - /** - * Method to show an overlay. - * - * @implementation This is a method to be called on the element and not a callback and should hence be camelCase - */ - showOverlay: () => void; - /** - * Method to hide an overlay. - * - * @implementation This is a method to be called on the element and not a callback and should hence be camelCase - */ - hideOverlay: () => void; - /** - * Method to toggle the visiblity of an overlay. - * - * @implementation This is a method to be called on the element and not a callback and should hence be camelCase - */ - toggleOverlay: () => void; + /** + * Method to show an overlay. + * + * @implementation This is a method to be called on the element and not a callback and should hence be camelCase + */ + showOverlay: () => void; + /** + * Method to hide an overlay. + * + * @implementation This is a method to be called on the element and not a callback and should hence be camelCase + */ + hideOverlay: () => void; + /** + * Method to toggle the visiblity of an overlay. + * + * @implementation This is a method to be called on the element and not a callback and should hence be camelCase + */ + toggleOverlay: () => void; } export interface FocusEventProps { - /** - * Callback when the element loses focus. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event - */ - onBlur?: (event: FocusEvent) => void; - /** - * Callback when the element receives focus. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event - */ - onFocus?: (event: FocusEvent) => void; -} -export type SizeKeyword = "small-500" | "small-400" | "small-300" | "small-200" | "small-100" | "small" | "base" | "large" | "large-100" | "large-200" | "large-300" | "large-400" | "large-500"; -export type ColorKeyword = "subdued" | "base" | "strong"; -export type BackgroundColorKeyword = "transparent" | ColorKeyword; + /** + * Callback when the element loses focus. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event + */ + onBlur?: (event: FocusEvent) => void; + /** + * Callback when the element receives focus. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event + */ + onFocus?: (event: FocusEvent) => void; +} +export type SizeKeyword = + | 'small-500' + | 'small-400' + | 'small-300' + | 'small-200' + | 'small-100' + | 'small' + | 'base' + | 'large' + | 'large-100' + | 'large-200' + | 'large-300' + | 'large-400' + | 'large-500'; +export type ColorKeyword = 'subdued' | 'base' | 'strong'; +export type BackgroundColorKeyword = 'transparent' | ColorKeyword; export interface BackgroundProps { - /** - * Adjust the background of the element. - * - * @default 'transparent' - */ - background?: BackgroundColorKeyword; + /** + * Adjust the background of the element. + * + * @default 'transparent' + */ + background?: BackgroundColorKeyword; } /** * Tone is a property for defining the color treatment of a component. @@ -111,574 +124,587 @@ export interface BackgroundProps { * * @default 'auto' */ -export type ToneKeyword = "auto" | "neutral" | "info" | "success" | "caution" | "warning" | "critical" | "accent" | "custom"; +export type ToneKeyword = + | 'auto' + | 'neutral' + | 'info' + | 'success' + | 'caution' + | 'warning' + | 'critical' + | 'accent' + | 'custom'; declare const privateIconArray: readonly [ - "adjust", - "affiliate", - "airplane", - "alert-bubble", - "alert-circle", - "alert-diamond", - "alert-location", - "alert-octagon", - "alert-octagon-filled", - "alert-triangle", - "alert-triangle-filled", - "align-horizontal-centers", - "app-extension", - "apps", - "archive", - "arrow-down", - "arrow-down-circle", - "arrow-down-right", - "arrow-left", - "arrow-left-circle", - "arrow-right", - "arrow-right-circle", - "arrow-up", - "arrow-up-circle", - "arrow-up-right", - "arrows-in-horizontal", - "arrows-out-horizontal", - "asterisk", - "attachment", - "automation", - "backspace", - "bag", - "bank", - "barcode", - "battery-low", - "bill", - "blank", - "blog", - "bolt", - "bolt-filled", - "book", - "book-open", - "bug", - "bullet", - "business-entity", - "button", - "button-press", - "calculator", - "calendar", - "calendar-check", - "calendar-compare", - "calendar-list", - "calendar-time", - "camera", - "camera-flip", - "caret-down", - "caret-left", - "caret-right", - "caret-up", - "cart", - "cart-abandoned", - "cart-discount", - "cart-down", - "cart-filled", - "cart-sale", - "cart-send", - "cart-up", - "cash-dollar", - "cash-euro", - "cash-pound", - "cash-rupee", - "cash-yen", - "catalog-product", - "categories", - "channels", - "chart-cohort", - "chart-donut", - "chart-funnel", - "chart-histogram-first", - "chart-histogram-first-last", - "chart-histogram-flat", - "chart-histogram-full", - "chart-histogram-growth", - "chart-histogram-last", - "chart-histogram-second-last", - "chart-horizontal", - "chart-line", - "chart-popular", - "chart-stacked", - "chart-vertical", - "chat", - "chat-new", - "chat-referral", - "check", - "check-circle", - "check-circle-filled", - "checkbox", - "chevron-down", - "chevron-down-circle", - "chevron-left", - "chevron-left-circle", - "chevron-right", - "chevron-right-circle", - "chevron-up", - "chevron-up-circle", - "circle", - "circle-dashed", - "clipboard", - "clipboard-check", - "clipboard-checklist", - "clock", - "clock-list", - "clock-revert", - "code", - "code-add", - "collection", - "collection-featured", - "collection-list", - "collection-reference", - "color", - "color-none", - "compass", - "complete", - "compose", - "confetti", - "connect", - "content", - "contract", - "corner-pill", - "corner-round", - "corner-square", - "credit-card", - "credit-card-cancel", - "credit-card-percent", - "credit-card-reader", - "credit-card-reader-chip", - "credit-card-reader-tap", - "credit-card-secure", - "credit-card-tap-chip", - "crop", - "currency-convert", - "cursor", - "cursor-banner", - "cursor-option", - "data-presentation", - "data-table", - "database", - "database-add", - "database-connect", - "delete", - "delivered", - "delivery", - "desktop", - "disabled", - "disabled-filled", - "discount", - "discount-add", - "discount-automatic", - "discount-code", - "discount-remove", - "dns-settings", - "dock-floating", - "dock-side", - "domain", - "domain-landing-page", - "domain-new", - "domain-redirect", - "download", - "drag-drop", - "drag-handle", - "drawer", - "duplicate", - "edit", - "email", - "email-follow-up", - "email-newsletter", - "empty", - "enabled", - "enter", - "envelope", - "envelope-soft-pack", - "eraser", - "exchange", - "exit", - "export", - "external", - "eye-check-mark", - "eye-dropper", - "eye-dropper-list", - "eye-first", - "eyeglasses", - "fav", - "favicon", - "file", - "file-list", - "filter", - "filter-active", - "flag", - "flip-horizontal", - "flip-vertical", - "flower", - "folder", - "folder-add", - "folder-down", - "folder-remove", - "folder-up", - "food", - "foreground", - "forklift", - "forms", - "games", - "gauge", - "geolocation", - "gift", - "gift-card", - "git-branch", - "git-commit", - "git-repository", - "globe", - "globe-asia", - "globe-europe", - "globe-lines", - "globe-list", - "graduation-hat", - "grid", - "hashtag", - "hashtag-decimal", - "hashtag-list", - "heart", - "hide", - "hide-filled", - "home", - "home-filled", - "icons", - "identity-card", - "image", - "image-add", - "image-alt", - "image-explore", - "image-magic", - "image-none", - "image-with-text-overlay", - "images", - "import", - "in-progress", - "incentive", - "incoming", - "incomplete", - "info", - "info-filled", - "inheritance", - "inventory", - "inventory-edit", - "inventory-list", - "inventory-transfer", - "inventory-updated", - "iq", - "key", - "keyboard", - "keyboard-filled", - "keyboard-hide", - "keypad", - "label-printer", - "language", - "language-translate", - "layout-block", - "layout-buy-button", - "layout-buy-button-horizontal", - "layout-buy-button-vertical", - "layout-column-1", - "layout-columns-2", - "layout-columns-3", - "layout-footer", - "layout-header", - "layout-logo-block", - "layout-popup", - "layout-rows-2", - "layout-section", - "layout-sidebar-left", - "layout-sidebar-right", - "lightbulb", - "link", - "link-list", - "list-bulleted", - "list-bulleted-filled", - "list-numbered", - "live", - "live-critical", - "live-none", - "location", - "location-none", - "lock", - "map", - "markets", - "markets-euro", - "markets-rupee", - "markets-yen", - "maximize", - "measurement-size", - "measurement-size-list", - "measurement-volume", - "measurement-volume-list", - "measurement-weight", - "measurement-weight-list", - "media-receiver", - "megaphone", - "mention", - "menu", - "menu-filled", - "menu-horizontal", - "menu-vertical", - "merge", - "metafields", - "metaobject", - "metaobject-list", - "metaobject-reference", - "microphone", - "microphone-muted", - "minimize", - "minus", - "minus-circle", - "mobile", - "money", - "money-none", - "money-split", - "moon", - "nature", - "note", - "note-add", - "notification", - "number-one", - "order", - "order-batches", - "order-draft", - "order-filled", - "order-first", - "order-fulfilled", - "order-repeat", - "order-unfulfilled", - "orders-status", - "organization", - "outdent", - "outgoing", - "package", - "package-cancel", - "package-fulfilled", - "package-on-hold", - "package-reassign", - "package-returned", - "page", - "page-add", - "page-attachment", - "page-clock", - "page-down", - "page-heart", - "page-list", - "page-reference", - "page-remove", - "page-report", - "page-up", - "pagination-end", - "pagination-start", - "paint-brush-flat", - "paint-brush-round", - "paper-check", - "partially-complete", - "passkey", - "paste", - "pause-circle", - "payment", - "payment-capture", - "payout", - "payout-dollar", - "payout-euro", - "payout-pound", - "payout-rupee", - "payout-yen", - "person", - "person-add", - "person-exit", - "person-filled", - "person-list", - "person-lock", - "person-remove", - "person-segment", - "personalized-text", - "phablet", - "phone", - "phone-down", - "phone-down-filled", - "phone-in", - "phone-out", - "pin", - "pin-remove", - "plan", - "play", - "play-circle", - "plus", - "plus-circle", - "plus-circle-down", - "plus-circle-filled", - "plus-circle-up", - "point-of-sale", - "point-of-sale-register", - "price-list", - "print", - "product", - "product-add", - "product-cost", - "product-filled", - "product-list", - "product-reference", - "product-remove", - "product-return", - "product-unavailable", - "profile", - "profile-filled", - "question-circle", - "question-circle-filled", - "radio-control", - "receipt", - "receipt-dollar", - "receipt-euro", - "receipt-folded", - "receipt-paid", - "receipt-pound", - "receipt-refund", - "receipt-rupee", - "receipt-yen", - "receivables", - "redo", - "referral-code", - "refresh", - "remove-background", - "reorder", - "replace", - "replay", - "reset", - "return", - "reward", - "rocket", - "rotate-left", - "rotate-right", - "sandbox", - "save", - "savings", - "scan-qr-code", - "search", - "search-add", - "search-list", - "search-recent", - "search-resource", - "select", - "send", - "settings", - "share", - "shield-check-mark", - "shield-none", - "shield-pending", - "shield-person", - "shipping-label", - "shipping-label-cancel", - "shopcodes", - "slideshow", - "smiley-happy", - "smiley-joy", - "smiley-neutral", - "smiley-sad", - "social-ad", - "social-post", - "sort", - "sort-ascending", - "sort-descending", - "sound", - "split", - "sports", - "star", - "star-circle", - "star-filled", - "star-half", - "star-list", - "status", - "status-active", - "stop-circle", - "store", - "store-import", - "store-managed", - "store-online", - "sun", - "table", - "table-masonry", - "tablet", - "target", - "tax", - "team", - "text", - "text-align-center", - "text-align-left", - "text-align-right", - "text-block", - "text-bold", - "text-color", - "text-font", - "text-font-list", - "text-grammar", - "text-in-columns", - "text-in-rows", - "text-indent", - "text-indent-remove", - "text-italic", - "text-quote", - "text-title", - "text-underline", - "text-with-image", - "theme", - "theme-edit", - "theme-store", - "theme-template", - "three-d-environment", - "thumbs-down", - "thumbs-up", - "tip-jar", - "toggle-off", - "toggle-on", - "transaction", - "transaction-fee-add", - "transaction-fee-dollar", - "transaction-fee-euro", - "transaction-fee-pound", - "transaction-fee-rupee", - "transaction-fee-yen", - "transfer", - "transfer-in", - "transfer-internal", - "transfer-out", - "truck", - "undo", - "unknown-device", - "unlock", - "upload", - "variant", - "variant-list", - "video", - "video-list", - "view", - "viewport-narrow", - "viewport-short", - "viewport-tall", - "viewport-wide", - "wallet", - "wand", - "watch", - "wifi", - "work", - "work-list", - "wrench", - "x", - "x-circle", - "x-circle-filled" + 'adjust', + 'affiliate', + 'airplane', + 'alert-bubble', + 'alert-circle', + 'alert-diamond', + 'alert-location', + 'alert-octagon', + 'alert-octagon-filled', + 'alert-triangle', + 'alert-triangle-filled', + 'align-horizontal-centers', + 'app-extension', + 'apps', + 'archive', + 'arrow-down', + 'arrow-down-circle', + 'arrow-down-right', + 'arrow-left', + 'arrow-left-circle', + 'arrow-right', + 'arrow-right-circle', + 'arrow-up', + 'arrow-up-circle', + 'arrow-up-right', + 'arrows-in-horizontal', + 'arrows-out-horizontal', + 'asterisk', + 'attachment', + 'automation', + 'backspace', + 'bag', + 'bank', + 'barcode', + 'battery-low', + 'bill', + 'blank', + 'blog', + 'bolt', + 'bolt-filled', + 'book', + 'book-open', + 'bug', + 'bullet', + 'business-entity', + 'button', + 'button-press', + 'calculator', + 'calendar', + 'calendar-check', + 'calendar-compare', + 'calendar-list', + 'calendar-time', + 'camera', + 'camera-flip', + 'caret-down', + 'caret-left', + 'caret-right', + 'caret-up', + 'cart', + 'cart-abandoned', + 'cart-discount', + 'cart-down', + 'cart-filled', + 'cart-sale', + 'cart-send', + 'cart-up', + 'cash-dollar', + 'cash-euro', + 'cash-pound', + 'cash-rupee', + 'cash-yen', + 'catalog-product', + 'categories', + 'channels', + 'chart-cohort', + 'chart-donut', + 'chart-funnel', + 'chart-histogram-first', + 'chart-histogram-first-last', + 'chart-histogram-flat', + 'chart-histogram-full', + 'chart-histogram-growth', + 'chart-histogram-last', + 'chart-histogram-second-last', + 'chart-horizontal', + 'chart-line', + 'chart-popular', + 'chart-stacked', + 'chart-vertical', + 'chat', + 'chat-new', + 'chat-referral', + 'check', + 'check-circle', + 'check-circle-filled', + 'checkbox', + 'chevron-down', + 'chevron-down-circle', + 'chevron-left', + 'chevron-left-circle', + 'chevron-right', + 'chevron-right-circle', + 'chevron-up', + 'chevron-up-circle', + 'circle', + 'circle-dashed', + 'clipboard', + 'clipboard-check', + 'clipboard-checklist', + 'clock', + 'clock-list', + 'clock-revert', + 'code', + 'code-add', + 'collection', + 'collection-featured', + 'collection-list', + 'collection-reference', + 'color', + 'color-none', + 'compass', + 'complete', + 'compose', + 'confetti', + 'connect', + 'content', + 'contract', + 'corner-pill', + 'corner-round', + 'corner-square', + 'credit-card', + 'credit-card-cancel', + 'credit-card-percent', + 'credit-card-reader', + 'credit-card-reader-chip', + 'credit-card-reader-tap', + 'credit-card-secure', + 'credit-card-tap-chip', + 'crop', + 'currency-convert', + 'cursor', + 'cursor-banner', + 'cursor-option', + 'data-presentation', + 'data-table', + 'database', + 'database-add', + 'database-connect', + 'delete', + 'delivered', + 'delivery', + 'desktop', + 'disabled', + 'disabled-filled', + 'discount', + 'discount-add', + 'discount-automatic', + 'discount-code', + 'discount-remove', + 'dns-settings', + 'dock-floating', + 'dock-side', + 'domain', + 'domain-landing-page', + 'domain-new', + 'domain-redirect', + 'download', + 'drag-drop', + 'drag-handle', + 'drawer', + 'duplicate', + 'edit', + 'email', + 'email-follow-up', + 'email-newsletter', + 'empty', + 'enabled', + 'enter', + 'envelope', + 'envelope-soft-pack', + 'eraser', + 'exchange', + 'exit', + 'export', + 'external', + 'eye-check-mark', + 'eye-dropper', + 'eye-dropper-list', + 'eye-first', + 'eyeglasses', + 'fav', + 'favicon', + 'file', + 'file-list', + 'filter', + 'filter-active', + 'flag', + 'flip-horizontal', + 'flip-vertical', + 'flower', + 'folder', + 'folder-add', + 'folder-down', + 'folder-remove', + 'folder-up', + 'food', + 'foreground', + 'forklift', + 'forms', + 'games', + 'gauge', + 'geolocation', + 'gift', + 'gift-card', + 'git-branch', + 'git-commit', + 'git-repository', + 'globe', + 'globe-asia', + 'globe-europe', + 'globe-lines', + 'globe-list', + 'graduation-hat', + 'grid', + 'hashtag', + 'hashtag-decimal', + 'hashtag-list', + 'heart', + 'hide', + 'hide-filled', + 'home', + 'home-filled', + 'icons', + 'identity-card', + 'image', + 'image-add', + 'image-alt', + 'image-explore', + 'image-magic', + 'image-none', + 'image-with-text-overlay', + 'images', + 'import', + 'in-progress', + 'incentive', + 'incoming', + 'incomplete', + 'info', + 'info-filled', + 'inheritance', + 'inventory', + 'inventory-edit', + 'inventory-list', + 'inventory-transfer', + 'inventory-updated', + 'iq', + 'key', + 'keyboard', + 'keyboard-filled', + 'keyboard-hide', + 'keypad', + 'label-printer', + 'language', + 'language-translate', + 'layout-block', + 'layout-buy-button', + 'layout-buy-button-horizontal', + 'layout-buy-button-vertical', + 'layout-column-1', + 'layout-columns-2', + 'layout-columns-3', + 'layout-footer', + 'layout-header', + 'layout-logo-block', + 'layout-popup', + 'layout-rows-2', + 'layout-section', + 'layout-sidebar-left', + 'layout-sidebar-right', + 'lightbulb', + 'link', + 'link-list', + 'list-bulleted', + 'list-bulleted-filled', + 'list-numbered', + 'live', + 'live-critical', + 'live-none', + 'location', + 'location-none', + 'lock', + 'map', + 'markets', + 'markets-euro', + 'markets-rupee', + 'markets-yen', + 'maximize', + 'measurement-size', + 'measurement-size-list', + 'measurement-volume', + 'measurement-volume-list', + 'measurement-weight', + 'measurement-weight-list', + 'media-receiver', + 'megaphone', + 'mention', + 'menu', + 'menu-filled', + 'menu-horizontal', + 'menu-vertical', + 'merge', + 'metafields', + 'metaobject', + 'metaobject-list', + 'metaobject-reference', + 'microphone', + 'microphone-muted', + 'minimize', + 'minus', + 'minus-circle', + 'mobile', + 'money', + 'money-none', + 'money-split', + 'moon', + 'nature', + 'note', + 'note-add', + 'notification', + 'number-one', + 'order', + 'order-batches', + 'order-draft', + 'order-filled', + 'order-first', + 'order-fulfilled', + 'order-repeat', + 'order-unfulfilled', + 'orders-status', + 'organization', + 'outdent', + 'outgoing', + 'package', + 'package-cancel', + 'package-fulfilled', + 'package-on-hold', + 'package-reassign', + 'package-returned', + 'page', + 'page-add', + 'page-attachment', + 'page-clock', + 'page-down', + 'page-heart', + 'page-list', + 'page-reference', + 'page-remove', + 'page-report', + 'page-up', + 'pagination-end', + 'pagination-start', + 'paint-brush-flat', + 'paint-brush-round', + 'paper-check', + 'partially-complete', + 'passkey', + 'paste', + 'pause-circle', + 'payment', + 'payment-capture', + 'payout', + 'payout-dollar', + 'payout-euro', + 'payout-pound', + 'payout-rupee', + 'payout-yen', + 'person', + 'person-add', + 'person-exit', + 'person-filled', + 'person-list', + 'person-lock', + 'person-remove', + 'person-segment', + 'personalized-text', + 'phablet', + 'phone', + 'phone-down', + 'phone-down-filled', + 'phone-in', + 'phone-out', + 'pin', + 'pin-remove', + 'plan', + 'play', + 'play-circle', + 'plus', + 'plus-circle', + 'plus-circle-down', + 'plus-circle-filled', + 'plus-circle-up', + 'point-of-sale', + 'point-of-sale-register', + 'price-list', + 'print', + 'product', + 'product-add', + 'product-cost', + 'product-filled', + 'product-list', + 'product-reference', + 'product-remove', + 'product-return', + 'product-unavailable', + 'profile', + 'profile-filled', + 'question-circle', + 'question-circle-filled', + 'radio-control', + 'receipt', + 'receipt-dollar', + 'receipt-euro', + 'receipt-folded', + 'receipt-paid', + 'receipt-pound', + 'receipt-refund', + 'receipt-rupee', + 'receipt-yen', + 'receivables', + 'redo', + 'referral-code', + 'refresh', + 'remove-background', + 'reorder', + 'replace', + 'replay', + 'reset', + 'return', + 'reward', + 'rocket', + 'rotate-left', + 'rotate-right', + 'sandbox', + 'save', + 'savings', + 'scan-qr-code', + 'search', + 'search-add', + 'search-list', + 'search-recent', + 'search-resource', + 'select', + 'send', + 'settings', + 'share', + 'shield-check-mark', + 'shield-none', + 'shield-pending', + 'shield-person', + 'shipping-label', + 'shipping-label-cancel', + 'shopcodes', + 'slideshow', + 'smiley-happy', + 'smiley-joy', + 'smiley-neutral', + 'smiley-sad', + 'social-ad', + 'social-post', + 'sort', + 'sort-ascending', + 'sort-descending', + 'sound', + 'split', + 'sports', + 'star', + 'star-circle', + 'star-filled', + 'star-half', + 'star-list', + 'status', + 'status-active', + 'stop-circle', + 'store', + 'store-import', + 'store-managed', + 'store-online', + 'sun', + 'table', + 'table-masonry', + 'tablet', + 'target', + 'tax', + 'team', + 'text', + 'text-align-center', + 'text-align-left', + 'text-align-right', + 'text-block', + 'text-bold', + 'text-color', + 'text-font', + 'text-font-list', + 'text-grammar', + 'text-in-columns', + 'text-in-rows', + 'text-indent', + 'text-indent-remove', + 'text-italic', + 'text-quote', + 'text-title', + 'text-underline', + 'text-with-image', + 'theme', + 'theme-edit', + 'theme-store', + 'theme-template', + 'three-d-environment', + 'thumbs-down', + 'thumbs-up', + 'tip-jar', + 'toggle-off', + 'toggle-on', + 'transaction', + 'transaction-fee-add', + 'transaction-fee-dollar', + 'transaction-fee-euro', + 'transaction-fee-pound', + 'transaction-fee-rupee', + 'transaction-fee-yen', + 'transfer', + 'transfer-in', + 'transfer-internal', + 'transfer-out', + 'truck', + 'undo', + 'unknown-device', + 'unlock', + 'upload', + 'variant', + 'variant-list', + 'video', + 'video-list', + 'view', + 'viewport-narrow', + 'viewport-short', + 'viewport-tall', + 'viewport-wide', + 'wallet', + 'wand', + 'watch', + 'wifi', + 'work', + 'work-list', + 'wrench', + 'x', + 'x-circle', + 'x-circle-filled', ]; export type IconType = (typeof privateIconArray)[number]; /** * Like `Extract`, but ensures that the extracted type is a strict subtype of the input type. */ export type ExtractStrict = Extract; -export type MaybeAllValuesShorthandProperty = T | `${T} ${T}` | `${T} ${T} ${T}` | `${T} ${T} ${T} ${T}`; +export type MaybeAllValuesShorthandProperty = + | T + | `${T} ${T}` + | `${T} ${T} ${T}` + | `${T} ${T} ${T} ${T}`; export type MaybeTwoValuesShorthandProperty = T | `${T} ${T}`; export type MaybeResponsive = T | `@container${string}`; /** @@ -696,1070 +722,1117 @@ export type AnyString = string & {}; * * For example in the `aspectRatio` property, `16/9` and `16 / 9` are both valid. */ -export type optionalSpace = "" | " "; +export type optionalSpace = '' | ' '; export interface BadgeProps extends GlobalProps { - /** - * The content of the Badge. - */ - children?: ComponentChildren; - /** - * Sets the tone of the Badge, based on the intention of the information being conveyed. - * - * @default 'auto' - */ - tone?: ToneKeyword; - /** - * Modify the color to be more or less intense. - * - * @default 'base' - */ - color?: ColorKeyword; - /** - * The type of icon to be displayed in the badge. - * - * @default '' - */ - icon?: IconType | AnyString; - /** - * The position of the icon in relation to the text. - */ - iconPosition?: "start" | "end"; - /** - * Adjusts the size. - * - * @default 'base' - */ - size?: SizeKeyword; + /** + * The content of the badge. + */ + children?: ComponentChildren; + /** + * Sets the tone of the badge, based on the intention of the information being conveyed. + * + * @default 'auto' + */ + tone?: ToneKeyword; + /** + * Modify the color to be more or less intense. + * + * @default 'base' + */ + color?: ColorKeyword; + /** + * The type of icon to be displayed in the badge. + * + * @default '' + */ + icon?: IconType | AnyString; + /** + * The position of the icon in relation to the text. + */ + iconPosition?: 'start' | 'end'; + /** + * Adjusts the size. + * + * @default 'base' + */ + size?: SizeKeyword; } export interface BannerProps extends GlobalProps, ActionSlots { - /** - * The title of the banner. - * - * @default '' - */ - heading?: string; - /** - * The content of the Banner. - */ - children?: ComponentChildren; - /** - * Sets the tone of the Banner, based on the intention of the information being conveyed. - * - * The banner is a live region and the type of status will be dictated by the Tone selected. - * - * - `critical` creates an [assertive live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role) that is announced by screen readers immediately. - * - `neutral`, `info`, `success`, `warning` and `caution` creates an [informative live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role) that is announced by screen readers after the current message. - * - * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions - * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role - * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role - * - * @default 'auto' - */ - tone?: ToneKeyword; - /** - * Makes the content collapsible. - * A collapsible banner will conceal child elements initially, but allow the user to expand the banner to see them. - * - * @default false - */ - collapsible?: boolean; - /** - * Determines whether the close button of the banner is present. - * - * When the close button is pressed, the `dismiss` event will fire, - * then `hidden` will be true, - * any animation will complete, - * and the `afterhide` event will fire. - * - * @default false - */ - dismissible?: boolean; - /** - * Event handler when the banner is dismissed by the user. - * - * This does not fire when setting `hidden` manually. - * - * The `hidden` property will be `false` when this event fires. - */ - onDismiss?: (event: Event) => void; - /** - * Event handler when the banner has fully hidden. - * - * The `hidden` property will be `true` when this event fires. - * - * @implementation If implementations animate the hiding of the banner, - * this event must fire after the banner has fully hidden. - * We can add an `onHide` event in future if we want to provide a hook for the start of the animation. - */ - onAfterHide?: (event: Event) => void; - /** - * Determines whether the banner is hidden. - * - * If this property is being set on each framework render (as in 'controlled' usage), - * and the banner is `dismissible`, - * ensure you update app state for this property when the `dismiss` event fires. - * - * If the banner is not `dismissible`, it can still be hidden by setting this property. - * - * @default false - */ - hidden?: boolean; + /** + * The title of the banner. + * + * @default '' + */ + heading?: string; + /** + * The content of the banner. + */ + children?: ComponentChildren; + /** + * Sets the tone of the banner, based on the intention of the information being conveyed. + * + * The banner is a live region and the type of status will be dictated by the Tone selected. + * + * - `critical` creates an [assertive live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role) that is announced by screen readers immediately. + * - `neutral`, `info`, `success`, `warning` and `caution` creates an [informative live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role) that is announced by screen readers after the current message. + * + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role + * + * @default 'auto' + */ + tone?: ToneKeyword; + /** + * Makes the content collapsible. + * A collapsible banner will conceal child elements initially, but allow the user to expand the banner to see them. + * + * @default false + */ + collapsible?: boolean; + /** + * Determines whether the close button of the banner is present. + * + * When the close button is pressed, the `dismiss` event will fire, + * then `hidden` will be true, + * any animation will complete, + * and the `afterhide` event will fire. + * + * @default false + */ + dismissible?: boolean; + /** + * Event handler when the banner is dismissed by the user. + * + * This does not fire when setting `hidden` manually. + * + * The `hidden` property will be `false` when this event fires. + */ + onDismiss?: (event: Event) => void; + /** + * Event handler when the banner has fully hidden. + * + * The `hidden` property will be `true` when this event fires. + * + * @implementation If implementations animate the hiding of the banner, + * this event must fire after the banner has fully hidden. + * We can add an `onHide` event in future if we want to provide a hook for the start of the animation. + */ + onAfterHide?: (event: Event) => void; + /** + * Determines whether the banner is hidden. + * + * If this property is being set on each framework render (as in 'controlled' usage), + * and the banner is `dismissible`, + * ensure you update app state for this property when the `dismiss` event fires. + * + * If the banner is not `dismissible`, it can still be hidden by setting this property. + * + * @default false + */ + hidden?: boolean; } export interface DisplayProps { - /** - * Sets the outer display type of the component. The outer type sets a component’s participation in [flow layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flow_layout). - * - * - `auto`: the component’s initial value. The actual value depends on the component and context. - * - `none`: hides the component from display and removes it from the accessibility tree, making it invisible to screen readers. - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/display - * @default 'auto' - */ - display?: MaybeResponsive<"auto" | "none">; + /** + * Sets the outer display type of the component. The outer type sets a component’s participation in [flow layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flow_layout). + * + * - `auto`: the component’s initial value. The actual value depends on the component and context. + * - `none`: hides the component from display and removes it from the accessibility tree, making it invisible to screen readers. + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/display + * @default 'auto' + */ + display?: MaybeResponsive<'auto' | 'none'>; } export interface AccessibilityRoleProps { - /** - * Sets the semantic meaning of the component’s content. When set, - * the role will be used by assistive technologies to help users - * navigate the page. - * - * @implementation Although, in HTML hosts, this property changes the element used, - * changing this property must not impact the visual styling of inside or outside of the box. - * - * @default 'generic' - */ - accessibilityRole?: AccessibilityRole; -} -export type AccessibilityRole = -/** - * Used to indicate the primary content. - * - * In an HTML host, `main` will render a `
` element. - * Learn more about the [`
` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main) and its [implicit role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) in the MDN web docs. - */ -"main" -/** - * Used to indicate the component is a header. - * - * In an HTML host `header` will render a `
` element. - * Learn more about the [`
` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header) and its [implicit role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) in the MDN web docs. - */ - | "header" -/** - * Used to display information such as copyright information, navigation links, and privacy statements. - * - * In an HTML host `footer` will render a `