UNPKG

@inspirer-dev/hero-widget-selector

Version:

A custom field plugin for Strapi v5 that provides a widget selector with size filtering capabilities. Perfect for selecting hero widgets from a filtered collection based on size (S, M, L, XL).

642 lines (641 loc) 26.2 kB
import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import { forwardRef, useState, useEffect } from "react"; import { useIntl } from "react-intl"; import { Box, Flex, Field, Popover, Button, Loader, Typography, Divider, SingleSelect, SingleSelectOption, Searchbar } from "@strapi/design-system"; import { Check, CaretDown } from "@strapi/icons"; const WidgetSelectorInput = forwardRef( ({ attribute, name, onChange, value, intlLabel, disabled = false, error, required, placeholder, description, hint }, ref) => { const [widgets, setWidgets] = useState([]); const [heroLayouts, setHeroLayouts] = useState([]); const [filteredLayouts, setFilteredLayouts] = useState([]); const [loading, setLoading] = useState(true); const [loadingLayouts, setLoadingLayouts] = useState(false); const [isOpen, setIsOpen] = useState(false); const [isLayoutSelectorOpen, setIsLayoutSelectorOpen] = useState(false); const [selectedWidget, setSelectedWidget] = useState(null); const [selectedCondition, setSelectedCondition] = useState(""); const [selectedLayout, setSelectedLayout] = useState(null); const [layoutSearchTerm, setLayoutSearchTerm] = useState(""); const { formatMessage } = useIntl(); const filterSize = attribute?.options?.size || "S"; const isFallbackLayout = name.includes("fallbackLayout") || name.includes("fallbackLayouts"); const getCurrentLayoutContext = () => { try { const pathParts = name.split("."); const layoutIndexStr = pathParts.find( (part, index) => pathParts[index - 1] === "defaultLayouts" && !isNaN(parseInt(part)) ); if (layoutIndexStr !== void 0) { const layoutIndex = parseInt(layoutIndexStr); try { const formElements = document.querySelectorAll( 'input[name*="defaultLayouts"][name*="id"]' ); for (const element of formElements) { const inputName = element.getAttribute("name") || ""; if (inputName.includes(`defaultLayouts.${layoutIndex}.`) && inputName.includes("id")) { const layoutId = element.value; if (layoutId && layoutId !== "") { return { layoutId: parseInt(layoutId), layoutIndex }; } } } const currentElement = document.querySelector( `[name*="defaultLayouts.${layoutIndex}"]` ); if (currentElement) { const parentSection = currentElement.closest("[data-layout-id]"); if (parentSection) { const layoutId = parentSection.getAttribute("data-layout-id"); if (layoutId) { return { layoutId: parseInt(layoutId), layoutIndex }; } } } } catch (e) { console.log("Could not extract layout ID from DOM:", e); } return { layoutId: null, layoutIndex }; } return { layoutId: null, layoutIndex: null }; } catch { return { layoutId: null, layoutIndex: null }; } }; const parseValue = (val) => { if (typeof val === "string") { try { return JSON.parse(val); } catch { return { widgetId: val, condition: "", layoutSlug: null, layoutId: null }; } } return val || { widgetId: null, condition: "", layoutSlug: null, layoutId: null }; }; const currentValue = parseValue(value); useEffect(() => { const fetchWidgets = async () => { try { setLoading(true); console.log("fetching widgets", filterSize); const response = await fetch(`/hero-widget-selector/widgets/admin?size=${filterSize}`, { method: "GET", headers: { "Content-Type": "application/json" } }); if (response.ok) { const data = await response.json(); if (data?.results) { setWidgets(data.results); if (currentValue.widgetId) { const selected = data.results.find( (w) => w.documentId === currentValue.widgetId || w.id.toString() === currentValue.widgetId ); setSelectedWidget(selected || null); } } else { console.error("No results in response:", data); setWidgets([]); } } else { console.error("Failed to fetch widgets:", response.status, response.statusText); setWidgets([]); } } catch (error2) { console.error("Failed to fetch widgets:", error2); setWidgets([]); } finally { setLoading(false); } }; fetchWidgets(); }, [filterSize, currentValue.widgetId]); useEffect(() => { if (currentValue.condition) { setSelectedCondition(currentValue.condition); } }, [currentValue.condition]); const fetchHeroLayouts = async (searchTerm = "", omitLayoutSlug = null, omitLayoutId = null, excludeIndex = null) => { try { setLoadingLayouts(true); let url = `/hero-widget-selector/hero-layouts/admin`; const params = new URLSearchParams(); if (searchTerm) { params.append("search", searchTerm); } if (omitLayoutSlug) { params.append("omitLayoutSlug", omitLayoutSlug); } if (omitLayoutId) { params.append("omitLayoutId", omitLayoutId.toString()); } if (excludeIndex !== null && excludeIndex !== void 0) { params.append("excludeIndex", excludeIndex.toString()); } if (isFallbackLayout) { params.append("fallbackOnly", "true"); } if (params.toString()) { url += `?${params.toString()}`; } console.log("Fetching hero layouts with URL:", url); const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" } }); if (response.ok) { const data = await response.json(); if (data?.results) { console.log("data.results", data.results); setHeroLayouts(data.results); setFilteredLayouts(data.results); } else { setHeroLayouts([]); setFilteredLayouts([]); } } else { console.error("Failed to fetch hero layouts:", response.status, response.statusText); setHeroLayouts([]); setFilteredLayouts([]); } } catch (error2) { console.error("Failed to fetch hero layouts:", error2); setHeroLayouts([]); setFilteredLayouts([]); } finally { setLoadingLayouts(false); } }; useEffect(() => { if (selectedCondition && selectedCondition !== "none" && selectedCondition !== "") { const currentContext = getCurrentLayoutContext(); console.log("Fetching layouts with context:", currentContext); fetchHeroLayouts("", null, currentContext.layoutId, currentContext.layoutIndex); } }, [selectedCondition]); useEffect(() => { if (heroLayouts.length > 0) { let selected = null; if (currentValue.layoutSlug) { selected = heroLayouts.find((l) => l.slug === currentValue.layoutSlug); } if (!selected && currentValue.layoutId) { selected = heroLayouts.find( (l) => l.id.toString() === currentValue.layoutId?.toString() ); } setSelectedLayout(selected || null); } else { setSelectedLayout(null); } }, [currentValue.layoutSlug, currentValue.layoutId, heroLayouts]); const handleWidgetSelect = (widget) => { setSelectedWidget(widget); setIsOpen(false); const newValue = { ...currentValue, widgetId: widget ? widget.documentId || widget.id.toString() : null }; onChange({ target: { name, value: JSON.stringify(newValue) } }); }; const handleConditionChange = (condition) => { setSelectedCondition(condition); setSelectedLayout(null); const newValue = { ...currentValue, condition, layoutSlug: null, layoutId: null }; onChange({ target: { name, value: JSON.stringify(newValue) } }); }; const handleLayoutSelect = (layout) => { setSelectedLayout(layout); setIsLayoutSelectorOpen(false); console.log("handleLayoutSelect", layout); const newValue = { ...currentValue, layoutSlug: layout ? layout.slug : null, layoutId: layout ? layout.id : null // Keep for backward compatibility }; onChange({ target: { name, value: JSON.stringify(newValue) } }); }; const handleLayoutSearch = (searchTerm) => { setLayoutSearchTerm(searchTerm); if (searchTerm.trim()) { const filtered = heroLayouts.filter((layout) => { const name2 = layout.name?.toLowerCase() || ""; const displayName = layout.displayName?.toLowerCase() || ""; const term = searchTerm.toLowerCase(); return name2.includes(term) || displayName.includes(term); }); setFilteredLayouts(filtered); } else { setFilteredLayouts(heroLayouts); } }; const getSizesArray = (widget) => { if (widget.sizes && typeof widget.sizes === "object" && widget.sizes.sizes) { return widget.sizes.sizes; } return []; }; const getDisplayName = () => { if (intlLabel?.defaultMessage && !intlLabel.defaultMessage.includes(".")) { return intlLabel.defaultMessage; } if (name) { const parts = name.split("."); const lastPart = parts[parts.length - 1]; return lastPart.replace(/[_-]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" "); } return "Widget Selector"; }; const hasWidget = !!selectedWidget; const hasCondition = !!selectedCondition && selectedCondition !== ""; const hasLayout = !!selectedLayout; const getTickColor = (step) => { if (step === "widget") { return hasWidget ? "#5cb85c" : "#ddd"; } if (step === "condition") { return hasWidget && !hasCondition ? "#ee5a52" : hasWidget && hasCondition ? "#5cb85c" : "#ddd"; } if (step === "layout") { return hasCondition && !hasLayout ? "#ee5a52" : hasCondition && hasLayout ? "#5cb85c" : "#ddd"; } return "#ddd"; }; const renderSelectedWidget = () => { if (selectedWidget) { const sizesArray = getSizesArray(selectedWidget); return /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [ selectedWidget.image?.url ? /* @__PURE__ */ jsx( Box, { borderRadius: "4px", background: "neutral100", borderColor: "neutral200", borderStyle: "solid", borderWidth: "1px", style: { width: 24, height: 24, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx( "img", { src: selectedWidget.image.url ?? "", alt: selectedWidget.title ?? "", style: { width: 24, height: 24, objectFit: "cover" } } ) } ) : /* @__PURE__ */ jsx( Box, { width: 8, height: 8, borderRadius: "4px", background: "neutral100", borderColor: "neutral200", borderStyle: "solid", borderWidth: "1px", style: { display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "📷" }) } ), /* @__PURE__ */ jsxs(Typography, { variant: "omega", textColor: "neutral800", children: [ selectedWidget.title, " (", sizesArray.join(", "), ")" ] }) ] }); } return /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral500", children: placeholder || (loading ? "Loading widgets..." : "Select a widget") }); }; const renderWidgetOption = (widget) => { const sizesArray = getSizesArray(widget); return /* @__PURE__ */ jsx( Box, { paddingTop: 2, paddingBottom: 2, paddingLeft: 3, paddingRight: 3, cursor: "pointer", background: "neutral0", _hover: { background: "neutral100" }, onClick: () => handleWidgetSelect(widget), children: /* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", gap: 3, children: [ widget.image?.url ? /* @__PURE__ */ jsx( Flex, { width: "70px", height: "70px", justifyContent: "center", alignItems: "center", borderRadius: "4px", background: "neutral100", borderColor: "neutral200", borderStyle: "solid", borderWidth: "1px", children: /* @__PURE__ */ jsx( "img", { src: widget.image.url ?? "", alt: widget.title ?? "", style: { width: 70, height: 70, objectFit: "cover" } } ) } ) : /* @__PURE__ */ jsx( Flex, { width: "70px", height: "70px", justifyContent: "center", alignItems: "center", borderRadius: "4px", background: "neutral100", borderColor: "neutral200", borderStyle: "solid", borderWidth: "1px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "📷" }) } ), /* @__PURE__ */ jsxs( Box, { style: { display: "flex", alignItems: "flex-start", justifyContent: "center", flexDirection: "column", gap: 2 }, children: [ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", textColor: "neutral800", children: widget.title }), /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [ "Sizes: ", sizesArray.join(", "), widget.subtitle && ` • ${widget.subtitle}` ] }) ] } ) ] }) }, widget.documentId || widget.id ); }; const renderSelectedLayout = () => { if (selectedLayout) { return /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral800", children: selectedLayout.name || selectedLayout.displayName }); } return /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral500", children: loadingLayouts ? "Loading layouts..." : "Select a layout" }); }; const renderLayoutOption = (layout) => { return /* @__PURE__ */ jsx( Box, { paddingTop: 2, paddingBottom: 2, paddingLeft: 3, paddingRight: 3, cursor: "pointer", background: "neutral0", _hover: { background: "neutral100" }, onClick: () => handleLayoutSelect(layout), children: /* @__PURE__ */ jsx(Flex, { alignItems: "center", justifyContent: "space-between", children: /* @__PURE__ */ jsxs(Box, { children: [ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", textColor: "neutral800", children: layout.name || layout.displayName }), /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [ layout.displayName, " • ", layout.component ] }) ] }) }) }, layout.id ); }; return /* @__PURE__ */ jsxs( Box, { padding: 4, background: "neutral0", borderRadius: "4px", borderColor: "neutral200", borderStyle: "solid", borderWidth: "1px", marginBottom: 4, style: { boxShadow: "0 1px 4px rgba(33, 33, 52, 0.1)" }, children: [ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Check, { width: "16px", height: "16px", style: { color: getTickColor("widget") } }) }), /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxs(Field.Root, { name, error, hint, required, children: [ /* @__PURE__ */ jsx(Field.Label, { action: void 0, children: getDisplayName() }), /* @__PURE__ */ jsxs(Popover.Root, { open: isOpen, onOpenChange: setIsOpen, children: [ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx( Button, { variant: "tertiary", size: "L", disabled: disabled || loading, fullWidth: true, justifyContent: "space-between", ref, children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", justifyContent: "center", gap: 5, children: [ renderSelectedWidget(), loading ? /* @__PURE__ */ jsx(Loader, { size: "S" }) : /* @__PURE__ */ jsx(CaretDown, { width: "12px", height: "12px" }) ] }) } ) }), /* @__PURE__ */ jsx( Popover.Content, { side: "bottom", align: "start", style: { minWidth: "300px", maxHeight: "400px", overflowY: "auto" }, children: /* @__PURE__ */ jsxs(Box, { padding: 1, children: [ /* @__PURE__ */ jsx( Box, { paddingTop: 2, paddingBottom: 2, paddingLeft: 3, paddingRight: 3, cursor: "pointer", background: "neutral0", _hover: { background: "neutral100" }, onClick: () => handleWidgetSelect(null), children: /* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 3, children: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral500", children: "None" }) }) } ), widgets.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Divider, {}), widgets.map(renderWidgetOption) ] }) ] }) } ) ] }), /* @__PURE__ */ jsx(Field.Hint, {}), /* @__PURE__ */ jsx(Field.Error, {}) ] }) }) ] }), !isFallbackLayout && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Box, { marginTop: 4, children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Check, { width: "16px", height: "16px", style: { color: getTickColor("condition") } }) }), /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxs(Field.Root, { name: `${name}.condition`, children: [ /* @__PURE__ */ jsx(Field.Label, { children: "Condition" }), /* @__PURE__ */ jsxs( SingleSelect, { value: selectedCondition, onChange: handleConditionChange, placeholder: "Select condition...", disabled, children: [ /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: "None" }), /* @__PURE__ */ jsx(SingleSelectOption, { value: "welcome_case_opened", children: "Welcome Case Opened" }) ] } ), /* @__PURE__ */ jsx(Field.Hint, {}), /* @__PURE__ */ jsx(Field.Error, {}) ] }) }) ] }) }), selectedCondition && selectedCondition !== "" && /* @__PURE__ */ jsx(Box, { marginTop: 4, children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, children: [ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Check, { width: "16px", height: "16px", style: { color: getTickColor("layout") } }) }), /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxs(Field.Root, { name: `${name}.layout`, children: [ /* @__PURE__ */ jsx(Field.Label, { children: "Conditional Hero Layout" }), /* @__PURE__ */ jsxs( Popover.Root, { open: isLayoutSelectorOpen, onOpenChange: setIsLayoutSelectorOpen, children: [ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx( Button, { variant: "tertiary", size: "L", disabled: disabled || loadingLayouts, fullWidth: true, justifyContent: "space-between", children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", justifyContent: "center", gap: 5, children: [ renderSelectedLayout(), loadingLayouts ? /* @__PURE__ */ jsx(Loader, { size: "S" }) : /* @__PURE__ */ jsx(CaretDown, { width: "12px", height: "12px" }) ] }) } ) }), /* @__PURE__ */ jsx( Popover.Content, { side: "bottom", align: "start", style: { minWidth: "300px", maxHeight: "400px", overflowY: "auto" }, children: /* @__PURE__ */ jsxs(Box, { padding: 2, children: [ /* @__PURE__ */ jsx(Box, { marginBottom: 2, children: /* @__PURE__ */ jsx( Searchbar, { name: "layout-search", placeholder: "Search layouts by name...", value: layoutSearchTerm, onChange: (e) => handleLayoutSearch(e.target.value) } ) }), /* @__PURE__ */ jsx( Box, { paddingTop: 2, paddingBottom: 2, paddingLeft: 3, paddingRight: 3, cursor: "pointer", background: "neutral0", _hover: { background: "neutral100" }, onClick: () => handleLayoutSelect(null), children: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral500", children: "None" }) } ), filteredLayouts.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Divider, {}), filteredLayouts.map(renderLayoutOption) ] }), filteredLayouts.length === 0 && layoutSearchTerm && /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsxs(Typography, { variant: "omega", textColor: "neutral500", children: [ 'No layouts found matching "', layoutSearchTerm, '"' ] }) }) ] }) } ) ] } ), /* @__PURE__ */ jsx(Field.Hint, {}), /* @__PURE__ */ jsx(Field.Error, {}) ] }) }) ] }) }) ] }) ] } ); } ); WidgetSelectorInput.displayName = "WidgetSelectorInput"; export { WidgetSelectorInput, WidgetSelectorInput as default };