From ce8bbe8299eaaca71a701d4795e9e6e03f683bea Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Fri, 3 Oct 2025 11:32:49 +0200 Subject: [PATCH 01/12] example project-specific implementation of API actions --- src/sd-specific.ts | 363 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index e69de29..3a59a4c 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -0,0 +1,363 @@ +import { + QUERYPARAM_MODELSTATEID, + QUERYPARAM_SETTINGSURL, +} from "@AppBuilderShared/types/shapediver/queryparams"; +import {IAppBuilderUrlBuilderData} from "@AppBuilderShared/utils/urlbuilder"; +import { + IAddItemToCartData, + IAddItemToCartReply, + IECommerceApiActions, + IGetParentPageInfoReply, + IGetUserProfileReply, + IScrollingApiLoadMoreData, + IScrollingApiLoadMoreReply, + IScrollingApiSetParametersData, + IScrollingApiSetParametersReply, + IUpdateSharingLinkData, + IUpdateSharingLinkReply, +} from "./shared/modules/ecommerce/types/ecommerceapi"; +import {IScrollingApiItemTypeSelect} from "./shared/modules/ecommerce/types/scrollingapi"; + +/** + * Type definition for product categories. + */ +interface ICategory { + id: number; + name: string; + slug: string; + path: string; + thumbnail_url: string; + children: ICategory[]; +} + +/** + * Type definition for product graphics. + */ +interface IProduct { + graphic_id: number; + graphic_name: string; + description: string; + category_id: number; + category_name: string; + thumbnail: string; + tags: string[]; + downloadable_files: {name: string; url: string}[]; +} + +/** + * Product/category API response. + */ +interface IGraphicsApiResponse { + categories?: ICategory[]; + products?: IProduct[]; +} + +/** + * Create query string for fetching from graphics API + * @param categories Optional categories to append as URL parameter "categories" + * @param tag Optional tag to append as URL parameter "tags" + * @param search Optional search term to append as URL parameter "search" + * @returns + */ +function getGraphicsApiQueryString( + categories?: string[] | string, + tags?: string, + search?: string, +): string { + const params = new URLSearchParams(); + if (Array.isArray(categories) && categories.length === 0) + categories = undefined; + + if (!categories && !tags && !search) categories = "all"; + + if (categories) { + if (Array.isArray(categories)) + params.set("categories", categories.join(",")); + else params.set("categories", categories); + } + if (tags) params.set("tags", tags); + if (search) params.set("search", search); + + return params.toString(); +} + +/** Query string used for the latest API call. */ +let latestQuery: string = getGraphicsApiQueryString(""); +/** Depth of categories for the latest API call. */ +let latestCategoryDepth: number = 0; +/** Page size to be used for data returned to the App Builder iframe. */ +let pageSize: number = 10; +/** Current index. */ +let currentIndex: number = 0; +/** Cached responses of previous API calls. */ +const cachedResults: Record = {}; + +/** + * Clear the cache of previous API calls. + */ +function clearCache() { + latestQuery = getGraphicsApiQueryString(); + latestCategoryDepth = 0; + pageSize = 10; + currentIndex = 0; + for (const key in cachedResults) delete cachedResults[key]; +} + +/** + * Fetch from https://tarablooms.in/wp-json/custom/v1/graphic-components + */ +async function fetchFromGraphicsApi( + queryString: string, + ignoreCache: boolean, +): Promise { + if (!ignoreCache && cachedResults[queryString]) { + return cachedResults[queryString]; + } + + const base = "https://tarablooms.in/wp-json/custom/v1/graphic-components"; + + const url = `${base}?${queryString}`; + + const resp = await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + }, + }); + + if (!resp.ok) { + const text = await resp.text().catch(() => ""); + throw new Error( + `Failed to fetch from graphics api: ${resp.status} ${resp.statusText} ${text}`, + ); + } + + const json = await resp.json().catch((err) => { + throw new Error( + `Failed to parse graphics api response: ${String(err)}`, + ); + }); + + // Basic runtime shape check + if (!json || typeof json !== "object") + throw new Error("Invalid graphics api response: not an object"); + + const result = json as IGraphicsApiResponse; + cachedResults[queryString] = result; + return result; +} + +/** + * Return and map cached results. + * @returns + */ +function returnAndMapCachedResults(): IScrollingApiLoadMoreReply { + // if there are products, return them + const products = cachedResults[latestQuery].products; + if (products) { + if (currentIndex >= products.length) { + return {hasNextPage: false, items: []}; + } + + // map products to items + const items: IScrollingApiItemTypeSelect[] = products + .slice(currentIndex, currentIndex + pageSize) + .map((p) => ({ + item: p.graphic_id + "", + data: { + displayname: p.graphic_name, + tooltip: p.description, + imageUrl: p.thumbnail, + data: p, + }, + })); + currentIndex += pageSize; + + const result = { + hasNextPage: currentIndex < products.length, + items: items, + }; + //console.debug("products", result); + return result; + } + + // if there are no products, map categories and return them + let categories = cachedResults[latestQuery].categories; + if (categories) { + if (currentIndex >= categories.length) { + return {hasNextPage: false, items: []}; + } + + for (let i = 0; i < latestCategoryDepth; i++) { + if (categories.length > 0) categories = categories[0].children; + else break; + } + + // map categories to items + const items: IScrollingApiItemTypeSelect[] = categories + .slice(currentIndex, currentIndex + pageSize) + .map((p) => ({ + item: "search:category:" + p.name, + data: { + displayname: p.name, + imageUrl: p.thumbnail_url, + data: p, + }, + })); + currentIndex += pageSize; + + const result = { + hasNextPage: currentIndex < categories.length, + items: items, + }; + //console.debug("categories", result); + return result; + } + + return {hasNextPage: false, items: []}; +} + +/** + * Handler for setting parameters for the scrolling API. + * @param data + * @returns + */ +async function scrollingApiSetParameters( + data: IScrollingApiSetParametersData, +): Promise> { + if (data.source !== "graphics") { + console.debug(`Unsupported data source name: ${data.source}`); + clearCache(); + return {hasNextPage: false, items: []}; + } + + if (data.terms) { + let categories: string[] = []; + let tags: string = ""; + let search: string = ""; + data.terms?.forEach((v) => { + if (v.startsWith("category:")) { + if (!categories) categories = []; + categories.push(v.substring("category:".length)); + } else if (v.startsWith("tag:")) tags = v.substring("tag:".length); + else search = v; + }); + const queryString = getGraphicsApiQueryString(categories, tags, search); + if (queryString !== latestQuery) { + latestQuery = queryString; + latestCategoryDepth = categories.length; + currentIndex = 0; + } + } + + if (data.pageSize !== undefined) { + pageSize = data.pageSize; + currentIndex = 0; + } + + await fetchFromGraphicsApi(latestQuery, false); + return returnAndMapCachedResults(); +} + +/** + * Handler for loading more items for the scrolling API. + * @param data + * @returns + */ +async function scrollingApiLoadMore( + data: IScrollingApiLoadMoreData, +): Promise> { + if (data.source !== "graphics") { + console.debug(`Unsupported data source name: ${data.source}`); + clearCache(); + return {hasNextPage: false, items: []}; + } + + await fetchFromGraphicsApi(latestQuery, false); + return returnAndMapCachedResults(); +} + +/** + * Specific implementation of IECommerceApiActions for the App Builder iframe. + * This overrides some of the default actions. + */ +class SpecificECommerceApiActions implements IECommerceApiActions { + constructor(private defaultActions?: IECommerceApiActions) {} + + getParentPageInfo(): Promise { + if (this.defaultActions) return this.defaultActions.getParentPageInfo(); + + return Promise.resolve({href: window.location.href}); + } + + closeConfigurator(): Promise { + if (this.defaultActions) return this.defaultActions.closeConfigurator(); + + return Promise.resolve(false); + } + + addItemToCart(data: IAddItemToCartData): Promise { + if (this.defaultActions) return this.defaultActions.addItemToCart(data); + + const reply: IAddItemToCartReply = { + id: "DUMMY_ID", + }; + + return Promise.reject(reply); + } + + getUserProfile(): Promise { + if (this.defaultActions) return this.defaultActions.getUserProfile(); + + const reply: IGetUserProfileReply = { + id: "DUMMY_ID", + email: "john@doe.com", + name: "John Doe", + }; + + return Promise.resolve(reply); + } + + updateSharingLink( + data: IUpdateSharingLinkData, + ): Promise { + if (this.defaultActions) + return this.defaultActions.updateSharingLink(data); + + const {modelStateId} = data; + const url = new URL(window.location.href); + url.searchParams.set(QUERYPARAM_MODELSTATEID, modelStateId); + const href = url.toString(); + history.replaceState(history.state, "", href); + return Promise.resolve({href}); + } + + async scrollingApiSetParameters( + data: IScrollingApiSetParametersData, + ): Promise> { + return scrollingApiSetParameters(data); + } + + async scrollingApiLoadMore( + data: IScrollingApiLoadMoreData, + ): Promise> { + return scrollingApiLoadMore(data); + } +} + +(globalThis as {[key: string]: any}).specificECommerceApiActions = + new SpecificECommerceApiActions(); + +const urlParams = new URLSearchParams(window.location.search); + +/** + * URL builder options for development environment. + */ +export const developmentUrlBuilderOptions: IAppBuilderUrlBuilderData = { + baseUrl: `https://appbuilder.shapediver.com/v1/main/${urlParams.get("appBuilderVersion") ?? "development"}/`, + settingsUrl: urlParams.get(QUERYPARAM_SETTINGSURL) ?? "_stringselect.json", + modelStateId: urlParams.get(QUERYPARAM_MODELSTATEID) ?? undefined, +}; + +(globalThis as {[key: string]: any}).developmentUrlBuilderOptions = + developmentUrlBuilderOptions; From 189fe4188e086efca67d5562158c9bbcb4a5af66 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Tue, 14 Oct 2025 13:48:20 +0200 Subject: [PATCH 02/12] support overriding of slug --- src/sd-specific.ts | 2 ++ tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index 3a59a4c..b86df4a 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -1,6 +1,7 @@ import { QUERYPARAM_MODELSTATEID, QUERYPARAM_SETTINGSURL, + QUERYPARAM_SLUG, } from "@AppBuilderShared/types/shapediver/queryparams"; import {IAppBuilderUrlBuilderData} from "@AppBuilderShared/utils/urlbuilder"; import { @@ -357,6 +358,7 @@ export const developmentUrlBuilderOptions: IAppBuilderUrlBuilderData = { baseUrl: `https://appbuilder.shapediver.com/v1/main/${urlParams.get("appBuilderVersion") ?? "development"}/`, settingsUrl: urlParams.get(QUERYPARAM_SETTINGSURL) ?? "_stringselect.json", modelStateId: urlParams.get(QUERYPARAM_MODELSTATEID) ?? undefined, + slug: urlParams.get(QUERYPARAM_SLUG) ?? undefined, }; (globalThis as {[key: string]: any}).developmentUrlBuilderOptions = diff --git a/tsconfig.json b/tsconfig.json index 91e541f..0f11da5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,6 +37,7 @@ "sourceMap": true }, "include": [ - "src/sd-wp.ts" + "src/sd-wp.ts", + "src/sd-specific.ts", ] } \ No newline at end of file From 1f4889b684f9b00fa102f33496f666a663d19a74 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Tue, 14 Oct 2025 14:25:57 +0200 Subject: [PATCH 03/12] adapt to updated graphics API --- src/sd-specific.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index b86df4a..12ddc1e 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -40,9 +40,13 @@ interface IProduct { description: string; category_id: number; category_name: string; - thumbnail: string; tags: string[]; - downloadable_files: {name: string; url: string}[]; + downloadable_files: { + name: string; + image_type: "thumbnail" | string; + image_png_url?: string; + image_jpg_url?: string; + }[]; } /** @@ -115,7 +119,8 @@ async function fetchFromGraphicsApi( return cachedResults[queryString]; } - const base = "https://tarablooms.in/wp-json/custom/v1/graphic-components"; + const base = + "https://dev1.tarablooms.in/wp-json/custom/v1/graphic-components"; const url = `${base}?${queryString}`; @@ -168,7 +173,9 @@ function returnAndMapCachedResults(): IScrollingApiLoadMoreReply { data: { displayname: p.graphic_name, tooltip: p.description, - imageUrl: p.thumbnail, + imageUrl: p.downloadable_files.find( + (f) => f.image_type === "thumbnail", + )?.image_png_url, data: p, }, })); @@ -236,10 +243,21 @@ async function scrollingApiSetParameters( let categories: string[] = []; let tags: string = ""; let search: string = ""; + let cachedCategories = cachedResults[latestQuery].categories; data.terms?.forEach((v) => { if (v.startsWith("category:")) { - if (!categories) categories = []; - categories.push(v.substring("category:".length)); + if (cachedCategories) { + const categoryName = v.substring("category:".length); + // Check if the category exists in the fetched categories + const matchedCategory = cachedCategories.find( + (cat) => cat.name === categoryName, + ); + if (matchedCategory) { + if (!categories) categories = []; + categories.push(matchedCategory.slug); + cachedCategories = matchedCategory.children; + } + } } else if (v.startsWith("tag:")) tags = v.substring("tag:".length); else search = v; }); From 0215ee91209355dccafb105bd0b5111b72ef5d89 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Tue, 4 Nov 2025 12:30:09 +0100 Subject: [PATCH 04/12] specific factory for API actions --- src/sd-specific.ts | 50 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index 12ddc1e..8be2cd8 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -316,16 +316,47 @@ class SpecificECommerceApiActions implements IECommerceApiActions { } addItemToCart(data: IAddItemToCartData): Promise { - if (this.defaultActions) return this.defaultActions.addItemToCart(data); + // here we skip the default action on purpose + //if (this.defaultActions) return this.defaultActions.addItemToCart(data); + + const {description, modelStateId} = data; + + // try to parse description as JSON + let pricingParameters: Record = {}; + if (description) { + try { + pricingParameters = JSON.parse(description); + } catch (e) { + console.warn( + `Failed to parse pricing parameters from description: ${description}`, + e, + ); + } + } + + // TODO Tara Blooms: + // Use pricing parameters to request price(s) from your backend, + // display user interface for confirming the addition to the cart, + // and finally return the cart item id. + // In case the user denies adding to the cart, return an empty cart item id. + + console.log( + "Adding item to cart with modelStateId:", + modelStateId, + "and pricing parameters:", + pricingParameters, + ); const reply: IAddItemToCartReply = { id: "DUMMY_ID", }; - return Promise.reject(reply); + return Promise.resolve(reply); } getUserProfile(): Promise { + // NOTE: This action is not used yet by App Builder, therefore no + // reason to implement it. if (this.defaultActions) return this.defaultActions.getUserProfile(); const reply: IGetUserProfileReply = { @@ -340,9 +371,13 @@ class SpecificECommerceApiActions implements IECommerceApiActions { updateSharingLink( data: IUpdateSharingLinkData, ): Promise { - if (this.defaultActions) - return this.defaultActions.updateSharingLink(data); + // here we skip the default action on purpose + //if (this.defaultActions) + // return this.defaultActions.updateSharingLink(data); + // TODO Tara Blooms: Here you could show a user interface for sharing the link + // via email, social media, etc. + // For now, we just update the URL in the browser. const {modelStateId} = data; const url = new URL(window.location.href); url.searchParams.set(QUERYPARAM_MODELSTATEID, modelStateId); @@ -354,18 +389,21 @@ class SpecificECommerceApiActions implements IECommerceApiActions { async scrollingApiSetParameters( data: IScrollingApiSetParametersData, ): Promise> { + // connection to the graphics API return scrollingApiSetParameters(data); } async scrollingApiLoadMore( data: IScrollingApiLoadMoreData, ): Promise> { + // connection to the graphics API return scrollingApiLoadMore(data); } } -(globalThis as {[key: string]: any}).specificECommerceApiActions = - new SpecificECommerceApiActions(); +(globalThis as {[key: string]: any}).specificECommerceApiActionsFactory = ( + defaultActions: IECommerceApiActions, +) => new SpecificECommerceApiActions(defaultActions); const urlParams = new URLSearchParams(window.location.search); From c3c1ac752a6bbd20293170607d76744a69ce0eb5 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Mon, 24 Nov 2025 12:18:02 +0100 Subject: [PATCH 05/12] update product API url --- src/sd-specific.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index 8be2cd8..e048279 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -120,7 +120,7 @@ async function fetchFromGraphicsApi( } const base = - "https://dev1.tarablooms.in/wp-json/custom/v1/graphic-components"; + "https://test1.tarablooms.in/wp-json/custom/v1/graphic-components"; const url = `${base}?${queryString}`; From 9a22c59b835195a793bffa20aaeb076706fa6cf3 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Wed, 26 Nov 2025 22:40:27 +0100 Subject: [PATCH 06/12] query using category_id --- src/sd-specific.ts | 130 +++++++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 53 deletions(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index e048279..e500444 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -68,6 +68,7 @@ function getGraphicsApiQueryString( categories?: string[] | string, tags?: string, search?: string, + categoryIds?: number[], ): string { const params = new URLSearchParams(); if (Array.isArray(categories) && categories.length === 0) @@ -75,10 +76,25 @@ function getGraphicsApiQueryString( if (!categories && !tags && !search) categories = "all"; - if (categories) { + if (categoryIds && categoryIds.length > 0) { + params.set( + "category_id", + categoryIds[categoryIds.length - 1].toString(), + ); + if (!tags && !search) { + // if a product category has been chosen, but no search term + // is present, get all products in that category + params.set("tags", "all"); + } + } else if (categories) { if (Array.isArray(categories)) params.set("categories", categories.join(",")); else params.set("categories", categories); + if (categories !== "all" && !tags && !search) { + // if a product category has been chosen, but no search term + // is present, get all products in that category + params.set("tags", "all"); + } } if (tags) params.set("tags", tags); if (search) params.set("search", search); @@ -88,8 +104,10 @@ function getGraphicsApiQueryString( /** Query string used for the latest API call. */ let latestQuery: string = getGraphicsApiQueryString(""); +/** Query string for fetching all categories. */ +const allCategoriesQuery: string = getGraphicsApiQueryString(); /** Depth of categories for the latest API call. */ -let latestCategoryDepth: number = 0; +let latestCategorySlugs: Array = []; /** Page size to be used for data returned to the App Builder iframe. */ let pageSize: number = 10; /** Current index. */ @@ -102,7 +120,7 @@ const cachedResults: Record = {}; */ function clearCache() { latestQuery = getGraphicsApiQueryString(); - latestCategoryDepth = 0; + latestCategorySlugs = []; pageSize = 10; currentIndex = 0; for (const key in cachedResults) delete cachedResults[key]; @@ -157,14 +175,45 @@ async function fetchFromGraphicsApi( * Return and map cached results. * @returns */ -function returnAndMapCachedResults(): IScrollingApiLoadMoreReply { - // if there are products, return them - const products = cachedResults[latestQuery].products; - if (products) { - if (currentIndex >= products.length) { - return {hasNextPage: false, items: []}; +async function returnAndMapCachedResults(): Promise< + IScrollingApiLoadMoreReply +> { + const result: IScrollingApiLoadMoreReply = { + hasNextPage: false, + items: [], + }; + + // map categories + const categoryData = await fetchFromGraphicsApi(allCategoriesQuery, false); + let categories = categoryData.categories; + if (categories) { + for (let i = 0; i < latestCategorySlugs.length; i++) { + const categorySlug = latestCategorySlugs[i]; + const matchedCategory: ICategory | undefined = categories.find( + (cat) => cat.slug === categorySlug, + ); + if (!matchedCategory) + throw new Error( + `Category with slug "${categorySlug}" not found`, + ); + categories = matchedCategory.children; } + // map categories to items + const items: IScrollingApiItemTypeSelect[] = categories.map((p) => ({ + item: "search:category:" + p.name, + data: { + displayname: p.name, + imageUrl: p.thumbnail_url, + data: p, + }, + })); + result.items = items; + } + + // map products + const products = cachedResults[latestQuery].products; + if (products) { // map products to items const items: IScrollingApiItemTypeSelect[] = products .slice(currentIndex, currentIndex + pageSize) @@ -181,48 +230,13 @@ function returnAndMapCachedResults(): IScrollingApiLoadMoreReply { })); currentIndex += pageSize; - const result = { - hasNextPage: currentIndex < products.length, - items: items, - }; - //console.debug("products", result); - return result; + result.hasNextPage = currentIndex < products.length; + result.items = result.items.concat(items); } - // if there are no products, map categories and return them - let categories = cachedResults[latestQuery].categories; - if (categories) { - if (currentIndex >= categories.length) { - return {hasNextPage: false, items: []}; - } + console.log(result); - for (let i = 0; i < latestCategoryDepth; i++) { - if (categories.length > 0) categories = categories[0].children; - else break; - } - - // map categories to items - const items: IScrollingApiItemTypeSelect[] = categories - .slice(currentIndex, currentIndex + pageSize) - .map((p) => ({ - item: "search:category:" + p.name, - data: { - displayname: p.name, - imageUrl: p.thumbnail_url, - data: p, - }, - })); - currentIndex += pageSize; - - const result = { - hasNextPage: currentIndex < categories.length, - items: items, - }; - //console.debug("categories", result); - return result; - } - - return {hasNextPage: false, items: []}; + return result; } /** @@ -240,10 +254,15 @@ async function scrollingApiSetParameters( } if (data.terms) { - let categories: string[] = []; + const categories: string[] = []; + const categoryIds: number[] = []; let tags: string = ""; let search: string = ""; - let cachedCategories = cachedResults[latestQuery].categories; + const categoryData = await fetchFromGraphicsApi( + allCategoriesQuery, + false, + ); + let cachedCategories = categoryData.categories; data.terms?.forEach((v) => { if (v.startsWith("category:")) { if (cachedCategories) { @@ -253,18 +272,23 @@ async function scrollingApiSetParameters( (cat) => cat.name === categoryName, ); if (matchedCategory) { - if (!categories) categories = []; categories.push(matchedCategory.slug); + categoryIds.push(matchedCategory.id); cachedCategories = matchedCategory.children; } } } else if (v.startsWith("tag:")) tags = v.substring("tag:".length); else search = v; }); - const queryString = getGraphicsApiQueryString(categories, tags, search); + const queryString = getGraphicsApiQueryString( + categories, + tags, + search, + categoryIds, + ); if (queryString !== latestQuery) { latestQuery = queryString; - latestCategoryDepth = categories.length; + latestCategorySlugs = categories; currentIndex = 0; } } From 39d4e2085e4414a30a921f99fd907d4819519f5e Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Thu, 27 Nov 2025 17:06:15 +0100 Subject: [PATCH 07/12] use invisible separator for category items --- src/sd-specific.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index e500444..ef16b12 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -114,6 +114,8 @@ let pageSize: number = 10; let currentIndex: number = 0; /** Cached responses of previous API calls. */ const cachedResults: Record = {}; +/** Identifier for category items. */ +const categoryIdentifier = "\u200B\u2063"; /** * Clear the cache of previous API calls. @@ -201,7 +203,7 @@ async function returnAndMapCachedResults(): Promise< // map categories to items const items: IScrollingApiItemTypeSelect[] = categories.map((p) => ({ - item: "search:category:" + p.name, + item: `search:${categoryIdentifier}` + p.name, data: { displayname: p.name, imageUrl: p.thumbnail_url, @@ -264,9 +266,9 @@ async function scrollingApiSetParameters( ); let cachedCategories = categoryData.categories; data.terms?.forEach((v) => { - if (v.startsWith("category:")) { + if (v.startsWith(categoryIdentifier)) { if (cachedCategories) { - const categoryName = v.substring("category:".length); + const categoryName = v.substring(categoryIdentifier.length); // Check if the category exists in the fetched categories const matchedCategory = cachedCategories.find( (cat) => cat.name === categoryName, From 12af2ad377bbf023094ca0a6b3305d03189ec368 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Thu, 27 Nov 2025 17:06:39 +0100 Subject: [PATCH 08/12] custom wordpress product field for parameter settings URL --- src/sd-wp.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/sd-wp.php b/src/sd-wp.php index fead630..aba85f1 100755 --- a/src/sd-wp.php +++ b/src/sd-wp.php @@ -497,13 +497,19 @@ public function add_custom_product_fields() { 'desc_tip' => 'true', 'description' => __('Optional. Enter a URL of the JSON file defining the App Builder theme of the configurator. If left empty, the default theme configured in the ShapeDiver plugin settings will be used. This can be a relative or absolute URL.', 'woocommerce') )); + woocommerce_wp_text_input(array( + 'id' => '_parameters_settings_url', + 'label' => __('Parameters Settings URL', 'woocommerce'), + 'desc_tip' => 'true', + 'description' => __('Optional. Value to use for the "parameters_settings_url" input of the configurator.', 'woocommerce') + )); echo ''; } // Save custom product fields public function save_custom_product_fields($post_id) { - $fields = array('_slug', '_embedding_ticket', '_model_view_url', '_model_state_id', '_configurator_url', '_settings_url'); + $fields = array('_slug', '_embedding_ticket', '_model_view_url', '_model_state_id', '_configurator_url', '_settings_url', '_parameters_settings_url'); foreach ($fields as $field) { if (isset($_POST[$field])) { update_post_meta($post_id, $field, sanitize_text_field(wp_unslash($_POST[$field]))); @@ -518,6 +524,7 @@ public function get_product_data() { if (!$product) { wp_send_json_error('Invalid product'); } + $data = array( 'id' => $product->get_id(), 'name' => esc_html($product->get_name()), @@ -528,8 +535,19 @@ public function get_product_data() { 'model_state_id' => sanitize_text_field(get_post_meta($product_id, '_model_state_id', true)), 'slug' => sanitize_text_field(get_post_meta($product_id, '_slug', true)), 'settings_url' => sanitize_text_field(get_post_meta($product_id, '_settings_url', true)), - //'query_params' => array(), // placeholder for returning custom query parameters ); + + // Prepare further query parameters to be appended to the configurator URL. + // This can be used to pass initial parameter values to the configurator. + // Related documentation: + // https://help.shapediver.com/doc/embed-apps-in-external-websites#EmbedAppsinexternalwebsites-URLparameters + $parameter_settings_url = sanitize_text_field(get_post_meta($product_id, '_parameters_settings_url', true)); + if (!empty($parameter_settings_url)) { + $query_params = array(); + $query_params['_parameters_settings_url'] = $parameter_settings_url; + $data['query_params'] = $query_params; + } + wp_send_json_success($data); } From 1772ae2ec0a5bd47e33b651b1c50ebad3c0507cd Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Fri, 12 Dec 2025 11:28:40 +0100 Subject: [PATCH 09/12] extent updateSharingLink example --- src/sd-specific.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index ef16b12..04d5cad 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -404,7 +404,12 @@ class SpecificECommerceApiActions implements IECommerceApiActions { // TODO Tara Blooms: Here you could show a user interface for sharing the link // via email, social media, etc. // For now, we just update the URL in the browser. - const {modelStateId} = data; + const {modelStateId, imageUrl} = data; + console.log( + imageUrl + ? `Model state with image data URL: ${imageUrl.substring(0, 10)}` + : "Model state without image", + ); const url = new URL(window.location.href); url.searchParams.set(QUERYPARAM_MODELSTATEID, modelStateId); const href = url.toString(); From b6b38977dd81d5e12c66a047bcd29de6390f4ec2 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Mon, 15 Dec 2025 13:54:41 +0100 Subject: [PATCH 10/12] update graphics API base url --- src/sd-specific.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index 04d5cad..a526bce 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -129,7 +129,7 @@ function clearCache() { } /** - * Fetch from https://tarablooms.in/wp-json/custom/v1/graphic-components + * Fetch from Graphics API. */ async function fetchFromGraphicsApi( queryString: string, @@ -140,7 +140,7 @@ async function fetchFromGraphicsApi( } const base = - "https://test1.tarablooms.in/wp-json/custom/v1/graphic-components"; + "https://test1.tarablooms.in/wp-json/shapediver/v1/graphic-components"; const url = `${base}?${queryString}`; From f6c3c362de9c5367cc89f199c82542d55559d1ce Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Mon, 15 Dec 2025 18:34:46 +0100 Subject: [PATCH 11/12] adapt graphics API connector to new API structure --- src/sd-specific.ts | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index a526bce..e53904b 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -23,30 +23,36 @@ import {IScrollingApiItemTypeSelect} from "./shared/modules/ecommerce/types/scro * Type definition for product categories. */ interface ICategory { + children: ICategory[]; id: number; name: string; - slug: string; path: string; + slug: string; thumbnail_url: string; - children: ICategory[]; } /** * Type definition for product graphics. */ interface IProduct { - graphic_id: number; - graphic_name: string; - description: string; category_id: number; category_name: string; - tags: string[]; + category_structure: Array; + description: string; downloadable_files: { - name: string; - image_type: "thumbnail" | string; - image_png_url?: string; - image_jpg_url?: string; + height: number; + image_size: string; + keep_horizontal_orientation: boolean; + keep_vertical_orientation: boolean; + parameterValues: { + file_url: string; + }; + sessionId: Array; + width: number; }[]; + graphic_id: number; + graphic_name: string; + tags: string[]; } /** @@ -224,9 +230,9 @@ async function returnAndMapCachedResults(): Promise< data: { displayname: p.graphic_name, tooltip: p.description, - imageUrl: p.downloadable_files.find( - (f) => f.image_type === "thumbnail", - )?.image_png_url, + imageUrl: p.downloadable_files.find((f) => + f.sessionId.includes("thumbnail"), + )?.parameterValues.file_url, data: p, }, })); From 9cd3944ee2c0aef38f52928cec60bcdd2195aea5 Mon Sep 17 00:00:00 2001 From: Alexander Schiftner Date: Tue, 20 Jan 2026 12:27:38 +0100 Subject: [PATCH 12/12] preparation for image upload --- src/sd-specific.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/sd-specific.ts b/src/sd-specific.ts index e53904b..ad6297b 100755 --- a/src/sd-specific.ts +++ b/src/sd-specific.ts @@ -10,6 +10,8 @@ import { IECommerceApiActions, IGetParentPageInfoReply, IGetUserProfileReply, + IMessageToParentData, + IMessageToParentReply, IScrollingApiLoadMoreData, IScrollingApiLoadMoreReply, IScrollingApiSetParametersData, @@ -122,6 +124,8 @@ let currentIndex: number = 0; const cachedResults: Record = {}; /** Identifier for category items. */ const categoryIdentifier = "\u200B\u2063"; +/** Callback for resetting cache in App Builder app. */ +let resetCacheCallback: (() => void) | null = null; /** * Clear the cache of previous API calls. @@ -132,6 +136,7 @@ function clearCache() { pageSize = 10; currentIndex = 0; for (const key in cachedResults) delete cachedResults[key]; + if (resetCacheCallback) resetCacheCallback(); } /** @@ -242,7 +247,7 @@ async function returnAndMapCachedResults(): Promise< result.items = result.items.concat(items); } - console.log(result); + // console.log(result); return result; } @@ -261,6 +266,10 @@ async function scrollingApiSetParameters( return {hasNextPage: false, items: []}; } + if (data.reset) { + resetCacheCallback = data.reset; + } + if (data.terms) { const categories: string[] = []; const categoryIds: number[] = []; @@ -328,6 +337,24 @@ async function scrollingApiLoadMore( return returnAndMapCachedResults(); } +/** + * This function is a placeholder for image upload implementation. + * + * What this function is expected to do: + * + * * Present a UI to the user to select one or several images to upload. + * * Upload the images. + * * Return true if the upload was successful and the graphics selection UI should be refreshed. + * * Return false if the upload failed or was cancelled by the user. + * + * @returns + */ +async function imageUpload(): Promise { + // TODO Tara Blooms: Implement image upload to your backend here. + console.log("Uploading image..."); + return Promise.resolve(true); +} + /** * Specific implementation of IECommerceApiActions for the App Builder iframe. * This overrides some of the default actions. @@ -436,6 +463,25 @@ class SpecificECommerceApiActions implements IECommerceApiActions { // connection to the graphics API return scrollingApiLoadMore(data); } + + async messageToParent( + data: IMessageToParentData, + ): Promise { + if (data.type === "uploadImages") { + const success = await imageUpload(); + if (success) clearCache(); + return Promise.resolve({ + notification: { + data: { + message: success + ? "Image uploaded successfully." + : "Image upload cancelled or failed.", + }, + }, + }); + } + return Promise.resolve({}); + } } (globalThis as {[key: string]: any}).specificECommerceApiActionsFactory = (