@fluid-app/portal-core 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data-sources/transformers.cjs +3 -1
- package/dist/data-sources/transformers.cjs.map +1 -1
- package/dist/data-sources/transformers.d.cts.map +1 -1
- package/dist/data-sources/transformers.d.mts.map +1 -1
- package/dist/data-sources/transformers.mjs +3 -1
- package/dist/data-sources/transformers.mjs.map +1 -1
- package/dist/data-sources/use-widget-data.cjs +37 -23
- package/dist/data-sources/use-widget-data.cjs.map +1 -1
- package/dist/data-sources/use-widget-data.d.cts +4 -0
- package/dist/data-sources/use-widget-data.d.cts.map +1 -1
- package/dist/data-sources/use-widget-data.d.mts +4 -0
- package/dist/data-sources/use-widget-data.d.mts.map +1 -1
- package/dist/data-sources/use-widget-data.mjs +38 -24
- package/dist/data-sources/use-widget-data.mjs.map +1 -1
- package/dist/registries/index.d.cts +3 -315
- package/dist/registries/index.d.cts.map +1 -1
- package/dist/registries/index.d.mts +3 -315
- package/dist/registries/index.d.mts.map +1 -1
- package/dist/widget-manifest-DQmTtAF1.d.mts +373 -0
- package/dist/widget-manifest-DQmTtAF1.d.mts.map +1 -0
- package/dist/widget-manifest-Ei8Wnspj.d.cts +373 -0
- package/dist/widget-manifest-Ei8Wnspj.d.cts.map +1 -0
- package/dist/widget-utils/index.cjs +7 -2
- package/dist/widget-utils/index.cjs.map +1 -1
- package/dist/widget-utils/index.d.cts +2 -1
- package/dist/widget-utils/index.d.cts.map +1 -1
- package/dist/widget-utils/index.d.mts +2 -1
- package/dist/widget-utils/index.d.mts.map +1 -1
- package/dist/widget-utils/index.mjs +7 -2
- package/dist/widget-utils/index.mjs.map +1 -1
- package/package.json +8 -1
|
@@ -83,16 +83,18 @@ const toVideoPropsFromShareable = (data) => {
|
|
|
83
83
|
*/
|
|
84
84
|
const toShareableProps = (data, source) => {
|
|
85
85
|
if (!Array.isArray(data)) return [];
|
|
86
|
+
const sourceTypeHint = source?.type === "api" ? source.variables?.shareable_type : void 0;
|
|
86
87
|
return data.map((item) => {
|
|
87
88
|
const d = item;
|
|
88
89
|
const widgetConfig = extractCustomWidgetConfig(d, source);
|
|
90
|
+
const shareableType = d.type ?? d.relateable_type ?? d.shareable_type ?? d.shareableType ?? sourceTypeHint ?? "";
|
|
89
91
|
return {
|
|
90
92
|
id: d.id,
|
|
91
93
|
title: d.title ?? d.name ?? "",
|
|
92
94
|
description: extractDescription(d),
|
|
93
95
|
imageUrl: extractImageUrl(d),
|
|
94
96
|
videoUrl: (d.video_url ?? d.videoUrl) || null,
|
|
95
|
-
shareableType
|
|
97
|
+
shareableType,
|
|
96
98
|
kind: d.kind ?? "image",
|
|
97
99
|
status: d.status ?? "active",
|
|
98
100
|
wholesalePrice: d.display_wholesale_price ?? d.wholesale_price ?? null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformers.cjs","names":[],"sources":["../../src/data-sources/transformers.ts"],"sourcesContent":["/**\n * Widget Transformers\n *\n * Transform functions that map API response data to widget-specific prop shapes.\n * Each widget has one transformer that handles all data structure variants:\n * - Standard: Direct field name match\n * - Legacy: Different field names that need mapping\n * - Minimal: Bare minimum fields that need defaults\n *\n * Usage:\n * 1. Transformers are registered in the DataSourceRegistry\n * 2. Reference by name in ApiDataSource.transform\n * 3. Applied after resultPath extraction, before targetProps assignment\n */\n\nimport type { DataTransformer, DataSource } from \"./types\";\n\n/**\n * Helper to extract image URL from various API response structures.\n * Handles:\n * - Flat fields (Medium, Page, Product): image_url, imageUrl, thumbnail_url, src\n * - Nested images array (EnrollmentPack): images[0].image_url\n * - Nested library_items (Library): library_items[0].relateable.image_url\n */\nfunction extractImageUrl(d: Record<string, unknown>): string {\n // Try flat fields first (Medium, Page, Product)\n if (d.image_url) return d.image_url as string;\n if (d.imageUrl) return d.imageUrl as string;\n if (d.thumbnail_url) return d.thumbnail_url as string;\n if (d.src) return d.src as string;\n\n // Try nested images array (EnrollmentPack)\n const images = d.images as Array<Record<string, unknown>> | undefined;\n if (images && images.length > 0) {\n const firstImage = images[0];\n if (firstImage?.image_url) return firstImage.image_url as string;\n if (firstImage?.url) return firstImage.url as string;\n }\n\n // Try nested library_items (Library/Playlist)\n const libraryItems = d.library_items as\n | Array<Record<string, unknown>>\n | undefined;\n if (libraryItems && libraryItems.length > 0) {\n const firstItem = libraryItems[0];\n const relateable = firstItem?.relateable as\n | Record<string, unknown>\n | undefined;\n if (relateable?.image_url) return relateable.image_url as string;\n if (relateable?.imageUrl) return relateable.imageUrl as string;\n if (relateable?.thumbnail_url) return relateable.thumbnail_url as string;\n }\n\n return \"\";\n}\n\n/**\n * Helper to extract description from various API response structures.\n * Handles different field names across endpoints.\n */\nfunction extractDescription(d: Record<string, unknown>): string {\n // Try various description field names\n const desc =\n d.description ?? d.stripped ?? d.stripped_description ?? d.body ?? \"\";\n\n // Handle nested description object (some APIs return { body: \"...\" })\n if (typeof desc === \"object\" && desc !== null && \"body\" in desc) {\n return ((desc as Record<string, unknown>).body as string) ?? \"\";\n }\n\n return desc as string;\n}\n\n/**\n * Helper to extract custom widget config from a selected item.\n */\nfunction extractCustomWidgetConfig(\n d: Record<string, unknown>,\n source: DataSource | undefined,\n): Record<string, unknown> {\n if (source === undefined || source.type !== \"custom\") return {};\n\n const selectedItem = source.selectedItems.find(\n (s) => String(s.id) === String(d.id),\n );\n return selectedItem?.widgetConfig ?? {};\n}\n\n/**\n * ImageWidget transformer from shareable data\n */\nconst toImagePropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", alt: \"Image\", objectFit: \"cover\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: extractImageUrl(d),\n alt: (d.title ?? d.name ?? d.alt ?? \"Image\") as string,\n objectFit: \"cover\" as const,\n };\n};\n\n/**\n * VideoWidget transformer from shareable data\n */\nconst toVideoPropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", poster: \"\", caption: \"\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: ((d.video_url ?? d.videoUrl ?? d.src) as string) || \"\",\n poster: extractImageUrl(d),\n caption: (d.title ?? d.name ?? d.caption ?? \"\") as string,\n };\n};\n\n/**\n * Shareable content transformer\n * Normalizes shareable API responses to a consistent format\n */\nconst toShareableProps: DataTransformer = (data, source) => {\n if (!Array.isArray(data)) return [];\n\n return data.map((item: unknown) => {\n const d = item as Record<string, unknown>;\n\n const widgetConfig = extractCustomWidgetConfig(d, source);\n\n return {\n id: d.id,\n title: (d.title ?? d.name ?? \"\") as string,\n description: extractDescription(d),\n imageUrl: extractImageUrl(d),\n videoUrl: ((d.video_url ?? d.videoUrl) as string) || null,\n shareableType
|
|
1
|
+
{"version":3,"file":"transformers.cjs","names":[],"sources":["../../src/data-sources/transformers.ts"],"sourcesContent":["/**\n * Widget Transformers\n *\n * Transform functions that map API response data to widget-specific prop shapes.\n * Each widget has one transformer that handles all data structure variants:\n * - Standard: Direct field name match\n * - Legacy: Different field names that need mapping\n * - Minimal: Bare minimum fields that need defaults\n *\n * Usage:\n * 1. Transformers are registered in the DataSourceRegistry\n * 2. Reference by name in ApiDataSource.transform\n * 3. Applied after resultPath extraction, before targetProps assignment\n */\n\nimport type { DataTransformer, DataSource } from \"./types\";\n\n/**\n * Helper to extract image URL from various API response structures.\n * Handles:\n * - Flat fields (Medium, Page, Product): image_url, imageUrl, thumbnail_url, src\n * - Nested images array (EnrollmentPack): images[0].image_url\n * - Nested library_items (Library): library_items[0].relateable.image_url\n */\nfunction extractImageUrl(d: Record<string, unknown>): string {\n // Try flat fields first (Medium, Page, Product)\n if (d.image_url) return d.image_url as string;\n if (d.imageUrl) return d.imageUrl as string;\n if (d.thumbnail_url) return d.thumbnail_url as string;\n if (d.src) return d.src as string;\n\n // Try nested images array (EnrollmentPack)\n const images = d.images as Array<Record<string, unknown>> | undefined;\n if (images && images.length > 0) {\n const firstImage = images[0];\n if (firstImage?.image_url) return firstImage.image_url as string;\n if (firstImage?.url) return firstImage.url as string;\n }\n\n // Try nested library_items (Library/Playlist)\n const libraryItems = d.library_items as\n | Array<Record<string, unknown>>\n | undefined;\n if (libraryItems && libraryItems.length > 0) {\n const firstItem = libraryItems[0];\n const relateable = firstItem?.relateable as\n | Record<string, unknown>\n | undefined;\n if (relateable?.image_url) return relateable.image_url as string;\n if (relateable?.imageUrl) return relateable.imageUrl as string;\n if (relateable?.thumbnail_url) return relateable.thumbnail_url as string;\n }\n\n return \"\";\n}\n\n/**\n * Helper to extract description from various API response structures.\n * Handles different field names across endpoints.\n */\nfunction extractDescription(d: Record<string, unknown>): string {\n // Try various description field names\n const desc =\n d.description ?? d.stripped ?? d.stripped_description ?? d.body ?? \"\";\n\n // Handle nested description object (some APIs return { body: \"...\" })\n if (typeof desc === \"object\" && desc !== null && \"body\" in desc) {\n return ((desc as Record<string, unknown>).body as string) ?? \"\";\n }\n\n return desc as string;\n}\n\n/**\n * Helper to extract custom widget config from a selected item.\n */\nfunction extractCustomWidgetConfig(\n d: Record<string, unknown>,\n source: DataSource | undefined,\n): Record<string, unknown> {\n if (source === undefined || source.type !== \"custom\") return {};\n\n const selectedItem = source.selectedItems.find(\n (s) => String(s.id) === String(d.id),\n );\n return selectedItem?.widgetConfig ?? {};\n}\n\n/**\n * ImageWidget transformer from shareable data\n */\nconst toImagePropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", alt: \"Image\", objectFit: \"cover\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: extractImageUrl(d),\n alt: (d.title ?? d.name ?? d.alt ?? \"Image\") as string,\n objectFit: \"cover\" as const,\n };\n};\n\n/**\n * VideoWidget transformer from shareable data\n */\nconst toVideoPropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", poster: \"\", caption: \"\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: ((d.video_url ?? d.videoUrl ?? d.src) as string) || \"\",\n poster: extractImageUrl(d),\n caption: (d.title ?? d.name ?? d.caption ?? \"\") as string,\n };\n};\n\n/**\n * Shareable content transformer\n * Normalizes shareable API responses to a consistent format\n */\nconst toShareableProps: DataTransformer = (data, source) => {\n if (!Array.isArray(data)) return [];\n\n // Derive a fallback shareable type from the data source configuration.\n // API presets pass shareable_type in their variables (e.g., \"products\").\n // Custom sources store it per-item (handled below).\n const sourceTypeHint =\n source?.type === \"api\" ? source.variables?.shareable_type : undefined;\n\n return data.map((item: unknown) => {\n const d = item as Record<string, unknown>;\n\n const widgetConfig = extractCustomWidgetConfig(d, source);\n\n // Resolve shareable type from the item itself, then fall back to source hint\n const shareableType = (d.type ??\n d.relateable_type ??\n d.shareable_type ??\n d.shareableType ??\n sourceTypeHint ??\n \"\") as string;\n\n return {\n id: d.id,\n title: (d.title ?? d.name ?? \"\") as string,\n description: extractDescription(d),\n imageUrl: extractImageUrl(d),\n videoUrl: ((d.video_url ?? d.videoUrl) as string) || null,\n shareableType,\n kind: (d.kind ?? \"image\") as string,\n status: (d.status ?? \"active\") as string,\n wholesalePrice: d.display_wholesale_price ?? d.wholesale_price ?? null,\n subscriptionPrice: d.subscription_price ?? null,\n outOfStock: d.out_of_stock ?? false,\n lowInStock: d.low_in_stock ?? false,\n ...d,\n ...widgetConfig,\n };\n });\n};\n\n/**\n * Carousel slides from shareables transformer\n */\nconst toCarouselSlidesFromShareables: DataTransformer = (data, source) => {\n if (!Array.isArray(data)) return [];\n\n return data.map((item: unknown, index: number) => {\n const d = item as Record<string, unknown>;\n\n const widgetConfig = extractCustomWidgetConfig(d, source);\n\n const imageUrl = extractImageUrl(d);\n const isVideo = d.kind === \"video\" || d.videoUrl || d.video_url;\n const title = (d.title ?? d.name ?? \"\") as string;\n\n const content = isVideo\n ? {\n type: \"VideoWidget\",\n props: {\n src: ((d.videoUrl ?? d.video_url) as string) || \"\",\n poster: imageUrl,\n caption: title,\n },\n }\n : {\n type: \"ImageWidget\",\n props: {\n src: imageUrl,\n alt: title || \"Slide image\",\n objectFit: \"cover\",\n },\n };\n\n const baseSlide = {\n id: String(d.id ?? `slide-${index}`),\n content,\n title,\n description: extractDescription(d),\n };\n\n return {\n ...baseSlide,\n ...widgetConfig,\n };\n });\n};\n\n/**\n * All widget transformers bundled for registration\n */\nexport const WIDGET_TRANSFORMERS: Record<string, DataTransformer> = {\n toShareableProps,\n toCarouselSlidesFromShareables,\n toImagePropsFromShareable,\n toVideoPropsFromShareable,\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformers.d.cts","names":[],"sources":["../../src/data-sources/transformers.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"transformers.d.cts","names":[],"sources":["../../src/data-sources/transformers.ts"],"mappings":";;;;;;cAyNa,mBAAA,EAAqB,MAAA,SAAe,eAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformers.d.mts","names":[],"sources":["../../src/data-sources/transformers.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"transformers.d.mts","names":[],"sources":["../../src/data-sources/transformers.ts"],"mappings":";;;;;;cAyNa,mBAAA,EAAqB,MAAA,SAAe,eAAA"}
|
|
@@ -82,16 +82,18 @@ const toVideoPropsFromShareable = (data) => {
|
|
|
82
82
|
*/
|
|
83
83
|
const toShareableProps = (data, source) => {
|
|
84
84
|
if (!Array.isArray(data)) return [];
|
|
85
|
+
const sourceTypeHint = source?.type === "api" ? source.variables?.shareable_type : void 0;
|
|
85
86
|
return data.map((item) => {
|
|
86
87
|
const d = item;
|
|
87
88
|
const widgetConfig = extractCustomWidgetConfig(d, source);
|
|
89
|
+
const shareableType = d.type ?? d.relateable_type ?? d.shareable_type ?? d.shareableType ?? sourceTypeHint ?? "";
|
|
88
90
|
return {
|
|
89
91
|
id: d.id,
|
|
90
92
|
title: d.title ?? d.name ?? "",
|
|
91
93
|
description: extractDescription(d),
|
|
92
94
|
imageUrl: extractImageUrl(d),
|
|
93
95
|
videoUrl: (d.video_url ?? d.videoUrl) || null,
|
|
94
|
-
shareableType
|
|
96
|
+
shareableType,
|
|
95
97
|
kind: d.kind ?? "image",
|
|
96
98
|
status: d.status ?? "active",
|
|
97
99
|
wholesalePrice: d.display_wholesale_price ?? d.wholesale_price ?? null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformers.mjs","names":[],"sources":["../../src/data-sources/transformers.ts"],"sourcesContent":["/**\n * Widget Transformers\n *\n * Transform functions that map API response data to widget-specific prop shapes.\n * Each widget has one transformer that handles all data structure variants:\n * - Standard: Direct field name match\n * - Legacy: Different field names that need mapping\n * - Minimal: Bare minimum fields that need defaults\n *\n * Usage:\n * 1. Transformers are registered in the DataSourceRegistry\n * 2. Reference by name in ApiDataSource.transform\n * 3. Applied after resultPath extraction, before targetProps assignment\n */\n\nimport type { DataTransformer, DataSource } from \"./types\";\n\n/**\n * Helper to extract image URL from various API response structures.\n * Handles:\n * - Flat fields (Medium, Page, Product): image_url, imageUrl, thumbnail_url, src\n * - Nested images array (EnrollmentPack): images[0].image_url\n * - Nested library_items (Library): library_items[0].relateable.image_url\n */\nfunction extractImageUrl(d: Record<string, unknown>): string {\n // Try flat fields first (Medium, Page, Product)\n if (d.image_url) return d.image_url as string;\n if (d.imageUrl) return d.imageUrl as string;\n if (d.thumbnail_url) return d.thumbnail_url as string;\n if (d.src) return d.src as string;\n\n // Try nested images array (EnrollmentPack)\n const images = d.images as Array<Record<string, unknown>> | undefined;\n if (images && images.length > 0) {\n const firstImage = images[0];\n if (firstImage?.image_url) return firstImage.image_url as string;\n if (firstImage?.url) return firstImage.url as string;\n }\n\n // Try nested library_items (Library/Playlist)\n const libraryItems = d.library_items as\n | Array<Record<string, unknown>>\n | undefined;\n if (libraryItems && libraryItems.length > 0) {\n const firstItem = libraryItems[0];\n const relateable = firstItem?.relateable as\n | Record<string, unknown>\n | undefined;\n if (relateable?.image_url) return relateable.image_url as string;\n if (relateable?.imageUrl) return relateable.imageUrl as string;\n if (relateable?.thumbnail_url) return relateable.thumbnail_url as string;\n }\n\n return \"\";\n}\n\n/**\n * Helper to extract description from various API response structures.\n * Handles different field names across endpoints.\n */\nfunction extractDescription(d: Record<string, unknown>): string {\n // Try various description field names\n const desc =\n d.description ?? d.stripped ?? d.stripped_description ?? d.body ?? \"\";\n\n // Handle nested description object (some APIs return { body: \"...\" })\n if (typeof desc === \"object\" && desc !== null && \"body\" in desc) {\n return ((desc as Record<string, unknown>).body as string) ?? \"\";\n }\n\n return desc as string;\n}\n\n/**\n * Helper to extract custom widget config from a selected item.\n */\nfunction extractCustomWidgetConfig(\n d: Record<string, unknown>,\n source: DataSource | undefined,\n): Record<string, unknown> {\n if (source === undefined || source.type !== \"custom\") return {};\n\n const selectedItem = source.selectedItems.find(\n (s) => String(s.id) === String(d.id),\n );\n return selectedItem?.widgetConfig ?? {};\n}\n\n/**\n * ImageWidget transformer from shareable data\n */\nconst toImagePropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", alt: \"Image\", objectFit: \"cover\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: extractImageUrl(d),\n alt: (d.title ?? d.name ?? d.alt ?? \"Image\") as string,\n objectFit: \"cover\" as const,\n };\n};\n\n/**\n * VideoWidget transformer from shareable data\n */\nconst toVideoPropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", poster: \"\", caption: \"\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: ((d.video_url ?? d.videoUrl ?? d.src) as string) || \"\",\n poster: extractImageUrl(d),\n caption: (d.title ?? d.name ?? d.caption ?? \"\") as string,\n };\n};\n\n/**\n * Shareable content transformer\n * Normalizes shareable API responses to a consistent format\n */\nconst toShareableProps: DataTransformer = (data, source) => {\n if (!Array.isArray(data)) return [];\n\n return data.map((item: unknown) => {\n const d = item as Record<string, unknown>;\n\n const widgetConfig = extractCustomWidgetConfig(d, source);\n\n return {\n id: d.id,\n title: (d.title ?? d.name ?? \"\") as string,\n description: extractDescription(d),\n imageUrl: extractImageUrl(d),\n videoUrl: ((d.video_url ?? d.videoUrl) as string) || null,\n shareableType
|
|
1
|
+
{"version":3,"file":"transformers.mjs","names":[],"sources":["../../src/data-sources/transformers.ts"],"sourcesContent":["/**\n * Widget Transformers\n *\n * Transform functions that map API response data to widget-specific prop shapes.\n * Each widget has one transformer that handles all data structure variants:\n * - Standard: Direct field name match\n * - Legacy: Different field names that need mapping\n * - Minimal: Bare minimum fields that need defaults\n *\n * Usage:\n * 1. Transformers are registered in the DataSourceRegistry\n * 2. Reference by name in ApiDataSource.transform\n * 3. Applied after resultPath extraction, before targetProps assignment\n */\n\nimport type { DataTransformer, DataSource } from \"./types\";\n\n/**\n * Helper to extract image URL from various API response structures.\n * Handles:\n * - Flat fields (Medium, Page, Product): image_url, imageUrl, thumbnail_url, src\n * - Nested images array (EnrollmentPack): images[0].image_url\n * - Nested library_items (Library): library_items[0].relateable.image_url\n */\nfunction extractImageUrl(d: Record<string, unknown>): string {\n // Try flat fields first (Medium, Page, Product)\n if (d.image_url) return d.image_url as string;\n if (d.imageUrl) return d.imageUrl as string;\n if (d.thumbnail_url) return d.thumbnail_url as string;\n if (d.src) return d.src as string;\n\n // Try nested images array (EnrollmentPack)\n const images = d.images as Array<Record<string, unknown>> | undefined;\n if (images && images.length > 0) {\n const firstImage = images[0];\n if (firstImage?.image_url) return firstImage.image_url as string;\n if (firstImage?.url) return firstImage.url as string;\n }\n\n // Try nested library_items (Library/Playlist)\n const libraryItems = d.library_items as\n | Array<Record<string, unknown>>\n | undefined;\n if (libraryItems && libraryItems.length > 0) {\n const firstItem = libraryItems[0];\n const relateable = firstItem?.relateable as\n | Record<string, unknown>\n | undefined;\n if (relateable?.image_url) return relateable.image_url as string;\n if (relateable?.imageUrl) return relateable.imageUrl as string;\n if (relateable?.thumbnail_url) return relateable.thumbnail_url as string;\n }\n\n return \"\";\n}\n\n/**\n * Helper to extract description from various API response structures.\n * Handles different field names across endpoints.\n */\nfunction extractDescription(d: Record<string, unknown>): string {\n // Try various description field names\n const desc =\n d.description ?? d.stripped ?? d.stripped_description ?? d.body ?? \"\";\n\n // Handle nested description object (some APIs return { body: \"...\" })\n if (typeof desc === \"object\" && desc !== null && \"body\" in desc) {\n return ((desc as Record<string, unknown>).body as string) ?? \"\";\n }\n\n return desc as string;\n}\n\n/**\n * Helper to extract custom widget config from a selected item.\n */\nfunction extractCustomWidgetConfig(\n d: Record<string, unknown>,\n source: DataSource | undefined,\n): Record<string, unknown> {\n if (source === undefined || source.type !== \"custom\") return {};\n\n const selectedItem = source.selectedItems.find(\n (s) => String(s.id) === String(d.id),\n );\n return selectedItem?.widgetConfig ?? {};\n}\n\n/**\n * ImageWidget transformer from shareable data\n */\nconst toImagePropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", alt: \"Image\", objectFit: \"cover\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: extractImageUrl(d),\n alt: (d.title ?? d.name ?? d.alt ?? \"Image\") as string,\n objectFit: \"cover\" as const,\n };\n};\n\n/**\n * VideoWidget transformer from shareable data\n */\nconst toVideoPropsFromShareable: DataTransformer = (data) => {\n const item = Array.isArray(data) ? data[0] : data;\n if (!item || typeof item !== \"object\") {\n return { src: \"\", poster: \"\", caption: \"\" };\n }\n\n const d = item as Record<string, unknown>;\n return {\n src: ((d.video_url ?? d.videoUrl ?? d.src) as string) || \"\",\n poster: extractImageUrl(d),\n caption: (d.title ?? d.name ?? d.caption ?? \"\") as string,\n };\n};\n\n/**\n * Shareable content transformer\n * Normalizes shareable API responses to a consistent format\n */\nconst toShareableProps: DataTransformer = (data, source) => {\n if (!Array.isArray(data)) return [];\n\n // Derive a fallback shareable type from the data source configuration.\n // API presets pass shareable_type in their variables (e.g., \"products\").\n // Custom sources store it per-item (handled below).\n const sourceTypeHint =\n source?.type === \"api\" ? source.variables?.shareable_type : undefined;\n\n return data.map((item: unknown) => {\n const d = item as Record<string, unknown>;\n\n const widgetConfig = extractCustomWidgetConfig(d, source);\n\n // Resolve shareable type from the item itself, then fall back to source hint\n const shareableType = (d.type ??\n d.relateable_type ??\n d.shareable_type ??\n d.shareableType ??\n sourceTypeHint ??\n \"\") as string;\n\n return {\n id: d.id,\n title: (d.title ?? d.name ?? \"\") as string,\n description: extractDescription(d),\n imageUrl: extractImageUrl(d),\n videoUrl: ((d.video_url ?? d.videoUrl) as string) || null,\n shareableType,\n kind: (d.kind ?? \"image\") as string,\n status: (d.status ?? \"active\") as string,\n wholesalePrice: d.display_wholesale_price ?? d.wholesale_price ?? null,\n subscriptionPrice: d.subscription_price ?? null,\n outOfStock: d.out_of_stock ?? false,\n lowInStock: d.low_in_stock ?? false,\n ...d,\n ...widgetConfig,\n };\n });\n};\n\n/**\n * Carousel slides from shareables transformer\n */\nconst toCarouselSlidesFromShareables: DataTransformer = (data, source) => {\n if (!Array.isArray(data)) return [];\n\n return data.map((item: unknown, index: number) => {\n const d = item as Record<string, unknown>;\n\n const widgetConfig = extractCustomWidgetConfig(d, source);\n\n const imageUrl = extractImageUrl(d);\n const isVideo = d.kind === \"video\" || d.videoUrl || d.video_url;\n const title = (d.title ?? d.name ?? \"\") as string;\n\n const content = isVideo\n ? {\n type: \"VideoWidget\",\n props: {\n src: ((d.videoUrl ?? d.video_url) as string) || \"\",\n poster: imageUrl,\n caption: title,\n },\n }\n : {\n type: \"ImageWidget\",\n props: {\n src: imageUrl,\n alt: title || \"Slide image\",\n objectFit: \"cover\",\n },\n };\n\n const baseSlide = {\n id: String(d.id ?? `slide-${index}`),\n content,\n title,\n description: extractDescription(d),\n };\n\n return {\n ...baseSlide,\n ...widgetConfig,\n };\n });\n};\n\n/**\n * All widget transformers bundled for registration\n */\nexport const WIDGET_TRANSFORMERS: Record<string, DataTransformer> = {\n toShareableProps,\n toCarouselSlidesFromShareables,\n toImagePropsFromShareable,\n toVideoPropsFromShareable,\n};\n"],"mappings":"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"}
|
|
@@ -5,7 +5,9 @@ let react = require("react");
|
|
|
5
5
|
let _tanstack_react_query = require("@tanstack/react-query");
|
|
6
6
|
//#region src/data-sources/use-widget-data.ts
|
|
7
7
|
/**
|
|
8
|
-
* Generates a
|
|
8
|
+
* Generates a cache key for the raw data fetch (endpoint/items identity only).
|
|
9
|
+
* Transforms and targetProps are NOT included — they are applied per-widget
|
|
10
|
+
* after reading from the shared cache.
|
|
9
11
|
*/
|
|
10
12
|
function getSourceKey(source) {
|
|
11
13
|
if (source.type === "api") {
|
|
@@ -19,20 +21,27 @@ function getSourceKey(source) {
|
|
|
19
21
|
return "unknown";
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
|
-
* Resolves
|
|
23
|
-
*
|
|
24
|
+
* Resolves cached raw data into widget props by applying each widget's own
|
|
25
|
+
* transforms and targetProps mapping.
|
|
24
26
|
*/
|
|
25
|
-
function resolvePropsFromQueries(queryResults, sources, errorConfig) {
|
|
27
|
+
function resolvePropsFromQueries(queryResults, sources, registry, errorConfig) {
|
|
26
28
|
if (sources.length === 0) return void 0;
|
|
27
29
|
if (queryResults.some((q) => q.isLoading && !q.data)) return void 0;
|
|
28
30
|
const hasError = !!queryResults.find((q) => q.error)?.error;
|
|
29
31
|
if (hasError && errorConfig?.fallback) return errorConfig.fallback;
|
|
30
32
|
if (hasError) return;
|
|
31
33
|
const resolvedProps = {};
|
|
32
|
-
for (
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (!source
|
|
34
|
+
for (let i = 0; i < queryResults.length; i++) {
|
|
35
|
+
const query = queryResults[i];
|
|
36
|
+
const source = sources[i];
|
|
37
|
+
if (!query?.data || !source) continue;
|
|
38
|
+
if (!source.targetProps) continue;
|
|
39
|
+
let result = query.data;
|
|
40
|
+
if (source.transform) {
|
|
41
|
+
const transformer = registry.transformers[source.transform];
|
|
42
|
+
if (transformer) result = transformer(result, source);
|
|
43
|
+
else console.warn(`Transform "${source.transform}" not found in registry`);
|
|
44
|
+
}
|
|
36
45
|
if (result !== null && typeof result === "object" && !Array.isArray(result) && source.targetProps.length > 1) {
|
|
37
46
|
const resultObj = result;
|
|
38
47
|
for (const prop of source.targetProps) if (prop in resultObj) resolvedProps[prop] = resultObj[prop];
|
|
@@ -42,6 +51,10 @@ function resolvePropsFromQueries(queryResults, sources, errorConfig) {
|
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Hook that fetches and resolves data sources for a widget using React Query.
|
|
54
|
+
*
|
|
55
|
+
* Raw API data is cached and shared across widgets with the same data source.
|
|
56
|
+
* Transforms and targetProps mapping are applied per-widget after cache reads,
|
|
57
|
+
* so multiple widgets can share one fetch but resolve data independently.
|
|
45
58
|
*/
|
|
46
59
|
function useWidgetData(widget, options) {
|
|
47
60
|
const config = require_data_sources_registry_context.useDataSourceRegistryConfig();
|
|
@@ -72,16 +85,7 @@ function useWidgetData(widget, options) {
|
|
|
72
85
|
};
|
|
73
86
|
const fetcher = registry.fetchers[source.type];
|
|
74
87
|
if (!fetcher) throw new Error(`No fetcher registered for source type: ${source.type}`);
|
|
75
|
-
|
|
76
|
-
if (source.transform) {
|
|
77
|
-
const transformer = registry.transformers[source.transform];
|
|
78
|
-
if (transformer) result = transformer(result, source);
|
|
79
|
-
else console.warn(`Transform "${source.transform}" not found in registry`);
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
source,
|
|
83
|
-
result
|
|
84
|
-
};
|
|
88
|
+
return fetcher(source, context);
|
|
85
89
|
},
|
|
86
90
|
enabled: sources.length > 0,
|
|
87
91
|
retry: errorConfig?.retryCount ?? 0,
|
|
@@ -92,12 +96,22 @@ function useWidgetData(widget, options) {
|
|
|
92
96
|
const isLoading = results.some((q) => q.isLoading);
|
|
93
97
|
const failedQuery = results.find((q) => q.error);
|
|
94
98
|
const error = failedQuery?.error instanceof Error ? failedQuery.error : failedQuery?.error ? new Error(String(failedQuery.error)) : null;
|
|
99
|
+
const queryFingerprint = results.map((r) => `${r.dataUpdatedAt}:${r.isLoading}`).join(",");
|
|
100
|
+
const resultsRef = (0, react.useRef)(results);
|
|
101
|
+
resultsRef.current = results;
|
|
95
102
|
return {
|
|
96
|
-
data:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
data: (0, react.useMemo)(() => {
|
|
104
|
+
return resolvePropsFromQueries(resultsRef.current.map((q) => ({
|
|
105
|
+
data: q.data,
|
|
106
|
+
isLoading: q.isLoading,
|
|
107
|
+
error: q.error instanceof Error ? q.error : null
|
|
108
|
+
})), sources, registry, errorConfig);
|
|
109
|
+
}, [
|
|
110
|
+
queryFingerprint,
|
|
111
|
+
sources,
|
|
112
|
+
registry,
|
|
113
|
+
errorConfig
|
|
114
|
+
]),
|
|
101
115
|
isLoading,
|
|
102
116
|
error,
|
|
103
117
|
refetch: (0, react.useCallback)(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-widget-data.cjs","names":["useDataSourceRegistryConfig"],"sources":["../../src/data-sources/use-widget-data.ts"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useCallback, useRef } from \"react\";\nimport type { WidgetSchema } from \"../types/widget-schema\";\nimport type {\n DataSourceRegistry,\n WidgetDataResult,\n DataSourceContext,\n DataSource,\n} from \"./types\";\nimport { useDataSourceRegistryConfig } from \"./registry-context\";\n\ninterface UseWidgetDataOptions {\n /** Override the default registry from context */\n registry?: DataSourceRegistry | undefined;\n /** Base URL for API calls (e.g., \"https://api.fluid.app/api\") */\n baseUrl?: string | undefined;\n}\n\n
|
|
1
|
+
{"version":3,"file":"use-widget-data.cjs","names":["useDataSourceRegistryConfig"],"sources":["../../src/data-sources/use-widget-data.ts"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useCallback, useMemo, useRef } from \"react\";\nimport type { WidgetSchema } from \"../types/widget-schema\";\nimport type {\n DataSourceRegistry,\n WidgetDataResult,\n DataSourceContext,\n DataSource,\n} from \"./types\";\nimport { useDataSourceRegistryConfig } from \"./registry-context\";\n\ninterface UseWidgetDataOptions {\n /** Override the default registry from context */\n registry?: DataSourceRegistry | undefined;\n /** Base URL for API calls (e.g., \"https://api.fluid.app/api\") */\n baseUrl?: string | undefined;\n}\n\n/**\n * Generates a cache key for the raw data fetch (endpoint/items identity only).\n * Transforms and targetProps are NOT included — they are applied per-widget\n * after reading from the shared cache.\n */\nfunction getSourceKey(source: DataSource): string {\n if (source.type === \"api\") {\n const varsKey = source.variables\n ? `:${JSON.stringify(source.variables)}`\n : \"\";\n return `${source.endpoint}${varsKey}`;\n }\n if (source.type === \"custom\") {\n const itemKeys =\n source.selectedItems?.map((item) => {\n return `${item.shareableType}:${item.id}`;\n }) ?? [];\n return `custom:${itemKeys.join(\",\")}`;\n }\n if (source.type === \"static\") {\n return `static:${source.staticType}:${source.selectedId}`;\n }\n return \"unknown\";\n}\n\n/**\n * Resolves cached raw data into widget props by applying each widget's own\n * transforms and targetProps mapping.\n */\nfunction resolvePropsFromQueries<T>(\n queryResults: ReadonlyArray<{\n data?: unknown;\n isLoading: boolean;\n error: Error | null;\n }>,\n sources: DataSource[],\n registry: DataSourceRegistry,\n errorConfig?: { fallback?: unknown },\n): T | undefined {\n if (sources.length === 0) return undefined;\n if (queryResults.some((q) => q.isLoading && !q.data)) return undefined;\n\n const failedQuery = queryResults.find((q) => q.error);\n const hasError = !!failedQuery?.error;\n\n if (hasError && errorConfig?.fallback) {\n return errorConfig.fallback as T;\n }\n\n if (hasError) {\n return undefined;\n }\n\n const resolvedProps: Record<string, unknown> = {};\n\n for (let i = 0; i < queryResults.length; i++) {\n const query = queryResults[i];\n const source = sources[i];\n if (!query?.data || !source) continue;\n\n // Sentry fix: FLUID-ADMIN-1DH — source or targetProps can be undefined from malformed widget config\n if (!source.targetProps) continue;\n\n // Apply this widget's transform to the cached raw data\n let result: unknown = query.data;\n if (source.transform) {\n const transformer = registry.transformers[source.transform];\n if (transformer) {\n result = transformer(result, source);\n } else {\n console.warn(`Transform \"${source.transform}\" not found in registry`);\n }\n }\n\n // Map to this widget's targetProps\n if (\n result !== null &&\n typeof result === \"object\" &&\n !Array.isArray(result) &&\n source.targetProps.length > 1\n ) {\n const resultObj = result as Record<string, unknown>;\n for (const prop of source.targetProps) {\n if (prop in resultObj) {\n resolvedProps[prop] = resultObj[prop];\n }\n }\n } else {\n for (const prop of source.targetProps) {\n resolvedProps[prop] = result;\n }\n }\n }\n\n return resolvedProps as T;\n}\n\n/**\n * Hook that fetches and resolves data sources for a widget using React Query.\n *\n * Raw API data is cached and shared across widgets with the same data source.\n * Transforms and targetProps mapping are applied per-widget after cache reads,\n * so multiple widgets can share one fetch but resolve data independently.\n */\nexport function useWidgetData<T = Record<string, unknown>>(\n widget: WidgetSchema,\n options?: UseWidgetDataOptions,\n): WidgetDataResult<T> {\n const config = useDataSourceRegistryConfig();\n const registry = options?.registry ?? config.registry;\n const baseUrl = options?.baseUrl ?? config.baseUrl;\n const getApiHeaders = config.getApiHeaders;\n const variables = config.variables;\n\n const sources = widget.dataSource?.sources ?? [];\n const errorConfig = widget.dataSource?.error;\n const widgetId = widget.id ?? \"unknown\";\n const widgetType = widget.type;\n\n const queryResultsRef = useRef<Array<{ refetch: () => void }>>([]);\n\n // Cache only raw fetched data — no transforms or source objects stored\n const results = useQueries({\n queries: sources.map((source: DataSource) => ({\n queryKey: [\n \"portal-widget-data\",\n getSourceKey(source),\n baseUrl,\n variables,\n ] as const,\n queryFn: async ({\n signal,\n }: {\n signal: AbortSignal;\n }): Promise<unknown> => {\n const context: DataSourceContext = {\n widgetId,\n widgetType,\n signal,\n baseUrl,\n getApiHeaders,\n variables,\n };\n\n const fetcher = registry.fetchers[source.type];\n if (!fetcher) {\n throw new Error(\n `No fetcher registered for source type: ${source.type}`,\n );\n }\n\n return fetcher(source, context);\n },\n enabled: sources.length > 0,\n retry: errorConfig?.retryCount ?? 0,\n retryDelay: errorConfig?.retryDelay ?? 1000,\n refetchInterval: source.refreshInterval || false,\n })),\n });\n\n // Update ref for refetch callback\n queryResultsRef.current = results;\n\n // Aggregate loading state\n const isLoading = results.some((q) => q.isLoading);\n\n // Get first error from any failed query\n const failedQuery = results.find((q) => q.error);\n const error =\n failedQuery?.error instanceof Error\n ? failedQuery.error\n : failedQuery?.error\n ? new Error(String(failedQuery.error))\n : null;\n\n // Stable fingerprint that changes only when query data or loading states change\n const queryFingerprint = results\n .map((r) => `${r.dataUpdatedAt}:${r.isLoading}`)\n .join(\",\");\n\n // Resolve data: apply per-widget transforms and map to targetProps\n const resultsRef = useRef(results);\n resultsRef.current = results;\n\n const data = useMemo(() => {\n const querySnapshots = resultsRef.current.map((q) => ({\n data: q.data,\n isLoading: q.isLoading,\n error: q.error instanceof Error ? q.error : null,\n }));\n return resolvePropsFromQueries<T>(\n querySnapshots,\n sources,\n registry,\n errorConfig,\n );\n }, [queryFingerprint, sources, registry, errorConfig]);\n\n // Stable refetch callback using ref\n const refetch = useCallback(() => {\n queryResultsRef.current.forEach((q) => q.refetch());\n }, []);\n\n return {\n data,\n isLoading,\n error,\n refetch,\n };\n}\n"],"mappings":"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"}
|
|
@@ -10,6 +10,10 @@ interface UseWidgetDataOptions {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Hook that fetches and resolves data sources for a widget using React Query.
|
|
13
|
+
*
|
|
14
|
+
* Raw API data is cached and shared across widgets with the same data source.
|
|
15
|
+
* Transforms and targetProps mapping are applied per-widget after cache reads,
|
|
16
|
+
* so multiple widgets can share one fetch but resolve data independently.
|
|
13
17
|
*/
|
|
14
18
|
declare function useWidgetData<T = Record<string, unknown>>(widget: WidgetSchema, options?: UseWidgetDataOptions): WidgetDataResult<T>;
|
|
15
19
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-widget-data.d.cts","names":[],"sources":["../../src/data-sources/use-widget-data.ts"],"mappings":";;;;UAWU,oBAAA;;EAER,QAAA,GAAW,kBAAA;EAFH;EAIR,OAAA;AAAA
|
|
1
|
+
{"version":3,"file":"use-widget-data.d.cts","names":[],"sources":["../../src/data-sources/use-widget-data.ts"],"mappings":";;;;UAWU,oBAAA;;EAER,QAAA,GAAW,kBAAA;EAFH;EAIR,OAAA;AAAA;;;;;;AA2GF;;iBAAgB,aAAA,KAAkB,MAAA,kBAAA,CAChC,MAAA,EAAQ,YAAA,EACR,OAAA,GAAU,oBAAA,GACT,gBAAA,CAAiB,CAAA"}
|
|
@@ -10,6 +10,10 @@ interface UseWidgetDataOptions {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Hook that fetches and resolves data sources for a widget using React Query.
|
|
13
|
+
*
|
|
14
|
+
* Raw API data is cached and shared across widgets with the same data source.
|
|
15
|
+
* Transforms and targetProps mapping are applied per-widget after cache reads,
|
|
16
|
+
* so multiple widgets can share one fetch but resolve data independently.
|
|
13
17
|
*/
|
|
14
18
|
declare function useWidgetData<T = Record<string, unknown>>(widget: WidgetSchema, options?: UseWidgetDataOptions): WidgetDataResult<T>;
|
|
15
19
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-widget-data.d.mts","names":[],"sources":["../../src/data-sources/use-widget-data.ts"],"mappings":";;;;UAWU,oBAAA;;EAER,QAAA,GAAW,kBAAA;EAFH;EAIR,OAAA;AAAA
|
|
1
|
+
{"version":3,"file":"use-widget-data.d.mts","names":[],"sources":["../../src/data-sources/use-widget-data.ts"],"mappings":";;;;UAWU,oBAAA;;EAER,QAAA,GAAW,kBAAA;EAFH;EAIR,OAAA;AAAA;;;;;;AA2GF;;iBAAgB,aAAA,KAAkB,MAAA,kBAAA,CAChC,MAAA,EAAQ,YAAA,EACR,OAAA,GAAU,oBAAA,GACT,gBAAA,CAAiB,CAAA"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useDataSourceRegistryConfig } from "./registry-context.mjs";
|
|
2
|
-
import { useCallback, useRef } from "react";
|
|
2
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
3
3
|
import { useQueries } from "@tanstack/react-query";
|
|
4
4
|
//#region src/data-sources/use-widget-data.ts
|
|
5
5
|
/**
|
|
6
|
-
* Generates a
|
|
6
|
+
* Generates a cache key for the raw data fetch (endpoint/items identity only).
|
|
7
|
+
* Transforms and targetProps are NOT included — they are applied per-widget
|
|
8
|
+
* after reading from the shared cache.
|
|
7
9
|
*/
|
|
8
10
|
function getSourceKey(source) {
|
|
9
11
|
if (source.type === "api") {
|
|
@@ -17,20 +19,27 @@ function getSourceKey(source) {
|
|
|
17
19
|
return "unknown";
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
|
-
* Resolves
|
|
21
|
-
*
|
|
22
|
+
* Resolves cached raw data into widget props by applying each widget's own
|
|
23
|
+
* transforms and targetProps mapping.
|
|
22
24
|
*/
|
|
23
|
-
function resolvePropsFromQueries(queryResults, sources, errorConfig) {
|
|
25
|
+
function resolvePropsFromQueries(queryResults, sources, registry, errorConfig) {
|
|
24
26
|
if (sources.length === 0) return void 0;
|
|
25
27
|
if (queryResults.some((q) => q.isLoading && !q.data)) return void 0;
|
|
26
28
|
const hasError = !!queryResults.find((q) => q.error)?.error;
|
|
27
29
|
if (hasError && errorConfig?.fallback) return errorConfig.fallback;
|
|
28
30
|
if (hasError) return;
|
|
29
31
|
const resolvedProps = {};
|
|
30
|
-
for (
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (!source
|
|
32
|
+
for (let i = 0; i < queryResults.length; i++) {
|
|
33
|
+
const query = queryResults[i];
|
|
34
|
+
const source = sources[i];
|
|
35
|
+
if (!query?.data || !source) continue;
|
|
36
|
+
if (!source.targetProps) continue;
|
|
37
|
+
let result = query.data;
|
|
38
|
+
if (source.transform) {
|
|
39
|
+
const transformer = registry.transformers[source.transform];
|
|
40
|
+
if (transformer) result = transformer(result, source);
|
|
41
|
+
else console.warn(`Transform "${source.transform}" not found in registry`);
|
|
42
|
+
}
|
|
34
43
|
if (result !== null && typeof result === "object" && !Array.isArray(result) && source.targetProps.length > 1) {
|
|
35
44
|
const resultObj = result;
|
|
36
45
|
for (const prop of source.targetProps) if (prop in resultObj) resolvedProps[prop] = resultObj[prop];
|
|
@@ -40,6 +49,10 @@ function resolvePropsFromQueries(queryResults, sources, errorConfig) {
|
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
42
51
|
* Hook that fetches and resolves data sources for a widget using React Query.
|
|
52
|
+
*
|
|
53
|
+
* Raw API data is cached and shared across widgets with the same data source.
|
|
54
|
+
* Transforms and targetProps mapping are applied per-widget after cache reads,
|
|
55
|
+
* so multiple widgets can share one fetch but resolve data independently.
|
|
43
56
|
*/
|
|
44
57
|
function useWidgetData(widget, options) {
|
|
45
58
|
const config = useDataSourceRegistryConfig();
|
|
@@ -70,16 +83,7 @@ function useWidgetData(widget, options) {
|
|
|
70
83
|
};
|
|
71
84
|
const fetcher = registry.fetchers[source.type];
|
|
72
85
|
if (!fetcher) throw new Error(`No fetcher registered for source type: ${source.type}`);
|
|
73
|
-
|
|
74
|
-
if (source.transform) {
|
|
75
|
-
const transformer = registry.transformers[source.transform];
|
|
76
|
-
if (transformer) result = transformer(result, source);
|
|
77
|
-
else console.warn(`Transform "${source.transform}" not found in registry`);
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
source,
|
|
81
|
-
result
|
|
82
|
-
};
|
|
86
|
+
return fetcher(source, context);
|
|
83
87
|
},
|
|
84
88
|
enabled: sources.length > 0,
|
|
85
89
|
retry: errorConfig?.retryCount ?? 0,
|
|
@@ -90,12 +94,22 @@ function useWidgetData(widget, options) {
|
|
|
90
94
|
const isLoading = results.some((q) => q.isLoading);
|
|
91
95
|
const failedQuery = results.find((q) => q.error);
|
|
92
96
|
const error = failedQuery?.error instanceof Error ? failedQuery.error : failedQuery?.error ? new Error(String(failedQuery.error)) : null;
|
|
97
|
+
const queryFingerprint = results.map((r) => `${r.dataUpdatedAt}:${r.isLoading}`).join(",");
|
|
98
|
+
const resultsRef = useRef(results);
|
|
99
|
+
resultsRef.current = results;
|
|
93
100
|
return {
|
|
94
|
-
data:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
data: useMemo(() => {
|
|
102
|
+
return resolvePropsFromQueries(resultsRef.current.map((q) => ({
|
|
103
|
+
data: q.data,
|
|
104
|
+
isLoading: q.isLoading,
|
|
105
|
+
error: q.error instanceof Error ? q.error : null
|
|
106
|
+
})), sources, registry, errorConfig);
|
|
107
|
+
}, [
|
|
108
|
+
queryFingerprint,
|
|
109
|
+
sources,
|
|
110
|
+
registry,
|
|
111
|
+
errorConfig
|
|
112
|
+
]),
|
|
99
113
|
isLoading,
|
|
100
114
|
error,
|
|
101
115
|
refetch: useCallback(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-widget-data.mjs","names":[],"sources":["../../src/data-sources/use-widget-data.ts"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useCallback, useRef } from \"react\";\nimport type { WidgetSchema } from \"../types/widget-schema\";\nimport type {\n DataSourceRegistry,\n WidgetDataResult,\n DataSourceContext,\n DataSource,\n} from \"./types\";\nimport { useDataSourceRegistryConfig } from \"./registry-context\";\n\ninterface UseWidgetDataOptions {\n /** Override the default registry from context */\n registry?: DataSourceRegistry | undefined;\n /** Base URL for API calls (e.g., \"https://api.fluid.app/api\") */\n baseUrl?: string | undefined;\n}\n\n
|
|
1
|
+
{"version":3,"file":"use-widget-data.mjs","names":[],"sources":["../../src/data-sources/use-widget-data.ts"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useCallback, useMemo, useRef } from \"react\";\nimport type { WidgetSchema } from \"../types/widget-schema\";\nimport type {\n DataSourceRegistry,\n WidgetDataResult,\n DataSourceContext,\n DataSource,\n} from \"./types\";\nimport { useDataSourceRegistryConfig } from \"./registry-context\";\n\ninterface UseWidgetDataOptions {\n /** Override the default registry from context */\n registry?: DataSourceRegistry | undefined;\n /** Base URL for API calls (e.g., \"https://api.fluid.app/api\") */\n baseUrl?: string | undefined;\n}\n\n/**\n * Generates a cache key for the raw data fetch (endpoint/items identity only).\n * Transforms and targetProps are NOT included — they are applied per-widget\n * after reading from the shared cache.\n */\nfunction getSourceKey(source: DataSource): string {\n if (source.type === \"api\") {\n const varsKey = source.variables\n ? `:${JSON.stringify(source.variables)}`\n : \"\";\n return `${source.endpoint}${varsKey}`;\n }\n if (source.type === \"custom\") {\n const itemKeys =\n source.selectedItems?.map((item) => {\n return `${item.shareableType}:${item.id}`;\n }) ?? [];\n return `custom:${itemKeys.join(\",\")}`;\n }\n if (source.type === \"static\") {\n return `static:${source.staticType}:${source.selectedId}`;\n }\n return \"unknown\";\n}\n\n/**\n * Resolves cached raw data into widget props by applying each widget's own\n * transforms and targetProps mapping.\n */\nfunction resolvePropsFromQueries<T>(\n queryResults: ReadonlyArray<{\n data?: unknown;\n isLoading: boolean;\n error: Error | null;\n }>,\n sources: DataSource[],\n registry: DataSourceRegistry,\n errorConfig?: { fallback?: unknown },\n): T | undefined {\n if (sources.length === 0) return undefined;\n if (queryResults.some((q) => q.isLoading && !q.data)) return undefined;\n\n const failedQuery = queryResults.find((q) => q.error);\n const hasError = !!failedQuery?.error;\n\n if (hasError && errorConfig?.fallback) {\n return errorConfig.fallback as T;\n }\n\n if (hasError) {\n return undefined;\n }\n\n const resolvedProps: Record<string, unknown> = {};\n\n for (let i = 0; i < queryResults.length; i++) {\n const query = queryResults[i];\n const source = sources[i];\n if (!query?.data || !source) continue;\n\n // Sentry fix: FLUID-ADMIN-1DH — source or targetProps can be undefined from malformed widget config\n if (!source.targetProps) continue;\n\n // Apply this widget's transform to the cached raw data\n let result: unknown = query.data;\n if (source.transform) {\n const transformer = registry.transformers[source.transform];\n if (transformer) {\n result = transformer(result, source);\n } else {\n console.warn(`Transform \"${source.transform}\" not found in registry`);\n }\n }\n\n // Map to this widget's targetProps\n if (\n result !== null &&\n typeof result === \"object\" &&\n !Array.isArray(result) &&\n source.targetProps.length > 1\n ) {\n const resultObj = result as Record<string, unknown>;\n for (const prop of source.targetProps) {\n if (prop in resultObj) {\n resolvedProps[prop] = resultObj[prop];\n }\n }\n } else {\n for (const prop of source.targetProps) {\n resolvedProps[prop] = result;\n }\n }\n }\n\n return resolvedProps as T;\n}\n\n/**\n * Hook that fetches and resolves data sources for a widget using React Query.\n *\n * Raw API data is cached and shared across widgets with the same data source.\n * Transforms and targetProps mapping are applied per-widget after cache reads,\n * so multiple widgets can share one fetch but resolve data independently.\n */\nexport function useWidgetData<T = Record<string, unknown>>(\n widget: WidgetSchema,\n options?: UseWidgetDataOptions,\n): WidgetDataResult<T> {\n const config = useDataSourceRegistryConfig();\n const registry = options?.registry ?? config.registry;\n const baseUrl = options?.baseUrl ?? config.baseUrl;\n const getApiHeaders = config.getApiHeaders;\n const variables = config.variables;\n\n const sources = widget.dataSource?.sources ?? [];\n const errorConfig = widget.dataSource?.error;\n const widgetId = widget.id ?? \"unknown\";\n const widgetType = widget.type;\n\n const queryResultsRef = useRef<Array<{ refetch: () => void }>>([]);\n\n // Cache only raw fetched data — no transforms or source objects stored\n const results = useQueries({\n queries: sources.map((source: DataSource) => ({\n queryKey: [\n \"portal-widget-data\",\n getSourceKey(source),\n baseUrl,\n variables,\n ] as const,\n queryFn: async ({\n signal,\n }: {\n signal: AbortSignal;\n }): Promise<unknown> => {\n const context: DataSourceContext = {\n widgetId,\n widgetType,\n signal,\n baseUrl,\n getApiHeaders,\n variables,\n };\n\n const fetcher = registry.fetchers[source.type];\n if (!fetcher) {\n throw new Error(\n `No fetcher registered for source type: ${source.type}`,\n );\n }\n\n return fetcher(source, context);\n },\n enabled: sources.length > 0,\n retry: errorConfig?.retryCount ?? 0,\n retryDelay: errorConfig?.retryDelay ?? 1000,\n refetchInterval: source.refreshInterval || false,\n })),\n });\n\n // Update ref for refetch callback\n queryResultsRef.current = results;\n\n // Aggregate loading state\n const isLoading = results.some((q) => q.isLoading);\n\n // Get first error from any failed query\n const failedQuery = results.find((q) => q.error);\n const error =\n failedQuery?.error instanceof Error\n ? failedQuery.error\n : failedQuery?.error\n ? new Error(String(failedQuery.error))\n : null;\n\n // Stable fingerprint that changes only when query data or loading states change\n const queryFingerprint = results\n .map((r) => `${r.dataUpdatedAt}:${r.isLoading}`)\n .join(\",\");\n\n // Resolve data: apply per-widget transforms and map to targetProps\n const resultsRef = useRef(results);\n resultsRef.current = results;\n\n const data = useMemo(() => {\n const querySnapshots = resultsRef.current.map((q) => ({\n data: q.data,\n isLoading: q.isLoading,\n error: q.error instanceof Error ? q.error : null,\n }));\n return resolvePropsFromQueries<T>(\n querySnapshots,\n sources,\n registry,\n errorConfig,\n );\n }, [queryFingerprint, sources, registry, errorConfig]);\n\n // Stable refetch callback using ref\n const refetch = useCallback(() => {\n queryResultsRef.current.forEach((q) => q.refetch());\n }, []);\n\n return {\n data,\n isLoading,\n error,\n refetch,\n };\n}\n"],"mappings":"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"}
|