@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
JavaScript
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
};