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).

298 lines (261 loc) 10 kB
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;