Files
encoach_ui_odoo19/src/utils/exam.differences.ts
2025-03-02 14:27:17 +00:00

169 lines
6.1 KiB
TypeScript

import { Exam } from "@/interfaces/exam";
const EXCLUDED_KEYS = new Set<string>(["_id", "id", "uuid", "isDiagnostic", "owners", "entities", "createdAt", "createdBy", "access", "requiresApproval", "exerciseID", "questionID", "sectionId", "userSolutions"]);
const PATH_LABELS: Record<string, string> = {
access: "Access Type",
parts: "Parts",
exercises: "Exercises",
userSolutions: "User Solutions",
words: "Words",
options: "Options",
prompt: "Prompt",
text: "Text",
audio: "Audio",
script: "Script",
difficulty: "Difficulty",
shuffle: "Shuffle",
solutions: "Solutions",
variant: "Variant",
prefix: "Prefix",
suffix: "Suffix",
topic: "Topic",
allowRepetition: "Allow Repetition",
maxWords: "Max Words",
minTimer: "Timer",
section: "Section",
module: "Module",
type: "Type",
intro: "Intro",
category: "Category",
context: "Context",
instructions: "Instructions",
name: "Name",
gender: "Gender",
voice: "Voice",
enableNavigation: "Enable Navigation",
limit: "Limit",
instructorGender: "Instructor Gender",
wordCounter: "Word Counter",
attachment: "Attachment",
first_title: "First Title",
second_title: "Second Title",
first_topic: "First Topic",
second_topic: "Second Topic",
questions: "Questions",
sentences: "Sentences",
sentence: "Sentence",
solution: "Solution",
passage: "Passage",
};
const ARRAY_ITEM_LABELS: Record<string, string> = {
exercises: "Exercise",
paths: "Path",
difficulties: "Difficulty",
solutions: "Solution",
options: "Option",
words: "Word",
questions: "Question",
userSolutions: "User Solution",
sentences: "Sentence",
parts: "Part",
};
export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] {
const differences: string[] = [];
compareObjects(oldExam, newExam, [], differences);
return differences;
}
function isObject(val: any): val is Record<string, any> {
return val !== null && typeof val === "object" && !Array.isArray(val);
}
function formatPrimitive(value: any): string {
if (value === undefined) return "undefined";
if (value === null) return "null";
return JSON.stringify(value);
}
function pathToHumanReadable(pathSegments: Array<string | number>): string {
const mapped = pathSegments.map((seg) => {
if (typeof seg === "number") {
return `#${seg + 1}`;
}
return PATH_LABELS[seg] ?? seg;
});
let result = "";
for (let i = 0; i < mapped.length; i++) {
result += mapped[i];
if (mapped[i].startsWith("#") && i < mapped.length - 1) {
result += " - ";
} else if (i < mapped.length - 1) {
result += " ";
}
}
return result.trim();
}
function getArrayItemLabel(path: (string | number)[]): string {
if (path.length === 0) return "item";
const lastSegment = path[path.length - 1];
if (typeof lastSegment === "string" && ARRAY_ITEM_LABELS[lastSegment]) {
return ARRAY_ITEM_LABELS[lastSegment];
}
return "item";
}
function getIdentifier(item: any): string | number | undefined {
if (item?.uuid !== undefined) return item.uuid;
if (item?.id !== undefined) return item.id;
return undefined;
}
function compareObjects(oldObj: any, newObj: any, path: (string | number)[], differences: string[]) {
if (Array.isArray(oldObj) && Array.isArray(newObj)) {
// Check if array elements are objects with an identifier (uuid or id).
if (oldObj.length > 0 && typeof oldObj[0] === "object" && getIdentifier(oldObj[0]) !== undefined) {
// Process removed items
const newIds = new Set(newObj.map((item: any) => getIdentifier(item)));
for (let i = 0; i < oldObj.length; i++) {
const oldItem = oldObj[i];
const identifier = getIdentifier(oldItem);
if (identifier !== undefined && !newIds.has(identifier)) {
differences.push(`• Removed ${getArrayItemLabel(path)} #${i + 1} from ${pathToHumanReadable(path)}\n`);
}
}
const oldIndexMap = new Map(oldObj.map((item: any, index: number) => [getIdentifier(item), index]));
// Process items in the new array using their order.
for (let i = 0; i < newObj.length; i++) {
const newItem = newObj[i];
const identifier = getIdentifier(newItem);
if (identifier !== undefined) {
if (oldIndexMap.has(identifier)) {
const oldIndex = oldIndexMap.get(identifier)!;
const oldItem = oldObj[oldIndex];
compareObjects(oldItem, newItem, path.concat(`#${i + 1}`), differences);
} else {
differences.push(`• Added new ${getArrayItemLabel(path)} #${i + 1} at ${pathToHumanReadable(path)}\n`);
}
} else {
// Fallback: if item does not have an identifier, compare by index.
compareObjects(oldObj[i], newItem, path.concat(`#${i + 1}`), differences);
}
}
} else {
// For arrays that are not identifier-based, compare element by element.
const maxLength = Math.max(oldObj.length, newObj.length);
for (let i = 0; i < maxLength; i++) {
compareObjects(oldObj[i], newObj[i], path.concat(`#${i + 1}`), differences);
}
}
} else if (isObject(oldObj) && isObject(newObj)) {
// Compare objects by keys (ignoring excluded keys).
const keys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
for (const key of keys) {
if (EXCLUDED_KEYS.has(key)) continue;
compareObjects(oldObj[key], newObj[key], path.concat(key), differences);
}
} else {
if (oldObj !== newObj) {
differences.push(`• Changed ${pathToHumanReadable(path)} from:\n ${formatPrimitive(oldObj)}\n To:\n ${formatPrimitive(newObj)}\n`);
}
}
}