From 226e520f994d6d6c80d066b03cdc2b323ff10627 Mon Sep 17 00:00:00 2001 From: Tristan Mouchet Date: Mon, 23 Mar 2026 15:38:49 +0100 Subject: [PATCH 1/3] fix: resolve reference field values on canvas - Enrich page collection item data with dotted relationship keys so reference fields (e.g. Category>Name via Blog) render on canvas - Restrict "Current page item" link option to pages sharing the same collection, preventing broken cross-collection links --- app/ycode/components/Canvas.tsx | 19 +++++++++++++++++-- app/ycode/components/LinkSettings.tsx | 12 +++++++++--- app/ycode/components/RichTextLinkSettings.tsx | 10 ++++++++-- 3 files changed, 34 insertions(+), 7 deletions(-) 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/LinkSettings.tsx b/app/ycode/components/LinkSettings.tsx index da50726e..71a5a367 100644 --- a/app/ycode/components/LinkSettings.tsx +++ b/app/ycode/components/LinkSettings.tsx @@ -239,6 +239,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)); @@ -903,8 +909,8 @@ 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 option (when both pages use the same collection) */} + {canUseCurrentPageItem && (
Current page item @@ -919,7 +925,7 @@ export default function LinkSettings(props: LinkSettingsProps) {
)} - {((isDynamicPage && isCurrentPageDynamic) || canUseCurrentCollectionItem) && } + {(canUseCurrentPageItem || canUseCurrentCollectionItem) && } {collectionItems.map((item) => ( {getItemDisplayName(item.id)} diff --git a/app/ycode/components/RichTextLinkSettings.tsx b/app/ycode/components/RichTextLinkSettings.tsx index 297b9651..bacd7e28 100644 --- a/app/ycode/components/RichTextLinkSettings.tsx +++ b/app/ycode/components/RichTextLinkSettings.tsx @@ -175,6 +175,12 @@ export default function RichTextLinkSettings({ 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)); @@ -660,7 +666,7 @@ export default function RichTextLinkSettings({ - {isDynamicPage && isCurrentPageDynamic && ( + {canUseCurrentPageItem && (
Current page item @@ -674,7 +680,7 @@ export default function RichTextLinkSettings({
)} - {((isDynamicPage && isCurrentPageDynamic) || canUseCurrentCollectionItem) && } + {(canUseCurrentPageItem || canUseCurrentCollectionItem) && } {collectionItems.map((item) => ( {getItemDisplayName(item.id)} From 78828d5e4d89b21544dd2f5ce55e496dfa297d26 Mon Sep 17 00:00:00 2001 From: Tristan Mouchet Date: Mon, 23 Mar 2026 16:46:04 +0100 Subject: [PATCH 2/3] feat: add reference field link resolution for dynamic pages - Enrich page collection item data with dotted keys for canvas - Restrict "Current page item" to pages sharing same collection - Add "{FieldName} - Reference field" options for cross-collection links - Resolve ref-page:* and ref-collection:* at render via item data --- app/ycode/components/LinkItemOptions.tsx | 69 +++++++++++++++++++ app/ycode/components/LinkSettings.tsx | 64 ++++++----------- app/ycode/components/RichTextLinkSettings.tsx | 57 +++++---------- components/PageRenderer.tsx | 47 ++++++++++++- lib/collection-field-utils.ts | 36 ++++++++++ lib/link-utils.ts | 33 ++++++++- lib/page-fetcher.ts | 12 +++- 7 files changed, 230 insertions(+), 88 deletions(-) create mode 100644 app/ycode/components/LinkItemOptions.tsx 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 71a5a367..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 { @@ -267,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; @@ -652,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 @@ -909,28 +900,13 @@ export default function LinkSettings(props: LinkSettingsProps) { - {/* Current page item option (when both pages use the same collection) */} - {canUseCurrentPageItem && ( - -
- Current page item -
-
- )} - {/* Current collection item option (when inside a collection layer OR when the layer IS a collection layer) */} - {canUseCurrentCollectionItem && ( - -
- Current collection item -
-
- )} - {(canUseCurrentPageItem || canUseCurrentCollectionItem) && } - {collectionItems.map((item) => ( - - {getItemDisplayName(item.id)} - - ))} +
diff --git a/app/ycode/components/RichTextLinkSettings.tsx b/app/ycode/components/RichTextLinkSettings.tsx index bacd7e28..420aa4c2 100644 --- a/app/ycode/components/RichTextLinkSettings.tsx +++ b/app/ycode/components/RichTextLinkSettings.tsx @@ -38,7 +38,8 @@ import { collectionsApi } from '@/lib/api'; import { getLayerIcon, getLayerName, getCollectionVariable } from '@/lib/layer-utils'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import PageSelector from './PageSelector'; -import { filterFieldGroupsByType, flattenFieldGroups, LINK_FIELD_TYPES, type FieldGroup } from '@/lib/collection-field-utils'; +import { filterFieldGroupsByType, flattenFieldGroups, LINK_FIELD_TYPES, buildReferenceItemOptions, type FieldGroup } from '@/lib/collection-field-utils'; +import LinkItemOptions from './LinkItemOptions'; export interface RichTextLinkSettingsProps { /** Current link settings */ @@ -202,6 +203,12 @@ export default function RichTextLinkSettings({ 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; @@ -494,23 +501,10 @@ export default function RichTextLinkSettings({ // 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; - - const collectionFields = pageCollectionId ? collectionsStoreFields[pageCollectionId] : []; - const nameField = collectionFields?.find((field) => field.key === 'name'); - if (nameField && item.values[nameField.id]) { - return item.values[nameField.id]; - } - - 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] ); return ( @@ -666,26 +660,13 @@ export default function RichTextLinkSettings({ - {canUseCurrentPageItem && ( - -
- Current page item -
-
- )} - {canUseCurrentCollectionItem && ( - -
- Current collection item -
-
- )} - {(canUseCurrentPageItem || canUseCurrentCollectionItem) && } - {collectionItems.map((item) => ( - - {getItemDisplayName(item.id)} - - ))} +
diff --git a/components/PageRenderer.tsx b/components/PageRenderer.tsx index 0f972b3e..9f2c3c72 100644 --- a/components/PageRenderer.tsx +++ b/components/PageRenderer.tsx @@ -12,8 +12,9 @@ import { buildCustomFontsCss, buildFontClassesCss, getGoogleFontLinks } from '@/ import { collectLayerAssetIds, getAssetProxyUrl } from '@/lib/asset-utils'; import { getAllPages } from '@/lib/repositories/pageRepository'; import { getAllPageFolders } from '@/lib/repositories/pageFolderRepository'; -import { getItemWithValues } from '@/lib/repositories/collectionItemRepository'; +import { getItemWithValues, getItemsWithValues } from '@/lib/repositories/collectionItemRepository'; import { getFieldsByCollectionId } from '@/lib/repositories/collectionFieldRepository'; +import { REF_PAGE_PREFIX, REF_COLLECTION_PREFIX } from '@/lib/link-utils'; import { getClassesString } from '@/lib/layer-utils'; import type { Layer, Component, Page, CollectionItemWithValues, CollectionField, Locale, PageFolder } from '@/types'; @@ -108,13 +109,14 @@ export default async function PageRenderer({ const resolvedLayers = layers || []; // Scan layers for collection_item_ids referenced in link settings - // Excludes special keywords like 'current-page' and 'current-collection' which are resolved at runtime + // Excludes special keywords and ref-* patterns which are resolved at runtime const findCollectionItemIds = (layers: Layer[]): Set => { const itemIds = new Set(); const specialKeywords = ['current-page', 'current-collection']; const scan = (layer: Layer) => { const itemId = layer.variables?.link?.page?.collection_item_id; - if (layer.variables?.link?.type === 'page' && itemId && !specialKeywords.includes(itemId)) { + if (layer.variables?.link?.type === 'page' && itemId + && !specialKeywords.includes(itemId) && !itemId.startsWith('ref-')) { itemIds.add(itemId); } if (layer.children) { @@ -125,6 +127,30 @@ export default async function PageRenderer({ return itemIds; }; + // Scan layers for ref-* links and collect target collection IDs + // so we can pre-fetch all items' slugs for those collections + const findRefTargetCollectionIds = (layers: Layer[], allPages: Page[]): Set => { + const collectionIds = new Set(); + const scan = (layer: Layer) => { + const itemId = layer.variables?.link?.page?.collection_item_id; + const pageId = layer.variables?.link?.page?.id; + if (layer.variables?.link?.type === 'page' && itemId + && (itemId.startsWith(REF_PAGE_PREFIX) || itemId.startsWith(REF_COLLECTION_PREFIX)) + && pageId) { + const targetPage = allPages.find(p => p.id === pageId); + const targetCollectionId = targetPage?.settings?.cms?.collection_id; + if (targetCollectionId) { + collectionIds.add(targetCollectionId); + } + } + if (layer.children) { + layer.children.forEach(scan); + } + }; + layers.forEach(scan); + return collectionIds; + }; + // Extract collection item slugs from resolved collection layers // These are populated by resolveCollectionLayers with `_collectionItemId` and `_collectionItemSlug` const extractCollectionItemSlugs = (layers: Layer[]): Record => { @@ -193,6 +219,21 @@ export default async function PageRenderer({ } } } + + // Fetch slugs for all items in collections targeted by ref-* links + const refTargetCollectionIds = findRefTargetCollectionIds(resolvedLayers, pages); + for (const collId of refTargetCollectionIds) { + const fields = await getFieldsByCollectionId(collId, false); + const slugField = fields.find(f => f.key === 'slug'); + if (!slugField) continue; + + const { items } = await getItemsWithValues(collId, false); + for (const item of items) { + if (item.values[slugField.id]) { + collectionItemSlugs[item.id] = item.values[slugField.id]; + } + } + } } catch (error) { console.error('[PageRenderer] Error fetching link resolution data:', error); } diff --git a/lib/collection-field-utils.ts b/lib/collection-field-utils.ts index 8725aa14..d5352377 100644 --- a/lib/collection-field-utils.ts +++ b/lib/collection-field-utils.ts @@ -601,6 +601,42 @@ export function flattenFieldGroups(fieldGroups: FieldGroup[] | undefined): Colle return fieldGroups?.flatMap(g => g.fields) || []; } +/** Prefix for reference-field-based collection item resolution (page source) */ +export const REF_PAGE_PREFIX = 'ref-page:'; +/** Prefix for reference-field-based collection item resolution (collection source) */ +export const REF_COLLECTION_PREFIX = 'ref-collection:'; + +/** A resolved option for a reference field pointing to a specific CMS page collection. */ +export interface ReferenceItemOption { + value: string; + label: string; +} + +/** + * Build select options for reference fields that point to a given target collection. + * Used in link settings dropdowns to offer "Category - Reference field" style options. + */ +export function buildReferenceItemOptions( + isDynamicPage: boolean, + targetPageCollectionId: string | null, + fieldGroups: FieldGroup[] | undefined +): ReferenceItemOption[] { + if (!isDynamicPage || !targetPageCollectionId || !fieldGroups) return []; + const options: ReferenceItemOption[] = []; + for (const group of fieldGroups) { + const prefix = group.source === 'page' ? REF_PAGE_PREFIX : REF_COLLECTION_PREFIX; + for (const field of group.fields) { + if (field.type === 'reference' && field.reference_collection_id === targetPageCollectionId) { + options.push({ + value: `${prefix}${field.id}`, + label: `${field.name} - Reference field`, + }); + } + } + } + return options; +} + /** * Check if any fields match a predicate across all groups. */ diff --git a/lib/link-utils.ts b/lib/link-utils.ts index 7568d51a..1e623258 100644 --- a/lib/link-utils.ts +++ b/lib/link-utils.ts @@ -194,6 +194,29 @@ export function hasLinkInTree(layer: Layer): boolean { return false; } +export { REF_PAGE_PREFIX, REF_COLLECTION_PREFIX } from '@/lib/collection-field-utils'; +import { REF_PAGE_PREFIX, REF_COLLECTION_PREFIX } from '@/lib/collection-field-utils'; + +/** + * Resolve a ref-* collection_item_id to the actual referenced item ID + * by looking up the reference field value in the current item data. + */ +export function resolveRefCollectionItemId( + collectionItemId: string, + pageCollectionItemData?: Record, + collectionItemData?: Record +): string | undefined { + if (collectionItemId.startsWith(REF_PAGE_PREFIX)) { + const fieldId = collectionItemId.slice(REF_PAGE_PREFIX.length); + return pageCollectionItemData?.[fieldId]; + } + if (collectionItemId.startsWith(REF_COLLECTION_PREFIX)) { + const fieldId = collectionItemId.slice(REF_COLLECTION_PREFIX.length); + return collectionItemData?.[fieldId]; + } + return undefined; +} + /** * Context for resolving links (page, asset, field types) */ @@ -436,13 +459,21 @@ export function generateLinkHref( if (page.is_dynamic && linkSettings.page.collection_item_id && collectionItemSlugs) { let itemSlug: string | undefined; - // Handle special "current" keywords + // Handle special "current" keywords and reference field resolution if (linkSettings.page.collection_item_id === 'current-page') { // Use the page's collection item (for dynamic pages) itemSlug = pageCollectionItemId ? collectionItemSlugs[pageCollectionItemId] : undefined; } else if (linkSettings.page.collection_item_id === 'current-collection') { // Use the current collection layer's item itemSlug = collectionItemId ? collectionItemSlugs[collectionItemId] : undefined; + } else if (linkSettings.page.collection_item_id.startsWith('ref-')) { + // Resolve via reference field value from current item data + const refItemId = resolveRefCollectionItemId( + linkSettings.page.collection_item_id, + pageCollectionItemData, + collectionItemData + ); + itemSlug = refItemId ? collectionItemSlugs[refItemId] : undefined; } else { // Use the specific item slug itemSlug = collectionItemSlugs[linkSettings.page.collection_item_id]; diff --git a/lib/page-fetcher.ts b/lib/page-fetcher.ts index de2c4f0b..6c2ae549 100644 --- a/lib/page-fetcher.ts +++ b/lib/page-fetcher.ts @@ -19,7 +19,7 @@ export interface PaginationContext { defaultPage?: number; } -import { resolveFieldLinkValue } from '@/lib/link-utils'; +import { resolveFieldLinkValue, resolveRefCollectionItemId } from '@/lib/link-utils'; import { SWIPER_CLASS_MAP, SWIPER_DATA_ATTR_MAP } from '@/lib/templates/utilities'; import { resolveInlineVariables, resolveInlineVariablesFromData } from '@/lib/inline-variables'; import { buildLayerTranslationKey, getTranslationByKey, hasValidTranslationValue, getTranslationValue } from '@/lib/localisation-utils'; @@ -3572,11 +3572,19 @@ function layerToHtml( if (linkedPage.is_dynamic && linkSettings.page.collection_item_id && collectionItemSlugs) { let itemSlug: string | undefined; - // Handle special "current" keywords - use the current collection item ID + // Handle special "current" keywords and reference field resolution if (linkSettings.page.collection_item_id === 'current-page' || linkSettings.page.collection_item_id === 'current-collection') { // Use the current collection item's slug (from effectiveCollectionItemId) itemSlug = effectiveCollectionItemId ? collectionItemSlugs[effectiveCollectionItemId] : undefined; + } else if (linkSettings.page.collection_item_id.startsWith('ref-')) { + // Resolve via reference field value from current item data + const refItemId = resolveRefCollectionItemId( + linkSettings.page.collection_item_id, + pageCollectionItemData, + effectiveCollectionItemData + ); + itemSlug = refItemId ? collectionItemSlugs[refItemId] : undefined; } else { // Use the specific item slug itemSlug = collectionItemSlugs[linkSettings.page.collection_item_id]; From eda1ae5f6b84513a492b0ba2be10b496112ea90f Mon Sep 17 00:00:00 2001 From: Tristan Mouchet Date: Mon, 23 Mar 2026 17:12:51 +0100 Subject: [PATCH 3/3] fix: render rich text links correctly on preview and generated pages - Add richTextLink mark handling in renderTiptapToHtml (page-fetcher) with full LinkResolutionContext so page/field/ref-* links resolve - Scan rich text Tiptap content for page links in PageRenderer so specific item slugs and ref-* target collections are pre-fetched - Add hidePageContextOptions prop to suppress "Current page item" and reference field options when editing CMS item content directly - Refactor PageRenderer link scanning into module-level pure utilities (collectTiptapPageLinks, collectLayerPageLinks, extractCollectionItemSlugs) replacing two separate tree traversals with a single shared pass --- app/ycode/components/CollectionItemSheet.tsx | 2 + app/ycode/components/RichTextEditor.tsx | 5 + app/ycode/components/RichTextEditorSheet.tsx | 4 + app/ycode/components/RichTextLinkPopover.tsx | 4 + app/ycode/components/RichTextLinkSettings.tsx | 15 +- components/PageRenderer.tsx | 135 +++++++++--------- lib/page-fetcher.ts | 48 +++++-- 7 files changed, 135 insertions(+), 78 deletions(-) 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/RichTextEditor.tsx b/app/ycode/components/RichTextEditor.tsx index 78f2f006..ec60e3a9 100644 --- a/app/ycode/components/RichTextEditor.tsx +++ b/app/ycode/components/RichTextEditor.tsx @@ -97,6 +97,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) */ @@ -319,6 +321,7 @@ const RichTextEditor = forwardRef(({ variant = 'compact', size = 'xs', excludedLinkTypes = [], + hidePageContextOptions = false, fullHeight = false, onExpandClick, allowedFieldTypes = RICH_TEXT_ONLY_FIELD_TYPES, @@ -811,6 +814,7 @@ const RichTextEditor = forwardRef(({ open={linkPopoverOpen} onOpenChange={setLinkPopoverOpen} excludedLinkTypes={excludedLinkTypes} + hidePageContextOptions={hidePageContextOptions} trigger={ (({ open={linkPopoverOpen} onOpenChange={setLinkPopoverOpen} excludedLinkTypes={excludedLinkTypes} + hidePageContextOptions={hidePageContextOptions} trigger={