Initial commit - LoreMind project

This commit is contained in:
2026-04-19 12:08:16 +02:00
parent 95928b7165
commit 094c759f2c
213 changed files with 25358 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import { forkJoin, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { LoreService } from '../services/lore.service';
import { TemplateService } from '../services/template.service';
import { PageService } from '../services/page.service';
import { Lore, LoreNode } from '../services/lore.model';
import { Template } from '../services/template.model';
import { Page } from '../services/page.model';
import {
SecondarySidebarConfig, TreeItem, GlobalItem, BottomPanel
} from '../services/layout.service';
export interface LoreSidebarData {
lore: Lore;
allLores: Lore[];
nodes: LoreNode[];
templates: Template[];
pages: Page[];
}
/**
* Charge toutes les données nécessaires à la sidebar d'un Lore en parallèle.
* Centralise le pattern commun aux écrans lore-detail, lore-node-create,
* template-create, template-edit, page-create, page-edit.
*/
export function loadLoreSidebarData(
loreId: string,
loreService: LoreService,
templateService: TemplateService,
pageService: PageService
): Observable<LoreSidebarData> {
return forkJoin({
lore: loreService.getLoreById(loreId),
allLores: loreService.getAllLores(),
nodes: loreService.getLoreNodes(loreId),
templates: templateService.getByLoreId(loreId),
pages: pageService.getByLoreId(loreId)
}).pipe(
map(data => data as LoreSidebarData)
);
}
/**
* Construit la config complète de la SecondarySidebar pour un Lore, incluant
* le panneau "Templates" en bas.
*/
export function buildLoreSidebarConfig(data: LoreSidebarData): SecondarySidebarConfig {
const { lore, allLores, nodes, templates, pages } = data;
// Regroupe les pages par nodeId et les sous-dossiers par parentId pour un accès O(1).
const pagesByNode = new Map<string, Page[]>();
for (const p of pages) {
const bucket = pagesByNode.get(p.nodeId) ?? [];
bucket.push(p);
pagesByNode.set(p.nodeId, bucket);
}
const childrenByParent = new Map<string, LoreNode[]>();
for (const n of nodes) {
const parentKey = n.parentId ?? '__root__';
const bucket = childrenByParent.get(parentKey) ?? [];
bucket.push(n);
childrenByParent.set(parentKey, bucket);
}
/**
* Construit récursivement le TreeItem d'un dossier :
* ses sous-dossiers, puis ses pages, puis les actions "+ Nouveau dossier" et "+ Nouvelle page".
*/
const buildFolderItem = (node: LoreNode): TreeItem => {
const subFolders = childrenByParent.get(node.id!) ?? [];
const nodePages = pagesByNode.get(node.id!) ?? [];
const children: TreeItem[] = [
...subFolders.map(buildFolderItem),
...nodePages.map(p => ({
id: p.id!,
label: p.title,
route: `/lore/${lore.id}/pages/${p.id}`
})),
{
id: `create-folder-${node.id}`,
label: '+ Nouveau dossier',
isAction: true,
route: `/lore/${lore.id}/folders/${node.id}/create`
},
{
id: `create-page-${node.id}`,
label: '+ Nouvelle page',
isAction: true,
route: `/lore/${lore.id}/nodes/${node.id}/pages/create`
}
];
return {
id: node.id!,
label: node.name,
iconKey: node.icon ?? undefined,
route: `/lore/${lore.id}/folders/${node.id}/edit`,
meta: nodePages.length > 0 ? String(nodePages.length) : undefined,
children
};
};
// L'arbre démarre aux dossiers racine (parentId nul ou vide).
const rootFolders = childrenByParent.get('__root__') ?? [];
const treeItems: TreeItem[] = rootFolders.map(buildFolderItem);
const globalItems: GlobalItem[] = allLores.map(l => ({
id: l.id!, name: l.name, route: `/lore/${l.id}`
}));
const templatesPanel: BottomPanel = {
id: 'templates',
title: 'Templates',
initiallyOpen: true,
items: [
...templates.map(t => ({
id: t.id!,
label: t.name,
meta: `${t.fieldCount ?? t.fields.length} champs`,
route: `/lore/${lore.id}/templates/${t.id}`
})),
{
id: 'create-template',
label: '+ Nouveau template',
isAction: true,
route: `/lore/${lore.id}/templates/create`
}
]
};
return {
title: lore.name,
items: treeItems,
createActions: [
{ id: 'create-node', label: '+ Dossier', variant: 'primary', route: `/lore/${lore.id}/nodes/create` },
{ id: 'create-page', label: '+ Page', variant: 'secondary', route: `/lore/${lore.id}/pages/create` }
],
globalItems,
globalBackLabel: 'Tous les lores',
globalBackRoute: '/lore',
bottomPanel: templatesPanel
};
}
/**
* Retourne l'ensemble des IDs de `rootId` et de tous ses descendants (sous-dossiers).
* Utilisé pour empêcher de choisir comme parent un dossier qui serait soi-même
* ou un de ses descendants (ce qui créerait un cycle dans l'arbre).
*/
export function collectDescendantIds(rootId: string, allNodes: LoreNode[]): Set<string> {
const ids = new Set<string>([rootId]);
let grew = true;
while (grew) {
grew = false;
for (const n of allNodes) {
if (n.parentId && ids.has(n.parentId) && !ids.has(n.id!)) {
ids.add(n.id!);
grew = true;
}
}
}
return ids;
}