diff --git a/examples/plugins/ecommerce/main.js b/examples/plugins/ecommerce/main.js index d65ceff..f29e831 100644 --- a/examples/plugins/ecommerce/main.js +++ b/examples/plugins/ecommerce/main.js @@ -129,6 +129,7 @@ async function syncRecords(collection, rows, keep = false) { } /** Field keys a layer shows of a product, by what the layer or a component property is called. */ +/** @type {[RegExp, string][]} */ const LINKS = [[/title|name/i, 'title'], [/price/i, 'price'], [/image|picture|photo|cover/i, 'image'], [/description|body/i, 'description']] /** @@ -139,7 +140,8 @@ const LINKS = [[/title|name/i, 'title'], [/price/i, 'price'], [/image|picture|ph async function link(product) { const col = await collectionFor('commerce.product') if (!col) throw new Error('This design has no products collection: Make it a store first') - const ids = await popcraft.getSelection() + // getSelection gives nodes; setRecord and bindField take their ids. + const ids = (await popcraft.getSelection()).map(n => n.id) if (!ids.length) throw new Error('Select the layers that should show this product first') await syncRecords(col, [product], true) await popcraft.setRecord({ ids, collection: col.id, record: product.handle }) @@ -147,12 +149,12 @@ async function link(product) { for (const id of ids) { const node = await popcraft.getNode(id) if (!node) continue - const has = node.boundExpressions ?? {} + const has = /** @type {Record} */ (node.boundExpressions ?? {}) const field = name => LINKS.find(([re]) => re.test(name))?.[1] if (node.type === 'IMAGE' && !has.imageUrl) { await popcraft.bindField({ ids: [id], property: 'picture', field: 'image' }); bound++ } if (node.type === 'TEXT' && !has.characters && field(node.name)) { await popcraft.bindField({ ids: [id], property: 'text', field: field(node.name) }); bound++ } if (node.type === 'INSTANCE') { - const main = await popcraft.getNode(node.mainComponentId) + const main = await popcraft.getNode(String(node.mainComponentId)) const set = main?.parentId ? await popcraft.getNode(main.parentId) : null const defs = (set?.type === 'COMPONENT_SET' ? set : main)?.componentPropertyDefinitions ?? {} for (const [name, def] of Object.entries(defs)) { diff --git a/src/runtime/main.ts b/src/runtime/main.ts index 00013b3..9a892cc 100644 --- a/src/runtime/main.ts +++ b/src/runtime/main.ts @@ -5,7 +5,8 @@ import { createRpcClient, type RpcClient } from './rpc.js' import type { PopCraftEvents, PopCraftExportOptions, PopCraftFindQuery, PopCraftNode, PopCraftNodeType, - PopCraftPage, PopCraftPaint, PopCraftShowUIOptions, PopCraftVariables, + PopCraftPage, PopCraftPaint, PopCraftShowUIOptions, PopCraftUploadOptions, PopCraftUploadResult, + PopCraftVariables, } from '../types.js' import type { PopCraftSerialisedWidget, PopCraftWidgetDefinition, PopCraftWidgetHandler, @@ -265,6 +266,20 @@ export function createMainApi(rpc: RpcClient = createRpcClient()) { exportNode: (id: string, options?: PopCraftExportOptions) => rpc.call('exportNode', { id, options: options || {} }), openExternal: (url: string) => rpc.call('openExternal', { url }), + /** + * Send a file to an https URL. PopCraft makes the request, not the sandbox, so it works for files of + * any size the server accepts, including straight to a presigned storage URL. Needs `network`. + */ + upload: (url: string, options: PopCraftUploadOptions) => { + const body = options.body instanceof ArrayBuffer ? new Uint8Array(options.body) : options.body + return rpc.call('upload', { + url, + body, + method: options.method, + contentType: options.contentType, + headers: options.headers, + }) + }, notify: (message: string, options?: { error?: boolean }) => rpc.call('notify', { message: String(message), error: !!(options && options.error) }), showUI: (options?: PopCraftShowUIOptions) => rpc.call('showUI', options || {}), diff --git a/src/types.ts b/src/types.ts index bebf62c..6921d86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,8 @@ export const ALL_PERMISSIONS: readonly PopCraftPermission[] = ['read', 'write', export type PopCraftNodeType = | 'RECTANGLE' | 'ELLIPSE' | 'POLYGON' | 'STAR' | 'LINE' | 'ARROW' - | 'FRAME' | 'SECTION' | 'TEXT' | 'STICKY' | 'VECTOR' | 'WIDGET' + | 'FRAME' | 'SECTION' | 'SLICE' | 'GROUP' | 'TEXT' | 'STICKY' | 'VECTOR' | 'BOOLEAN' | 'WIDGET' + | 'IMAGE' | 'COMPONENT' | 'COMPONENT_SET' | 'INSTANCE' | 'PIXEL_GRID' | 'RASTER' export interface PopCraftCommand { /** Passed to the plugin as `popcraft.command`. */ @@ -150,6 +151,27 @@ export interface PopCraftExportOptions { scale?: number } +/** What `popcraft.upload` sends. */ +export interface PopCraftUploadOptions { + /** The file: bytes (an `exportNode` result, say) or text. */ + body: Uint8Array | ArrayBuffer | string + /** Defaults to `PUT`, the verb presigned storage URLs expect. */ + method?: 'PUT' | 'POST' + /** Sent as the request's `Content-Type`. */ + contentType?: string + /** Extra request headers, such as one a storage service asks for. */ + headers?: Record +} + +/** The server's answer to `popcraft.upload`. */ +export interface PopCraftUploadResult { + ok: boolean + status: number + headers: Record + /** The response body as text (parse it yourself if it is JSON). */ + body: string +} + export interface PopCraftShowUIOptions { width?: number height?: number