fix: enable nested base recipe references to display correctly
All checks were successful
CI / update (push) Successful in 1m10s
All checks were successful
CI / update (push) Successful in 1m10s
- Add recursive population for nested base recipe references (up to 3 levels deep) in API endpoints - Implement recursive mapping of baseRecipeRef to resolvedRecipe for all nesting levels - Add recursive flattening functions in frontend components to handle nested references - Fix TranslationApproval to use short_name instead of ObjectId for base recipe lookups - Add circular reference detection to prevent infinite loops This ensures that when Recipe A references Recipe B as a base, and Recipe B references Recipe C, all three recipes' content is properly displayed.
This commit is contained in:
@@ -7,27 +7,32 @@ import HefeSwapper from './HefeSwapper.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
// Flatten ingredient references for display
|
||||
const flattenedIngredients = $derived.by(() => {
|
||||
if (!data.ingredients) return [];
|
||||
// Recursively flatten nested ingredient references
|
||||
function flattenIngredientReferences(items, lang, visited = new Set()) {
|
||||
const result = [];
|
||||
|
||||
return data.ingredients.flatMap((item) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'reference' && item.resolvedRecipe) {
|
||||
const sections = [];
|
||||
// Prevent circular references
|
||||
const recipeId = item.resolvedRecipe._id?.toString() || item.resolvedRecipe.short_name;
|
||||
if (visited.has(recipeId)) {
|
||||
console.warn('Circular reference detected:', recipeId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newVisited = new Set(visited);
|
||||
newVisited.add(recipeId);
|
||||
|
||||
// Get translated or original ingredients
|
||||
const lang = data.lang || 'de';
|
||||
const ingredientsToUse = (lang === 'en' &&
|
||||
item.resolvedRecipe.translations?.en?.ingredients)
|
||||
? item.resolvedRecipe.translations.en.ingredients
|
||||
: item.resolvedRecipe.ingredients || [];
|
||||
|
||||
// Filter to only sections (not nested references)
|
||||
const baseIngredients = item.includeIngredients
|
||||
? ingredientsToUse.filter(i => i.type === 'section' || !i.type)
|
||||
: [];
|
||||
// Recursively flatten nested references
|
||||
const flattenedNested = flattenIngredientReferences(ingredientsToUse, lang, newVisited);
|
||||
|
||||
// Combine all items into one section
|
||||
// Combine all items into one list
|
||||
const combinedList = [];
|
||||
|
||||
// Add items before
|
||||
@@ -35,12 +40,14 @@ const flattenedIngredients = $derived.by(() => {
|
||||
combinedList.push(...item.itemsBefore);
|
||||
}
|
||||
|
||||
// Add base recipe ingredients
|
||||
baseIngredients.forEach(section => {
|
||||
if (section.list) {
|
||||
combinedList.push(...section.list);
|
||||
}
|
||||
});
|
||||
// Add base recipe ingredients (now recursively flattened)
|
||||
if (item.includeIngredients) {
|
||||
flattenedNested.forEach(section => {
|
||||
if (section.list) {
|
||||
combinedList.push(...section.list);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add items after
|
||||
if (item.itemsAfter && item.itemsAfter.length > 0) {
|
||||
@@ -49,12 +56,11 @@ const flattenedIngredients = $derived.by(() => {
|
||||
|
||||
// Push as one section with optional label
|
||||
if (combinedList.length > 0) {
|
||||
// Use labelOverride if present, otherwise use base recipe name (translated if viewing in English)
|
||||
const baseRecipeName = (lang === 'en' && item.resolvedRecipe.translations?.en?.name)
|
||||
? item.resolvedRecipe.translations.en.name
|
||||
: item.resolvedRecipe.name;
|
||||
|
||||
sections.push({
|
||||
result.push({
|
||||
type: 'section',
|
||||
name: item.showLabel ? (item.labelOverride || baseRecipeName) : '',
|
||||
list: combinedList,
|
||||
@@ -62,13 +68,20 @@ const flattenedIngredients = $derived.by(() => {
|
||||
short_name: item.resolvedRecipe.short_name
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
} else if (item.type === 'section' || !item.type) {
|
||||
// Regular section - pass through
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular section - pass through
|
||||
return [item];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Flatten ingredient references for display
|
||||
const flattenedIngredients = $derived.by(() => {
|
||||
if (!data.ingredients) return [];
|
||||
const lang = data.lang || 'de';
|
||||
return flattenIngredientReferences(data.ingredients, lang);
|
||||
});
|
||||
let multiplier = $state(data.multiplier || 1);
|
||||
|
||||
|
||||
@@ -3,25 +3,32 @@ let { data } = $props();
|
||||
|
||||
let multiplier = $state(data.multiplier || 1);
|
||||
|
||||
// Flatten instruction references for display
|
||||
const flattenedInstructions = $derived.by(() => {
|
||||
if (!data.instructions) return [];
|
||||
// Recursively flatten nested instruction references
|
||||
function flattenInstructionReferences(items, lang, visited = new Set()) {
|
||||
const result = [];
|
||||
|
||||
return data.instructions.flatMap((item) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'reference' && item.resolvedRecipe) {
|
||||
// Prevent circular references
|
||||
const recipeId = item.resolvedRecipe._id?.toString() || item.resolvedRecipe.short_name;
|
||||
if (visited.has(recipeId)) {
|
||||
console.warn('Circular reference detected:', recipeId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newVisited = new Set(visited);
|
||||
newVisited.add(recipeId);
|
||||
|
||||
// Get translated or original instructions
|
||||
const lang = data.lang || 'de';
|
||||
const instructionsToUse = (lang === 'en' &&
|
||||
item.resolvedRecipe.translations?.en?.instructions)
|
||||
? item.resolvedRecipe.translations.en.instructions
|
||||
: item.resolvedRecipe.instructions || [];
|
||||
|
||||
// Filter to only sections (not nested references)
|
||||
const baseInstructions = item.includeInstructions
|
||||
? instructionsToUse.filter(i => i.type === 'section' || !i.type)
|
||||
: [];
|
||||
// Recursively flatten nested references
|
||||
const flattenedNested = flattenInstructionReferences(instructionsToUse, lang, newVisited);
|
||||
|
||||
// Combine all steps into one section
|
||||
// Combine all steps into one list
|
||||
const combinedSteps = [];
|
||||
|
||||
// Add steps before
|
||||
@@ -29,12 +36,14 @@ const flattenedInstructions = $derived.by(() => {
|
||||
combinedSteps.push(...item.stepsBefore);
|
||||
}
|
||||
|
||||
// Add base recipe instructions
|
||||
baseInstructions.forEach(section => {
|
||||
if (section.steps) {
|
||||
combinedSteps.push(...section.steps);
|
||||
}
|
||||
});
|
||||
// Add base recipe instructions (now recursively flattened)
|
||||
if (item.includeInstructions) {
|
||||
flattenedNested.forEach(section => {
|
||||
if (section.steps) {
|
||||
combinedSteps.push(...section.steps);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add steps after
|
||||
if (item.stepsAfter && item.stepsAfter.length > 0) {
|
||||
@@ -43,26 +52,32 @@ const flattenedInstructions = $derived.by(() => {
|
||||
|
||||
// Push as one section with optional label
|
||||
if (combinedSteps.length > 0) {
|
||||
// Use labelOverride if present, otherwise use base recipe name (translated if viewing in English)
|
||||
const baseRecipeName = (lang === 'en' && item.resolvedRecipe.translations?.en?.name)
|
||||
? item.resolvedRecipe.translations.en.name
|
||||
: item.resolvedRecipe.name;
|
||||
|
||||
return [{
|
||||
result.push({
|
||||
type: 'section',
|
||||
name: item.showLabel ? (item.labelOverride || baseRecipeName) : '',
|
||||
steps: combinedSteps,
|
||||
isReference: item.showLabel,
|
||||
short_name: item.resolvedRecipe.short_name
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
} else if (item.type === 'section' || !item.type) {
|
||||
// Regular section - pass through
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular section - pass through
|
||||
return [item];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Flatten instruction references for display
|
||||
const flattenedInstructions = $derived.by(() => {
|
||||
if (!data.instructions) return [];
|
||||
const lang = data.lang || 'de';
|
||||
return flattenInstructionReferences(data.instructions, lang);
|
||||
});
|
||||
|
||||
const isEnglish = $derived(data.lang === 'en');
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
let translationMetadata: any = null;
|
||||
|
||||
// Track base recipes that need translation
|
||||
let untranslatedBaseRecipes: { id: string, name: string }[] = [];
|
||||
let untranslatedBaseRecipes: { shortName: string, name: string }[] = [];
|
||||
let checkingBaseRecipes = false;
|
||||
|
||||
// Sync base recipe references from German to English
|
||||
@@ -36,21 +36,32 @@
|
||||
|
||||
checkingBaseRecipes = true;
|
||||
|
||||
// Helper to extract short_name from baseRecipeRef (which might be an object or string)
|
||||
const getShortName = (baseRecipeRef: any): string => {
|
||||
return typeof baseRecipeRef === 'object' ? baseRecipeRef.short_name : baseRecipeRef;
|
||||
};
|
||||
|
||||
// Collect all base recipe references from German data
|
||||
const germanBaseRecipeIds = new Set<string>();
|
||||
const germanBaseRecipeShortNames = new Set<string>();
|
||||
const baseRecipeRefMap = new Map<string, any>(); // Map short_name to baseRecipeRef (ID or object)
|
||||
|
||||
(germanData.ingredients || []).forEach((ing: any) => {
|
||||
if (ing.type === 'reference' && ing.baseRecipeRef) {
|
||||
germanBaseRecipeIds.add(ing.baseRecipeRef);
|
||||
const shortName = getShortName(ing.baseRecipeRef);
|
||||
germanBaseRecipeShortNames.add(shortName);
|
||||
baseRecipeRefMap.set(shortName, ing.baseRecipeRef);
|
||||
}
|
||||
});
|
||||
(germanData.instructions || []).forEach((inst: any) => {
|
||||
if (inst.type === 'reference' && inst.baseRecipeRef) {
|
||||
germanBaseRecipeIds.add(inst.baseRecipeRef);
|
||||
const shortName = getShortName(inst.baseRecipeRef);
|
||||
germanBaseRecipeShortNames.add(shortName);
|
||||
baseRecipeRefMap.set(shortName, inst.baseRecipeRef);
|
||||
}
|
||||
});
|
||||
|
||||
// If no base recipes in German, just initialize editableEnglish from German data if needed
|
||||
if (germanBaseRecipeIds.size === 0) {
|
||||
if (germanBaseRecipeShortNames.size === 0) {
|
||||
if (!editableEnglish) {
|
||||
editableEnglish = {
|
||||
...germanData,
|
||||
@@ -64,25 +75,25 @@
|
||||
}
|
||||
|
||||
// Fetch all base recipes and check their English translations
|
||||
const untranslated: { id: string, name: string }[] = [];
|
||||
const untranslated: { shortName: string, name: string }[] = [];
|
||||
const baseRecipeTranslations = new Map<string, { deName: string, enName: string }>();
|
||||
|
||||
for (const recipeId of germanBaseRecipeIds) {
|
||||
for (const shortName of germanBaseRecipeShortNames) {
|
||||
try {
|
||||
const response = await fetch(`/api/rezepte/${recipeId}`);
|
||||
const response = await fetch(`/api/rezepte/items/${shortName}`);
|
||||
if (response.ok) {
|
||||
const recipe = await response.json();
|
||||
if (!recipe.translations?.en) {
|
||||
untranslated.push({ id: recipeId, name: recipe.name });
|
||||
untranslated.push({ shortName, name: recipe.name });
|
||||
} else {
|
||||
baseRecipeTranslations.set(recipeId, {
|
||||
baseRecipeTranslations.set(shortName, {
|
||||
deName: recipe.name,
|
||||
enName: recipe.translations.en.name
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching base recipe ${recipeId}:`, error);
|
||||
console.error(`Error fetching base recipe ${shortName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,14 +115,16 @@
|
||||
translationStatus: 'pending',
|
||||
ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])).map((ing: any) => {
|
||||
if (ing.type === 'reference' && ing.baseRecipeRef) {
|
||||
const translation = baseRecipeTranslations.get(ing.baseRecipeRef);
|
||||
const shortName = getShortName(ing.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
return translation ? { ...ing, name: translation.enName } : ing;
|
||||
}
|
||||
return ing;
|
||||
}),
|
||||
instructions: JSON.parse(JSON.stringify(germanData.instructions || [])).map((inst: any) => {
|
||||
if (inst.type === 'reference' && inst.baseRecipeRef) {
|
||||
const translation = baseRecipeTranslations.get(inst.baseRecipeRef);
|
||||
const shortName = getShortName(inst.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
return translation ? { ...inst, name: translation.enName } : inst;
|
||||
}
|
||||
return inst;
|
||||
@@ -125,7 +138,8 @@
|
||||
ingredients: germanData.ingredients.map((germanIng: any, index: number) => {
|
||||
if (germanIng.type === 'reference' && germanIng.baseRecipeRef) {
|
||||
// This is a base recipe reference - use English base recipe name
|
||||
const translation = baseRecipeTranslations.get(germanIng.baseRecipeRef);
|
||||
const shortName = getShortName(germanIng.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
const englishIng = editableEnglish.ingredients[index];
|
||||
|
||||
// If English already has this reference at same position, keep it
|
||||
@@ -148,7 +162,8 @@
|
||||
instructions: germanData.instructions.map((germanInst: any, index: number) => {
|
||||
if (germanInst.type === 'reference' && germanInst.baseRecipeRef) {
|
||||
// This is a base recipe reference - use English base recipe name
|
||||
const translation = baseRecipeTranslations.get(germanInst.baseRecipeRef);
|
||||
const shortName = getShortName(germanInst.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
const englishInst = editableEnglish.instructions[index];
|
||||
|
||||
// If English already has this reference at same position, keep it
|
||||
|
||||
@@ -17,11 +17,27 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
})
|
||||
.populate({
|
||||
path: 'translations.en.ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations'
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.populate({
|
||||
path: 'translations.en.instructions.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations'
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.lean();
|
||||
|
||||
@@ -64,23 +80,32 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
germanShortName: recipe.short_name,
|
||||
};
|
||||
|
||||
// Map populated base recipe refs to resolvedRecipe field
|
||||
if (englishRecipe.ingredients) {
|
||||
englishRecipe.ingredients = englishRecipe.ingredients.map((item: any) => {
|
||||
// Recursively map populated base recipe refs to resolvedRecipe field
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
return items.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
const resolvedRecipe = { ...item.baseRecipeRef };
|
||||
|
||||
// Recursively map nested baseRecipeRefs
|
||||
if (resolvedRecipe.ingredients) {
|
||||
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
|
||||
}
|
||||
if (resolvedRecipe.instructions) {
|
||||
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
|
||||
}
|
||||
|
||||
return { ...item, resolvedRecipe };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (englishRecipe.ingredients) {
|
||||
englishRecipe.ingredients = mapBaseRecipeRefs(englishRecipe.ingredients);
|
||||
}
|
||||
|
||||
if (englishRecipe.instructions) {
|
||||
englishRecipe.instructions = englishRecipe.instructions.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
englishRecipe.instructions = mapBaseRecipeRefs(englishRecipe.instructions);
|
||||
}
|
||||
|
||||
// Merge English alt/caption with original image paths
|
||||
|
||||
@@ -9,11 +9,27 @@ export const GET: RequestHandler = async ({params}) => {
|
||||
let recipe = await Recipe.findOne({ short_name: params.name})
|
||||
.populate({
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations'
|
||||
select: 'short_name name ingredients translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.populate({
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations'
|
||||
select: 'short_name name instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.lean() as RecipeModelType[];
|
||||
|
||||
@@ -22,23 +38,32 @@ export const GET: RequestHandler = async ({params}) => {
|
||||
throw error(404, "Recipe not found")
|
||||
}
|
||||
|
||||
// Map populated refs to resolvedRecipe field
|
||||
if (recipe?.ingredients) {
|
||||
recipe.ingredients = recipe.ingredients.map((item: any) => {
|
||||
// Recursively map populated refs to resolvedRecipe field
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
return items.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
const resolvedRecipe = { ...item.baseRecipeRef };
|
||||
|
||||
// Recursively map nested baseRecipeRefs
|
||||
if (resolvedRecipe.ingredients) {
|
||||
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
|
||||
}
|
||||
if (resolvedRecipe.instructions) {
|
||||
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
|
||||
}
|
||||
|
||||
return { ...item, resolvedRecipe };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (recipe?.ingredients) {
|
||||
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients);
|
||||
}
|
||||
|
||||
if (recipe?.instructions) {
|
||||
recipe.instructions = recipe.instructions.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
recipe.instructions = mapBaseRecipeRefs(recipe.instructions);
|
||||
}
|
||||
|
||||
return json(recipe);
|
||||
|
||||
Reference in New Issue
Block a user