/** * Patterns List Page * * Custom patterns list page with quickAction to view usage reports. * Uses EntityTable directly to enable custom quickActions. */ 'use client' import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { Plus, Loader2, BarChart3, Pencil, Trash2 } from 'lucide-react' import { EntityTable } from '@nextsparkjs/core/components/entities/EntityTable' import { EntityBulkActions } from '@nextsparkjs/core/components/entities/EntityBulkActions' import { Alert, AlertDescription } from '@nextsparkjs/core/components/ui/alert' import { Button } from '@nextsparkjs/core/components/ui/button' import { SkeletonEntityList } from '@nextsparkjs/core/components/ui/skeleton-list' import { SearchInput } from '@nextsparkjs/core/components/shared/SearchInput' import { MultiSelectFilter } from '@nextsparkjs/core/components/shared/MultiSelectFilter' import { PatternDeleteDialog } from '@nextsparkjs/core/components/patterns' import { useEntityConfig } from '@nextsparkjs/core/hooks/useEntityConfig' import { useUrlFilters, type FilterSchema, type EntityFiltersReturn } from '@nextsparkjs/core/hooks/useUrlFilters' import { listEntityData, deleteEntityData } from '@nextsparkjs/core/lib/api/entities' import { useTeam } from '@nextsparkjs/core/hooks/useTeam' import { usePermission } from '@nextsparkjs/core/lib/permissions/hooks' import type { Permission } from '@nextsparkjs/core/lib/permissions/types' import type { QuickAction, DropdownAction } from '@nextsparkjs/core/components/entities/entity-table.types' import { sel } from '@nextsparkjs/core/lib/test' import { toast } from 'sonner' import { getTemplateOrDefaultClient } from '@nextsparkjs/registries/template-registry.client' interface PatternItem { id: string name?: string title?: string usageCount?: number [key: string]: unknown } function PatternsListPage() { const entityType = 'patterns' const router = useRouter() // Use the new centralized hook for entity configuration const { config: entityConfig, isLoading, error: configError, isOverride } = useEntityConfig(entityType) // Get current team for relation resolution const { teamId } = useTeam() // Check permissions const canCreate = usePermission(`${entityType}.create` as Permission) // Generate filter schema dynamically from entityConfig const filterSchema = useMemo(() => { const schema: FilterSchema = { search: { type: 'search', urlParam: 'search' } as const } // Add filters from entity config entityConfig?.ui.dashboard.filters?.forEach(filterConfig => { if (filterConfig.type === 'multiSelect' || filterConfig.type === 'singleSelect') { schema[filterConfig.field] = { type: filterConfig.type, urlParam: filterConfig.urlParam || filterConfig.field } } }) return schema }, [entityConfig?.ui.dashboard.filters]) // Use URL-synchronized filters with dynamic schema const { filters, setFilter } = useUrlFilters(filterSchema) as unknown as EntityFiltersReturn // Data loading states const [data, setData] = useState([]) const [isInitialLoad, setIsInitialLoad] = useState(true) const [isSearching, setIsSearching] = useState(false) const [dataError, setDataError] = useState(null) // Selection state for bulk actions const [selectedIds, setSelectedIds] = useState>(new Set()) // Delete dialog state for PatternDeleteDialog const [deleteTarget, setDeleteTarget] = useState(null) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [isDeleting, setIsDeleting] = useState(false) // Build API filters from URL filter state const buildApiFilters = useCallback(() => { const apiFilters: Record = {} if (filters.search && typeof filters.search === 'string' && filters.search.trim()) { apiFilters.search = filters.search } entityConfig?.ui.dashboard.filters?.forEach(filterConfig => { const value = filters[filterConfig.field] if (Array.isArray(value) && value.length > 0) { apiFilters[filterConfig.field] = value.join(',') } else if (typeof value === 'string' && value) { apiFilters[filterConfig.field] = value } }) return apiFilters }, [filters, entityConfig?.ui.dashboard.filters]) // Load entity data function const loadData = useCallback(async (isInitial = false) => { if (!entityConfig) return if (!entityConfig.enabled) { setDataError(`Entity "${entityType}" is disabled`) return } try { if (isInitial) { setIsInitialLoad(true) } else { setIsSearching(true) } setDataError(null) const apiFilters = buildApiFilters() const result = await listEntityData(entityType, { limit: 50, includeMeta: true, ...(Object.keys(apiFilters).length > 0 && { filters: apiFilters }) }) setData(result.data as PatternItem[]) setDataError(null) } catch (err) { console.error(`[PatternsListPage] Error loading data:`, err) setDataError(`Error loading data: ${err instanceof Error ? err.message : 'Unknown error'}`) setData([]) } finally { setIsInitialLoad(false) setIsSearching(false) } }, [entityConfig, entityType, buildApiFilters]) // Track previous filters and schema to detect changes const filtersKey = JSON.stringify(filters) const prevFiltersKeyRef = useRef('') const hasLoadedRef = useRef(false) const schemaKeysForSync = useMemo( () => Object.keys(filterSchema).sort().join(','), [filterSchema] ) const prevSchemaKeysRef = useRef(schemaKeysForSync) // Check if filters are synced with URL params const filtersMatchUrl = useMemo(() => { if (typeof window === 'undefined') return true if (schemaKeysForSync !== prevSchemaKeysRef.current) { prevSchemaKeysRef.current = schemaKeysForSync return false } const urlParams = new URLSearchParams(window.location.search) for (const filterConfig of entityConfig?.ui.dashboard.filters || []) { const urlValue = urlParams.get(filterConfig.field) const filterValue = filters[filterConfig.field] if (urlValue && (!filterValue || (Array.isArray(filterValue) && filterValue.length === 0))) { return false } } return true }, [filters, entityConfig?.ui.dashboard.filters, schemaKeysForSync]) // Load data when config is ready AND filters are synced with URL useEffect(() => { if (!entityConfig?.slug) return if (!filtersMatchUrl) return const isInitial = !hasLoadedRef.current const filtersChanged = prevFiltersKeyRef.current !== filtersKey if (isInitial || filtersChanged) { prevFiltersKeyRef.current = filtersKey hasLoadedRef.current = true loadData(isInitial) } }, [entityConfig?.slug, filtersKey, filtersMatchUrl, loadData]) // Handle search input change const handleSearch = useCallback((query: string) => { setFilter('search', query) }, [setFilter]) // Handler that opens the delete dialog (called from dropdown action) const handleDeleteClick = useCallback((item: PatternItem) => { setDeleteTarget(item) setIsDeleteDialogOpen(true) }, []) // Handler that executes the delete (called from PatternDeleteDialog) const handleConfirmDelete = useCallback(async () => { if (!deleteTarget) return setIsDeleting(true) try { await deleteEntityData(entityType, deleteTarget.id) toast.success(`Pattern deleted successfully`) setSelectedIds(prev => { const newSet = new Set(prev) newSet.delete(deleteTarget.id) return newSet }) await loadData(false) } catch (err) { console.error(`[PatternsListPage] Error deleting pattern:`, err) toast.error(`Error deleting: ${err instanceof Error ? err.message : 'Unknown error'}`) } finally { setIsDeleting(false) setIsDeleteDialogOpen(false) setDeleteTarget(null) } }, [deleteTarget, entityType, loadData]) // Handle bulk delete const handleBulkDelete = useCallback(async (ids: string[]) => { try { await Promise.all(ids.map(id => deleteEntityData(entityType, id))) toast.success(`${ids.length} pattern${ids.length === 1 ? '' : 's'} deleted successfully`) await loadData(false) } catch (err) { console.error(`[PatternsListPage] Error bulk deleting patterns:`, err) toast.error(`Error deleting: ${err instanceof Error ? err.message : 'Unknown error'}`) throw err } }, [entityType, loadData]) // Handle select all const handleSelectAll = useCallback(() => { const allIds = new Set(data.map(item => String(item.id))) setSelectedIds(allIds) }, [data]) // Handle clear selection const handleClearSelection = useCallback(() => { setSelectedIds(new Set()) }, []) // Get item name for confirmations const getItemName = useCallback((item: PatternItem): string => { return String(item.name || item.title || item.id) }, []) // Custom quickActions for patterns - includes "View Usages" action const quickActions: QuickAction[] = useMemo( () => [ { id: 'view-usages', label: 'View Usages', icon: , onClick: (item: PatternItem) => { router.push(`/dashboard/patterns/${item.id}/reports`) }, dataCySuffix: 'usages', }, ], [router] ) // Custom dropdownActions - replace default delete with PatternDeleteDialog const dropdownActions: DropdownAction[] = useMemo( () => [ { id: 'edit', label: 'Edit', icon: , onClick: (item) => router.push(`/dashboard/patterns/${item.id}/edit`), dataCySuffix: 'edit', }, { id: 'delete', label: 'Delete', icon: , onClick: handleDeleteClick, variant: 'destructive', separator: true, dataCySuffix: 'delete', }, ], [router, handleDeleteClick] ) if (isLoading) { return } if (configError || !entityConfig) { return ( {configError || `Could not load configuration for entity "${entityType}".`} ) } if (dataError) { return ( {dataError} ) } const bulkOperationsEnabled = entityConfig.ui.features.bulkOperations const enableSearch = entityConfig.ui.features.searchable const enableFilters = entityConfig.ui.features.filterable && entityConfig.ui.dashboard.filters?.length const searchValue = typeof filters.search === 'string' ? filters.search : '' return (
{/* Row 1: Title + Create button */}

{entityConfig.names.plural}

Manage your {entityConfig.names.plural.toLowerCase()}

{canCreate && ( )}
{/* Row 2: Search + Filters */} {(enableSearch || enableFilters) && (
{enableSearch && ( handleSearch(e.target.value)} containerClassName="flex-1 max-w-md" data-cy={sel('entities.list.search.container', { slug: entityConfig.slug })} /> )} {enableFilters && entityConfig.ui.dashboard.filters?.map(filterConfig => { const field = entityConfig.fields.find(f => f.name === filterConfig.field) if (!field?.options) return null const filterValues = filters[filterConfig.field] const values = Array.isArray(filterValues) ? filterValues : [] return ( ({ value: String(opt.value), label: opt.label }))} values={values} onChange={(newValues) => setFilter(filterConfig.field, newValues)} data-cy={sel('entities.list.filters.trigger', { slug: entityConfig.slug, field: filterConfig.field })} /> ) })} {isSearching && ( )}
)} {/* Row 3: Data Table with custom quickActions and dropdownActions */} } loading={isInitialLoad} selectable={bulkOperationsEnabled} selectedIds={selectedIds} onSelectionChange={setSelectedIds} enableSearch={false} searchQuery={searchValue} onSearch={handleSearch} getItemName={getItemName} teamId={teamId} useDefaultActions={false} quickActions={quickActions} dropdownActions={dropdownActions} showHeader={false} pagination={{ pageSize: 10, showPageSizeSelector: true, pageSizeOptions: [10, 20, 50, 100], }} /> {/* Floating Bulk Actions Bar */} {bulkOperationsEnabled && ( )} {/* Pattern Delete Dialog - shows usage warning before deleting */} {deleteTarget && ( )}
) } export default getTemplateOrDefaultClient('app/dashboard/(main)/patterns/page.tsx', PatternsListPage)