diff --git a/app/ycode/components/CollectionFieldSelector.tsx b/app/ycode/components/CollectionFieldSelector.tsx index 49767702..637a8601 100644 --- a/app/ycode/components/CollectionFieldSelector.tsx +++ b/app/ycode/components/CollectionFieldSelector.tsx @@ -24,13 +24,30 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { selectVariants } from '@/components/ui/select'; -import type { CollectionField, Collection } from '@/types'; +import type { CollectionField, Collection, CollectionFieldType } from '@/types'; import { getFieldIcon, filterFieldGroupsByType, flattenFieldGroups, DISPLAYABLE_FIELD_TYPES } from '@/lib/collection-field-utils'; // Import and re-export from centralized location for backwards compatibility import type { FieldSourceType, FieldGroup } from '@/lib/collection-field-utils'; export type { FieldSourceType, FieldGroup } from '@/lib/collection-field-utils'; +/** + * Derives the effective allowed types from pre-filtered field groups by collecting + * all non-reference field types present. Used to constrain reference sub-options + * to the same types that were used to filter the root level. + */ +function deriveAllowedTypesFromGroups(fieldGroups: FieldGroup[]): CollectionFieldType[] { + const types = new Set(); + for (const group of fieldGroups) { + for (const field of group.fields) { + if (field.type !== 'reference' && field.type !== 'multi_reference') { + types.add(field.type as CollectionFieldType); + } + } + } + return Array.from(types); +} + interface CollectionFieldListProps { /** Fields to display at the current level */ fields: CollectionField[]; @@ -42,14 +59,14 @@ interface CollectionFieldListProps { onSelect: (fieldId: string, relationshipPath: string[], source?: FieldSourceType, layerId?: string) => void; /** Current relationship path (used internally for recursion) */ relationshipPath?: string[]; - /** Label for the current collection group */ - collectionLabel?: string; /** Source type for these fields (used internally for recursion) */ source?: FieldSourceType; /** ID of the collection layer these fields belong to */ layerId?: string; /** Depth level for indentation (used internally) */ depth?: number; + /** Allowed field types for filtering sub-options */ + allowedTypes?: CollectionFieldType[]; } /** @@ -90,6 +107,7 @@ function ReferenceFieldGroup({ source, layerId, depth = 0, + allowedTypes, }: { field: CollectionField; allFields: Record; @@ -99,27 +117,33 @@ function ReferenceFieldGroup({ source?: FieldSourceType; layerId?: string; depth?: number; + allowedTypes?: CollectionFieldType[]; }) { const referencedCollectionId = field.reference_collection_id; const referencedFields = referencedCollectionId ? allFields[referencedCollectionId] || [] : []; const referencedCollection = collections.find((c) => c.id === referencedCollectionId); - // Filter out multi-reference fields from nested display - const displayableFields = referencedFields.filter((f) => f.type !== 'multi_reference'); - const hasNestedFields = displayableFields.length > 0; + // Filter sub-fields: exclude multi_reference, apply allowedTypes if provided (keeping reference for deep nesting) + const displayableFields = referencedFields.filter((f) => { + if (f.type === 'multi_reference') return false; + if (allowedTypes && allowedTypes.length > 0 && f.type !== 'reference') { + return allowedTypes.includes(f.type); + } + return true; + }); + if (displayableFields.length === 0) return null; return ( {field.name} - {hasNestedFields && ( + {( {referencedCollection && ( @@ -136,6 +160,7 @@ function ReferenceFieldGroup({ source={source} layerId={layerId} depth={0} + allowedTypes={allowedTypes} /> )} @@ -155,6 +180,7 @@ function CollectionFieldSelectorInner({ source, layerId, depth = 0, + allowedTypes, }: CollectionFieldListProps) { // Filter out multi-reference fields const displayableFields = fields.filter((f) => f.type !== 'multi_reference'); @@ -175,6 +201,7 @@ function CollectionFieldSelectorInner({ source={source} layerId={layerId} depth={depth} + allowedTypes={allowedTypes} /> ); } @@ -201,51 +228,6 @@ function CollectionFieldSelectorInner({ ); } -/** - * Collection Field List - Renders a single group's fields with reference submenus. - * Used internally by CollectionFieldSelector. - */ -function CollectionFieldList({ - fields, - allFields, - collections, - onSelect, - collectionLabel, - source, - layerId, - relationshipPath = [], - depth = 0, -}: CollectionFieldListProps) { - // Filter out multi-reference fields at root level - const displayableFields = fields.filter((f) => f.type !== 'multi_reference'); - - if (displayableFields.length === 0) { - return ( -
- No fields available -
- ); - } - - return ( -
- {collectionLabel && ( - {collectionLabel} - )} - -
- ); -} - interface CollectionFieldSelectorProps { /** Field groups to display, each with their own source and label */ fieldGroups: FieldGroup[]; @@ -255,6 +237,8 @@ interface CollectionFieldSelectorProps { collections: Collection[]; /** Callback when a field is selected */ onSelect: (fieldId: string, relationshipPath: string[], source?: FieldSourceType, layerId?: string) => void; + /** Allowed field types for filtering sub-options in reference fields */ + allowedTypes?: CollectionFieldType[]; } /** @@ -268,9 +252,20 @@ export function CollectionFieldSelector({ allFields, collections, onSelect, + allowedTypes, }: CollectionFieldSelectorProps) { - // Filter to groups with displayable fields (excludes multi_reference) - const nonEmptyGroups = filterFieldGroupsByType(fieldGroups, DISPLAYABLE_FIELD_TYPES); + // Derive effective types from the incoming groups when not explicitly provided. + // Call sites already pre-filter groups to specific types, so the non-reference + // types present in the groups reflect the intended constraint. + const effectiveAllowedTypes = allowedTypes ?? deriveAllowedTypesFromGroups(fieldGroups); + + // Single filter pass: keeps only matching fields and excludes reference fields + // whose referenced collections have no matching sub-fields (via allFields check). + const nonEmptyGroups = filterFieldGroupsByType( + fieldGroups, + effectiveAllowedTypes.length > 0 ? effectiveAllowedTypes : DISPLAYABLE_FIELD_TYPES, + { allFields }, + ); if (nonEmptyGroups.length === 0) { return ( @@ -310,6 +305,7 @@ export function CollectionFieldSelector({ source={group.source} layerId={group.layerId} depth={0} + allowedTypes={effectiveAllowedTypes} /> ); @@ -338,7 +334,7 @@ interface FieldSelectDropdownProps { /** Additional class names for the trigger button */ className?: string; /** Field types to filter to (defaults to all displayable types) */ - allowedFieldTypes?: string[]; + allowedFieldTypes?: CollectionFieldType[]; } /** @@ -362,11 +358,9 @@ export function FieldSelectDropdown({ // Filter field groups by allowed types const filteredGroups = useMemo(() => { - if (allowedFieldTypes && allowedFieldTypes.length > 0) { - return filterFieldGroupsByType(fieldGroups, allowedFieldTypes as any); - } - return filterFieldGroupsByType(fieldGroups, DISPLAYABLE_FIELD_TYPES); - }, [fieldGroups, allowedFieldTypes]); + const types = allowedFieldTypes && allowedFieldTypes.length > 0 ? allowedFieldTypes : DISPLAYABLE_FIELD_TYPES; + return filterFieldGroupsByType(fieldGroups, types, { allFields }); + }, [fieldGroups, allowedFieldTypes, allFields]); // Find the selected field for display const selectedField = useMemo(() => { @@ -414,6 +408,7 @@ export function FieldSelectDropdown({ allFields={allFields} collections={collections} onSelect={handleSelect} + allowedTypes={allowedFieldTypes} /> diff --git a/lib/collection-field-utils.ts b/lib/collection-field-utils.ts index d5352377..389bf9ab 100644 --- a/lib/collection-field-utils.ts +++ b/lib/collection-field-utils.ts @@ -569,28 +569,64 @@ export function isVirtualAssetField(fieldId: string): boolean { return fieldId.startsWith('__asset_'); } +/** + * Checks recursively whether a reference field has at least one sub-field + * matching the allowed types (directly or via nested references). + */ +function referenceHasMatchingSubFields( + field: CollectionField, + allowedTypes: CollectionFieldType[], + allFields: Record, + visited: Set = new Set(), +): boolean { + if (!field.reference_collection_id) return false; + if (visited.has(field.reference_collection_id)) return false; + visited.add(field.reference_collection_id); + + const subFields = allFields[field.reference_collection_id] || []; + return subFields.some(f => { + if (f.type === 'multi_reference') return false; + if (allowedTypes.includes(f.type as CollectionFieldType)) return true; + if (f.type === 'reference') return referenceHasMatchingSubFields(f, allowedTypes, allFields, visited); + return false; + }); +} + /** * Filter field groups to only include fields of specified types. * Returns empty array if no matching fields exist. - * When options.excludeMultipleAsset is true, also excludes fields with multiple assets. + * - When options.excludeMultipleAsset is true, also excludes fields with multiple assets. + * - When options.allFields is provided, reference fields are only kept if their referenced + * collection contains at least one field matching the allowed types (checked recursively). */ export function filterFieldGroupsByType( fieldGroups: FieldGroup[] | undefined, allowedTypes: CollectionFieldType[], - options?: { excludeMultipleAsset?: boolean } + options?: { excludeMultipleAsset?: boolean; allFields?: Record } ): FieldGroup[] { if (!fieldGroups || fieldGroups.length === 0) return []; return fieldGroups - .map(group => ({ - ...group, - fields: group.fields.filter(field => { - if (field.type === 'reference' && field.reference_collection_id) return true; + .map(group => { + const fields = group.fields.filter(field => { + if (field.type === 'reference' && field.reference_collection_id) { + if (options?.allFields) { + return referenceHasMatchingSubFields(field, allowedTypes, options.allFields); + } + return true; + } if (!allowedTypes.includes(field.type)) return false; if (options?.excludeMultipleAsset && isMultipleAssetField(field)) return false; return true; - }), - })) + }); + // References always appear after regular fields + fields.sort((a, b) => { + const aIsRef = a.type === 'reference' ? 1 : 0; + const bIsRef = b.type === 'reference' ? 1 : 0; + return aIsRef - bIsRef; + }); + return { ...group, fields }; + }) .filter(group => group.fields.length > 0); }