@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).
298 lines (261 loc) • 10 kB
text/typescript
import type { Core } from '@strapi/strapi';
const controller = ({ strapi }: { strapi: Core.Strapi }) => ({
index(ctx) {
ctx.body = strapi
.plugin('hero-widget-selector')
// the name of the service file & the method.
.service('service')
.getWelcomeMessage();
},
async getFilteredWidgets(ctx) {
const { size, sizes } = ctx.query;
try {
let entities;
if (size) {
// Use raw SQL query for JSONB field
const knex = strapi.db.connection;
const results = await knex('widgets')
.select('id', 'document_id', 'title', 'sizes', 'subtitle')
.whereRaw("sizes->>'sizes' LIKE ?", [`%"${size}"%`]);
// Convert raw results to the expected format
entities = results.map((row) => ({
id: row.id,
documentId: row.document_id,
title: row.title,
subtitle: row.subtitle,
sizes: typeof row.sizes === 'string' ? JSON.parse(row.sizes) : row.sizes,
}));
// Get populated data for each entity
const populatedEntities = await Promise.all(
entities.map(async (entity) => {
const fullEntity = await strapi.entityService.findOne('api::widget.widget', entity.id, {
populate: {
image: {
fields: ['id', 'documentId', 'name', 'url', 'alternativeText'],
},
ctaButton: true,
},
});
return {
...entity,
image: fullEntity?.image,
ctaButton: fullEntity?.ctaButton,
};
})
);
entities = populatedEntities;
} else {
// If no size filter, get all widgets
entities = await strapi.entityService.findMany('api::widget.widget', {
fields: ['id', 'documentId', 'title', 'sizes', 'subtitle'],
populate: {
image: {
fields: ['id', 'documentId', 'name', 'url', 'alternativeText'],
},
ctaButton: true,
},
pagination: {
pageSize: 100,
},
});
}
ctx.body = {
results: entities.map((entity: any) => ({
id: entity.id,
documentId: entity.documentId,
title: entity.title,
subtitle: entity.subtitle,
sizes: entity.sizes,
image: entity.image,
ctaButton: entity.ctaButton,
})),
};
} catch (err: any) {
strapi.log.error('Failed to fetch widgets:', err);
ctx.throw(500, `Failed to fetch widgets: ${err.message}`);
}
},
async getHeroLayouts(ctx) {
const { search, omitLayoutId, omitLayoutSlug, excludeIndex, fallbackOnly } = ctx.query;
try {
// Get the hero section with all its layouts
const heroSection = await strapi.entityService.findOne('api::hero-section.hero-section', 1, {
populate: {
defaultLayouts: {
populate: '*',
},
fallbackLayout: {
populate: '*',
},
},
});
if (!heroSection) {
ctx.body = { results: [] };
return;
}
let layouts = [];
// Determine which layouts to use based on fallbackOnly flag
if (fallbackOnly === 'true') {
// Only return fallback layouts
layouts = heroSection.fallbackLayout || [];
console.log('Fetching fallback layouts only:', layouts.length);
} else {
// Return default layouts (excluding fallback layouts to avoid conflicts)
layouts = heroSection.defaultLayouts || [];
console.log('Fetching default layouts:', layouts.length);
// If we have fallback layouts, exclude them from default layouts to avoid duplicates
if (heroSection.fallbackLayout && heroSection.fallbackLayout.length > 0) {
const fallbackComponents = new Set(
heroSection.fallbackLayout.map((layout: any) => layout.__component)
);
const fallbackSlugs = new Set(
heroSection.fallbackLayout.map((layout: any) => layout.slug).filter(Boolean)
);
layouts = layouts.filter((layout: any) => {
const isFallbackComponent = fallbackComponents.has(layout.__component);
const isFallbackSlug = layout.slug && fallbackSlugs.has(layout.slug);
return !isFallbackComponent && !isFallbackSlug;
});
console.log('Filtered out fallback layouts, remaining:', layouts.length);
}
}
// Filter out the current layout if omitLayoutId or omitLayoutSlug is provided
if (omitLayoutId) {
const omitId = omitLayoutId.toString();
layouts = layouts.filter((layout: any) => {
const layoutIdStr = layout.id ? layout.id.toString() : '';
const documentIdStr = layout.documentId ? layout.documentId.toString() : '';
return layoutIdStr !== omitId && documentIdStr !== omitId;
});
console.log(`Filtering out layout ID: ${omitId}, remaining layouts:`, layouts.length);
}
if (omitLayoutSlug) {
layouts = layouts.filter((layout: any) => layout.slug !== omitLayoutSlug);
console.log(
`Filtering out layout slug: ${omitLayoutSlug}, remaining layouts:`,
layouts.length
);
}
// If we have an exclude index but no layout ID, filter by position
if (!omitLayoutId && excludeIndex !== undefined) {
const excludeIdx = parseInt(excludeIndex);
layouts = layouts.filter((layout: any, index: number) => index !== excludeIdx);
console.log(
`Filtering out layout at index: ${excludeIdx}, remaining layouts:`,
layouts.length
);
}
// Filter by search term if provided
if (search && search.trim()) {
const searchTerm = search.toLowerCase().trim();
layouts = layouts.filter((layout: any) => {
const name = layout.name?.toLowerCase() || '';
const displayName = layout.__component?.split('.')[1]?.replace(/-/g, ' ') || '';
return name.includes(searchTerm) || displayName.includes(searchTerm);
});
}
// Format the results
const results = layouts.map((layout: any) => {
// Auto-generate slug fallback if not present
const autoSlug =
layout.slug ||
layout.name
?.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '') ||
layout.__component?.split('.')[1] ||
`layout-${layout.id}`;
console.log('Layout data:', {
id: layout.id,
documentId: layout.documentId,
slug: layout.slug || autoSlug,
name: layout.name,
component: layout.__component,
isFallback: fallbackOnly === 'true',
});
return {
id: layout.id,
documentId: layout.documentId,
slug: layout.slug || autoSlug, // Use actual slug or fallback
name: layout.name || '',
displayName:
layout.__component?.split('.')[1]?.replace(/-/g, ' ').toUpperCase() || 'Unknown Layout',
component: layout.__component,
__component: layout.__component,
isFallback: fallbackOnly === 'true',
};
});
console.log('Returning layout results:', results);
ctx.body = { results };
} catch (err: any) {
strapi.log.error('Failed to fetch hero layouts:', err);
ctx.throw(500, `Failed to fetch hero layouts: ${err.message}`);
}
},
async getPopulatedLayout(ctx) {
const { layoutId, layoutSlug } = ctx.params;
try {
// Get the hero section with all its layouts
const heroSection = await strapi.entityService.findOne('api::hero-section.hero-section', 1, {
populate: {
defaultLayouts: {
populate: {
extraLarge: true,
extraLargeLeft: true,
extraLargeRight: true,
small1: true,
small2: true,
medium: true,
large1: true,
large2: true,
},
},
},
});
if (!heroSection || !heroSection.defaultLayouts) {
ctx.throw(404, 'Hero section not found');
}
// Find the specific layout by slug (preferred) or ID (fallback)
let layout;
if (layoutSlug) {
layout = heroSection.defaultLayouts.find((l: any) => l.slug === layoutSlug);
} else if (layoutId) {
layout = heroSection.defaultLayouts.find((l: any) => l.id.toString() === layoutId);
}
if (!layout) {
ctx.throw(404, 'Layout not found');
}
// Extract widget assignments from each position
const populatedLayout = {
id: layout.id,
name: layout.name,
component: layout.__component,
displayName:
layout.__component?.split('.')[1]?.replace(/-/g, ' ').toUpperCase() || 'Unknown Layout',
widgets: {},
};
// Map widget positions to their assigned widgets
const positions = ['extraLarge', 'extraLargeLeft', 'extraLargeRight', 'small1', 'small2', 'medium', 'large1', 'large2'];
for (const position of positions) {
if (layout[position]) {
// Parse the widget selector value
let widgetData = layout[position];
if (typeof widgetData === 'string') {
try {
widgetData = JSON.parse(widgetData);
} catch (e) {
// If parsing fails, treat as simple widget ID
widgetData = { widgetId: widgetData };
}
}
populatedLayout.widgets[position] = widgetData;
}
}
ctx.body = { layout: populatedLayout };
} catch (err: any) {
strapi.log.error('Failed to fetch populated layout:', err);
ctx.throw(500, `Failed to fetch populated layout: ${err.message}`);
}
},
});
export default controller;