diff --git a/app/ycode/components/Canvas.tsx b/app/ycode/components/Canvas.tsx index 05ce106e..07e93e5d 100644 --- a/app/ycode/components/Canvas.tsx +++ b/app/ycode/components/Canvas.tsx @@ -22,6 +22,7 @@ import { getCanvasIframeHtml } from '@/lib/canvas-utils'; import { CanvasPortalProvider } from '@/lib/canvas-portal-context'; import { cn } from '@/lib/utils'; import { loadSwiperCss } from '@/lib/slider-utils'; +import { resolveReferenceFieldsSync } from '@/lib/collection-utils'; import { useEditorStore } from '@/stores/useEditorStore'; import { useFontsStore } from '@/stores/useFontsStore'; import { useColorVariablesStore } from '@/stores/useColorVariablesStore'; @@ -298,6 +299,19 @@ export default function Canvas({ return serializeLayers(layers, components, editingComponentVariables); }, [layers, components, editingComponentVariables]); + // Enrich page collection item data with reference field dotted keys + // so variables like "refFieldId.targetFieldId" resolve on canvas + const enrichedPageCollectionItemData = useMemo(() => { + const values = pageCollectionItem?.values; + if (!values || !pageCollectionFields?.length) return values || null; + return resolveReferenceFieldsSync( + values, + pageCollectionFields, + collectionItems, + collectionFields + ); + }, [pageCollectionItem?.values, pageCollectionFields, collectionItems, collectionFields]); + // Collect layer IDs that should be hidden on canvas (display: hidden with on-load) const editorHiddenLayerIds = useMemo(() => { if (disableEditorHiddenLayers) return undefined; @@ -472,7 +486,7 @@ export default function Canvas({ hoveredLayerId={effectiveHoveredLayerId} pageId={pageId} pageCollectionItemId={pageCollectionItem?.id} - pageCollectionItemData={pageCollectionItem?.values || null} + pageCollectionItemData={enrichedPageCollectionItemData} onLayerClick={handleLayerClick} onLayerUpdate={onLayerUpdate} onLayerHover={handleLayerHover} @@ -495,7 +509,8 @@ export default function Canvas({ editingComponentId, editingComponentVariables, pageId, - pageCollectionItem, + pageCollectionItem?.id, + enrichedPageCollectionItemData, handleLayerClick, onLayerUpdate, handleLayerHover, diff --git a/app/ycode/components/CollectionItemSheet.tsx b/app/ycode/components/CollectionItemSheet.tsx index bf2c9a1b..37ef6b49 100644 --- a/app/ycode/components/CollectionItemSheet.tsx +++ b/app/ycode/components/CollectionItemSheet.tsx @@ -569,6 +569,7 @@ export default function CollectionItemSheet({ variant="full" withFormatting={true} excludedLinkTypes={['asset', 'field']} + hidePageContextOptions={true} onExpandClick={() => setExpandedRichTextField(field.id)} /> ) : field.type === 'reference' && field.reference_collection_id ? ( diff --git a/app/ycode/components/LinkItemOptions.tsx b/app/ycode/components/LinkItemOptions.tsx new file mode 100644 index 00000000..e25287e8 --- /dev/null +++ b/app/ycode/components/LinkItemOptions.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { SelectItem, SelectSeparator } from '@/components/ui/select'; +import type { CollectionItemWithValues, CollectionField } from '@/types'; +import type { ReferenceItemOption } from '@/lib/collection-field-utils'; + +interface CollectionItemSelectOptionsProps { + canUseCurrentPageItem: boolean; + canUseCurrentCollectionItem: boolean; + referenceItemOptions: ReferenceItemOption[]; + collectionItems: CollectionItemWithValues[]; + /** Fields for the linked page's collection, used to derive display names */ + collectionFields: CollectionField[]; +} + +/** Derives a human-readable label for a collection item. */ +function getDisplayName(item: CollectionItemWithValues, collectionFields: CollectionField[]): string { + const nameField = collectionFields.find(f => f.key === 'name'); + if (nameField && item.values[nameField.id]) return item.values[nameField.id]; + const values = Object.values(item.values); + return values[0] || item.id; +} + +/** + * Shared SelectContent items for CMS item pickers used in link settings. + * Renders "Current page item", "Current collection item", reference field options, + * a separator, and the concrete item list. + */ +export default function LinkItemOptions({ + canUseCurrentPageItem, + canUseCurrentCollectionItem, + referenceItemOptions, + collectionItems, + collectionFields, +}: CollectionItemSelectOptionsProps) { + const hasSpecialOptions = canUseCurrentPageItem || canUseCurrentCollectionItem || referenceItemOptions.length > 0; + + return ( + <> + {canUseCurrentPageItem && ( + +
+ Current page item +
+
+ )} + {canUseCurrentCollectionItem && ( + +
+ Current collection item +
+
+ )} + {referenceItemOptions.map((opt) => ( + +
+ {opt.label} +
+
+ ))} + {hasSpecialOptions && } + {collectionItems.map((item) => ( + + {getDisplayName(item, collectionFields)} + + ))} + + ); +} diff --git a/app/ycode/components/LinkSettings.tsx b/app/ycode/components/LinkSettings.tsx index da50726e..3a5cb787 100644 --- a/app/ycode/components/LinkSettings.tsx +++ b/app/ycode/components/LinkSettings.tsx @@ -16,7 +16,9 @@ import { Checkbox } from '@/components/ui/checkbox'; import Icon, { type IconProps } from '@/components/ui/icon'; import SettingsPanel from './SettingsPanel'; import RichTextEditor from './RichTextEditor'; -import { filterFieldGroupsByType, flattenFieldGroups, LINK_FIELD_TYPES } from '@/lib/collection-field-utils'; +import { filterFieldGroupsByType, flattenFieldGroups, LINK_FIELD_TYPES, buildReferenceItemOptions } from '@/lib/collection-field-utils'; +import { generateLinkHref } from '@/lib/link-utils'; +import LinkItemOptions from './LinkItemOptions'; import { FieldSelectDropdown, type FieldGroup, type FieldSourceType } from './CollectionFieldSelector'; import ComponentVariableLabel, { VARIABLE_TYPE_ICONS } from './ComponentVariableLabel'; import { @@ -239,6 +241,12 @@ export default function LinkSettings(props: LinkSettingsProps) { const currentPage = currentPageId ? pages.find(p => p.id === currentPageId) : null; const isCurrentPageDynamic = currentPage?.is_dynamic || false; + // "Current page item" only makes sense when both pages use the same collection + const currentPageCollectionId = currentPage?.settings?.cms?.collection_id || null; + const targetPageCollectionId = selectedPage?.settings?.cms?.collection_id || null; + const canUseCurrentPageItem = isDynamicPage && isCurrentPageDynamic + && !!currentPageCollectionId && currentPageCollectionId === targetPageCollectionId; + // Check if the layer itself is a collection layer const isCollectionLayer = !!(layer && getCollectionVariable(layer)); @@ -261,6 +269,12 @@ export default function LinkSettings(props: LinkSettingsProps) { const hasCollectionFields = !!(collectionGroup && collectionGroup.fields.length > 0 && isInsideCollectionLayer); const canUseCurrentCollectionItem = hasCollectionFields || isCollectionLayer; + // Find reference fields that point to the target page's collection + const referenceItemOptions = useMemo( + () => buildReferenceItemOptions(isDynamicPage, targetPageCollectionId, fieldGroups), + [isDynamicPage, targetPageCollectionId, fieldGroups] + ); + // Get collection ID from dynamic page settings const pageCollectionId = selectedPage?.settings?.cms?.collection_id || null; @@ -646,27 +660,10 @@ export default function LinkSettings(props: LinkSettingsProps) { // Get asset info for display const selectedAsset = assetId ? getAsset(assetId) : null; - // Get display name for selected collection item - const getItemDisplayName = useCallback( - (itemId: string) => { - if (itemId === 'current') return 'Current Item'; - const item = collectionItems.find((i) => i.id === itemId); - if (!item) return itemId; - - // Get fields from store for the page's collection - const collectionFields = pageCollectionId ? collectionsStoreFields[pageCollectionId] : []; - - // Find the field with key === 'name' - const nameField = collectionFields?.find((field) => field.key === 'name'); - if (nameField && item.values[nameField.id]) { - return item.values[nameField.id]; - } - - // Fall back to first available value - const values = Object.values(item.values); - return values[0] || itemId; - }, - [collectionItems, pageCollectionId, collectionsStoreFields] + // Fields for the linked page's collection (for display names) + const linkedPageCollectionFields = useMemo( + () => pageCollectionId ? collectionsStoreFields[pageCollectionId] || [] : [], + [pageCollectionId, collectionsStoreFields] ); // Layer mode requires a layer @@ -903,28 +900,13 @@ export default function LinkSettings(props: LinkSettingsProps) { - {/* Current page item option (when on a dynamic page AND linking to a dynamic page) */} - {isDynamicPage && isCurrentPageDynamic && ( - -
- Current page item -
-
- )} - {/* Current collection item option (when inside a collection layer OR when the layer IS a collection layer) */} - {canUseCurrentCollectionItem && ( - -
- Current collection item -
-
- )} - {((isDynamicPage && isCurrentPageDynamic) || canUseCurrentCollectionItem) && } - {collectionItems.map((item) => ( - - {getItemDisplayName(item.id)} - - ))} +
diff --git a/app/ycode/components/RichTextEditor.tsx b/app/ycode/components/RichTextEditor.tsx index e4f8aa11..f30a29b5 100644 --- a/app/ycode/components/RichTextEditor.tsx +++ b/app/ycode/components/RichTextEditor.tsx @@ -98,6 +98,8 @@ interface RichTextEditorProps { size?: 'xs' | 'sm'; /** Link types to exclude from the link settings dropdown */ excludedLinkTypes?: LinkType[]; + /** Hide "Current page item" and "Reference field" options (e.g. when editing CMS item content) */ + hidePageContextOptions?: boolean; /** Stretch editor to fill parent height (scrolls content instead of growing) */ fullHeight?: boolean; /** Callback to open the full editor sheet (shown as expand button in toolbar) */ @@ -320,6 +322,7 @@ const RichTextEditor = forwardRef(({ variant = 'compact', size = 'xs', excludedLinkTypes = [], + hidePageContextOptions = false, fullHeight = false, onExpandClick, allowedFieldTypes = RICH_TEXT_ONLY_FIELD_TYPES, @@ -813,6 +816,7 @@ const RichTextEditor = forwardRef(({ open={linkPopoverOpen} onOpenChange={setLinkPopoverOpen} excludedLinkTypes={excludedLinkTypes} + hidePageContextOptions={hidePageContextOptions} trigger={ (({ open={linkPopoverOpen} onOpenChange={setLinkPopoverOpen} excludedLinkTypes={excludedLinkTypes} + hidePageContextOptions={hidePageContextOptions} trigger={