Fix : problème d'ascenseur en bas de la page au niveau des templates
Sélection du template par défaut lors de la création d'une page en fonction du dossier Passage v0.6.2
This commit is contained in:
@@ -40,7 +40,7 @@ from app.infrastructure.onemin_adapter import OneMinAiLLMProvider
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="LoreMind Brain",
|
title="LoreMind Brain",
|
||||||
description="Backend IA pour la génération de contenu narratif.",
|
description="Backend IA pour la génération de contenu narratif.",
|
||||||
version="0.6.1",
|
version="0.6.2",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<groupId>com.loremind</groupId>
|
<groupId>com.loremind</groupId>
|
||||||
<artifactId>loremind-core</artifactId>
|
<artifactId>loremind-core</artifactId>
|
||||||
<version>0.6.1</version>
|
<version>0.6.2</version>
|
||||||
<name>LoreMind Core</name>
|
<name>LoreMind Core</name>
|
||||||
<description>Backend Core - Architecture Hexagonale</description>
|
<description>Backend Core - Architecture Hexagonale</description>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"description": "LoreMind Frontend - Angular",
|
"description": "LoreMind Frontend - Angular",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -87,7 +87,8 @@
|
|||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
border-color: #6c63ff;
|
border-color: #6c63ff;
|
||||||
background: #1e1c3a;
|
background: #2a2558;
|
||||||
|
box-shadow: 0 0 0 1px #6c63ff, 0 0 12px rgba(108, 99, 255, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-card-head {
|
.template-card-head {
|
||||||
|
|||||||
@@ -94,8 +94,28 @@ export class PageCreateComponent implements OnInit, OnDestroy {
|
|||||||
if (this.preselectedNodeId) {
|
if (this.preselectedNodeId) {
|
||||||
this.form.patchValue({ nodeId: this.preselectedNodeId });
|
this.form.patchValue({ nodeId: this.preselectedNodeId });
|
||||||
this.form.get('nodeId')?.disable();
|
this.form.get('nodeId')?.disable();
|
||||||
|
this.autoSelectTemplateForNode(this.preselectedNodeId);
|
||||||
|
} else {
|
||||||
|
// Pas de nodeId dans l'URL : le <select> affiche visuellement la
|
||||||
|
// première option mais la valeur du FormControl reste ''. On tente
|
||||||
|
// l'auto-sélection inverse : si un seul template a un defaultNodeId
|
||||||
|
// qui pointe sur un dossier existant, on le sélectionne et on
|
||||||
|
// pré-remplit le dossier — sinon on laisse l'utilisateur choisir.
|
||||||
|
const validNodeIds = new Set(this.nodes.map(n => n.id));
|
||||||
|
const candidates = this.templates.filter(
|
||||||
|
t => t.defaultNodeId && validNodeIds.has(t.defaultNodeId)
|
||||||
|
);
|
||||||
|
if (candidates.length === 1) {
|
||||||
|
const tpl = candidates[0];
|
||||||
|
this.selectedTemplateId = tpl.id!;
|
||||||
|
this.form.patchValue({ nodeId: tpl.defaultNodeId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.form.get('nodeId')?.valueChanges.subscribe(nodeId => {
|
||||||
|
this.autoSelectTemplateForNode(nodeId);
|
||||||
|
});
|
||||||
|
|
||||||
this.restoreDraft();
|
this.restoreDraft();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,6 +157,18 @@ export class PageCreateComponent implements OnInit, OnDestroy {
|
|||||||
} catch { /* JSON corrompu : on ignore */ }
|
} catch { /* JSON corrompu : on ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-sélection du template dont defaultNodeId === nodeId courant.
|
||||||
|
* Ne fait rien si l'utilisateur a déjà choisi un template manuellement
|
||||||
|
* (on ne veut pas écraser un choix explicite).
|
||||||
|
*/
|
||||||
|
private autoSelectTemplateForNode(nodeId: string | null | undefined): void {
|
||||||
|
if (!nodeId) return;
|
||||||
|
if (this.selectedTemplateId) return;
|
||||||
|
const matching = this.templates.find(t => t.defaultNodeId === nodeId);
|
||||||
|
if (matching) this.selectedTemplateId = matching.id!;
|
||||||
|
}
|
||||||
|
|
||||||
selectTemplate(template: Template): void {
|
selectTemplate(template: Template): void {
|
||||||
this.selectedTemplateId = template.id!;
|
this.selectedTemplateId = template.id!;
|
||||||
// Si pas de noeud pré-choisi par l'URL, on pré-remplit avec le defaultNodeId du template.
|
// Si pas de noeud pré-choisi par l'URL, on pré-remplit avec le defaultNodeId du template.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { shareReplay, tap } from 'rxjs/operators';
|
||||||
import { Lore, LoreCreate, LoreNode, LoreNodeCreate } from './lore.model';
|
import { Lore, LoreCreate, LoreNode, LoreNodeCreate } from './lore.model';
|
||||||
|
|
||||||
/** Compte des entités qui seront supprimées en cascade avec un dossier. */
|
/** Compte des entités qui seront supprimées en cascade avec un dossier. */
|
||||||
@@ -23,6 +24,11 @@ export interface LoreDeletionImpact {
|
|||||||
/**
|
/**
|
||||||
* Service HTTP pour la gestion des Lores.
|
* Service HTTP pour la gestion des Lores.
|
||||||
* Port de sortie vers le Backend Java (Architecture Hexagonale).
|
* Port de sortie vers le Backend Java (Architecture Hexagonale).
|
||||||
|
*
|
||||||
|
* Les lectures agrégées par la sidebar (getAllLores, getLoreById, getLoreNodes)
|
||||||
|
* sont mises en cache via `shareReplay(1)` pour éviter 5 fetchs redondants à
|
||||||
|
* chaque navigation interne. Toute mutation (create/update/delete) invalide
|
||||||
|
* l'ensemble du cache du service.
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -31,26 +37,51 @@ export class LoreService {
|
|||||||
private apiUrl = '/api/lores';
|
private apiUrl = '/api/lores';
|
||||||
private nodesUrl = '/api/lore-nodes';
|
private nodesUrl = '/api/lore-nodes';
|
||||||
|
|
||||||
|
private allLoresCache: Observable<Lore[]> | null = null;
|
||||||
|
private loreByIdCache = new Map<string, Observable<Lore>>();
|
||||||
|
private nodesByLoreIdCache = new Map<string, Observable<LoreNode[]>>();
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
/** Vide tous les caches de lecture — appelé après toute mutation. */
|
||||||
|
private invalidate(): void {
|
||||||
|
this.allLoresCache = null;
|
||||||
|
this.loreByIdCache.clear();
|
||||||
|
this.nodesByLoreIdCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
getAllLores(): Observable<Lore[]> {
|
getAllLores(): Observable<Lore[]> {
|
||||||
return this.http.get<Lore[]>(this.apiUrl);
|
if (!this.allLoresCache) {
|
||||||
|
this.allLoresCache = this.http.get<Lore[]>(this.apiUrl).pipe(
|
||||||
|
tap({ error: () => (this.allLoresCache = null) }),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.allLoresCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoreById(id: string): Observable<Lore> {
|
getLoreById(id: string): Observable<Lore> {
|
||||||
return this.http.get<Lore>(`${this.apiUrl}/${id}`);
|
let obs = this.loreByIdCache.get(id);
|
||||||
|
if (!obs) {
|
||||||
|
obs = this.http.get<Lore>(`${this.apiUrl}/${id}`).pipe(
|
||||||
|
tap({ error: () => this.loreByIdCache.delete(id) }),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.loreByIdCache.set(id, obs);
|
||||||
|
}
|
||||||
|
return obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
createLore(lore: LoreCreate): Observable<Lore> {
|
createLore(lore: LoreCreate): Observable<Lore> {
|
||||||
return this.http.post<Lore>(this.apiUrl, lore);
|
return this.http.post<Lore>(this.apiUrl, lore).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLore(id: string, lore: LoreCreate): Observable<Lore> {
|
updateLore(id: string, lore: LoreCreate): Observable<Lore> {
|
||||||
return this.http.put<Lore>(`${this.apiUrl}/${id}`, lore);
|
return this.http.put<Lore>(`${this.apiUrl}/${id}`, lore).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLore(id: string): Observable<void> {
|
deleteLore(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoreDeletionImpact(id: string): Observable<LoreDeletionImpact> {
|
getLoreDeletionImpact(id: string): Observable<LoreDeletionImpact> {
|
||||||
@@ -58,7 +89,15 @@ export class LoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLoreNodes(loreId: string): Observable<LoreNode[]> {
|
getLoreNodes(loreId: string): Observable<LoreNode[]> {
|
||||||
return this.http.get<LoreNode[]>(`${this.nodesUrl}?loreId=${loreId}`);
|
let obs = this.nodesByLoreIdCache.get(loreId);
|
||||||
|
if (!obs) {
|
||||||
|
obs = this.http.get<LoreNode[]>(`${this.nodesUrl}?loreId=${loreId}`).pipe(
|
||||||
|
tap({ error: () => this.nodesByLoreIdCache.delete(loreId) }),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.nodesByLoreIdCache.set(loreId, obs);
|
||||||
|
}
|
||||||
|
return obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoreNodeById(id: string): Observable<LoreNode> {
|
getLoreNodeById(id: string): Observable<LoreNode> {
|
||||||
@@ -66,16 +105,16 @@ export class LoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createLoreNode(node: LoreNodeCreate): Observable<LoreNode> {
|
createLoreNode(node: LoreNodeCreate): Observable<LoreNode> {
|
||||||
return this.http.post<LoreNode>(this.nodesUrl, node);
|
return this.http.post<LoreNode>(this.nodesUrl, node).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PUT complet — envoie l'objet entier au backend (qui attend un LoreNodeDTO). */
|
/** PUT complet — envoie l'objet entier au backend (qui attend un LoreNodeDTO). */
|
||||||
updateLoreNode(id: string, node: LoreNode): Observable<LoreNode> {
|
updateLoreNode(id: string, node: LoreNode): Observable<LoreNode> {
|
||||||
return this.http.put<LoreNode>(`${this.nodesUrl}/${id}`, node);
|
return this.http.put<LoreNode>(`${this.nodesUrl}/${id}`, node).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLoreNode(id: string): Observable<void> {
|
deleteLoreNode(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`${this.nodesUrl}/${id}`);
|
return this.http.delete<void>(`${this.nodesUrl}/${id}`).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoreNodeDeletionImpact(id: string): Observable<LoreNodeDeletionImpact> {
|
getLoreNodeDeletionImpact(id: string): Observable<LoreNodeDeletionImpact> {
|
||||||
|
|||||||
@@ -1,23 +1,40 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, shareReplay, tap } from 'rxjs/operators';
|
||||||
import { Page, PageCreate } from './page.model';
|
import { Page, PageCreate } from './page.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service HTTP pour la gestion des Pages.
|
* Service HTTP pour la gestion des Pages.
|
||||||
* Port de sortie du Frontend vers le Backend Java (/api/pages).
|
* Port de sortie du Frontend vers le Backend Java (/api/pages).
|
||||||
|
*
|
||||||
|
* `getByLoreId` est cache via shareReplay(1) — toute mutation
|
||||||
|
* (create/update/delete) invalide l'ensemble du cache.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PageService {
|
export class PageService {
|
||||||
private apiUrl = '/api/pages';
|
private apiUrl = '/api/pages';
|
||||||
|
|
||||||
|
private byLoreIdCache = new Map<string, Observable<Page[]>>();
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
private invalidate(): void {
|
||||||
|
this.byLoreIdCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/** Toutes les pages d'un Lore (flat, pour répartir ensuite par nodeId). */
|
/** Toutes les pages d'un Lore (flat, pour répartir ensuite par nodeId). */
|
||||||
getByLoreId(loreId: string): Observable<Page[]> {
|
getByLoreId(loreId: string): Observable<Page[]> {
|
||||||
const params = new HttpParams().set('loreId', loreId);
|
let obs = this.byLoreIdCache.get(loreId);
|
||||||
return this.http.get<Page[]>(this.apiUrl, { params });
|
if (!obs) {
|
||||||
|
const params = new HttpParams().set('loreId', loreId);
|
||||||
|
obs = this.http.get<Page[]>(this.apiUrl, { params }).pipe(
|
||||||
|
tap({ error: () => this.byLoreIdCache.delete(loreId) }),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.byLoreIdCache.set(loreId, obs);
|
||||||
|
}
|
||||||
|
return obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Toutes les pages d'un noeud donné. */
|
/** Toutes les pages d'un noeud donné. */
|
||||||
@@ -31,15 +48,15 @@ export class PageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(payload: PageCreate): Observable<Page> {
|
create(payload: PageCreate): Observable<Page> {
|
||||||
return this.http.post<Page>(this.apiUrl, payload);
|
return this.http.post<Page>(this.apiUrl, payload).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, page: Page): Observable<Page> {
|
update(id: string, page: Page): Observable<Page> {
|
||||||
return this.http.put<Page>(`${this.apiUrl}/${id}`, page);
|
return this.http.put<Page>(`${this.apiUrl}/${id}`, page).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: string): Observable<void> {
|
delete(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
search(q: string): Observable<Page[]> {
|
search(q: string): Observable<Page[]> {
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { shareReplay, tap } from 'rxjs/operators';
|
||||||
import { Template, TemplateCreate } from './template.model';
|
import { Template, TemplateCreate } from './template.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service HTTP pour la gestion des Templates.
|
* Service HTTP pour la gestion des Templates.
|
||||||
* Port de sortie du Frontend vers le Backend Java (/api/templates).
|
* Port de sortie du Frontend vers le Backend Java (/api/templates).
|
||||||
|
*
|
||||||
|
* `getByLoreId` est cache via shareReplay(1) — toute mutation
|
||||||
|
* (create/update/delete) invalide l'ensemble du cache.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TemplateService {
|
export class TemplateService {
|
||||||
private apiUrl = '/api/templates';
|
private apiUrl = '/api/templates';
|
||||||
|
|
||||||
|
private byLoreIdCache = new Map<string, Observable<Template[]>>();
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
private invalidate(): void {
|
||||||
|
this.byLoreIdCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/** Tous les templates d'un Lore (alimente le panneau sidebar). */
|
/** Tous les templates d'un Lore (alimente le panneau sidebar). */
|
||||||
getByLoreId(loreId: string): Observable<Template[]> {
|
getByLoreId(loreId: string): Observable<Template[]> {
|
||||||
const params = new HttpParams().set('loreId', loreId);
|
let obs = this.byLoreIdCache.get(loreId);
|
||||||
return this.http.get<Template[]>(this.apiUrl, { params });
|
if (!obs) {
|
||||||
|
const params = new HttpParams().set('loreId', loreId);
|
||||||
|
obs = this.http.get<Template[]>(this.apiUrl, { params }).pipe(
|
||||||
|
tap({ error: () => this.byLoreIdCache.delete(loreId) }),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
this.byLoreIdCache.set(loreId, obs);
|
||||||
|
}
|
||||||
|
return obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(id: string): Observable<Template> {
|
getById(id: string): Observable<Template> {
|
||||||
@@ -24,15 +42,15 @@ export class TemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(payload: TemplateCreate): Observable<Template> {
|
create(payload: TemplateCreate): Observable<Template> {
|
||||||
return this.http.post<Template>(this.apiUrl, payload);
|
return this.http.post<Template>(this.apiUrl, payload).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, template: Template): Observable<Template> {
|
update(id: string, template: Template): Observable<Template> {
|
||||||
return this.http.put<Template>(`${this.apiUrl}/${id}`, template);
|
return this.http.put<Template>(`${this.apiUrl}/${id}`, template).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: string): Observable<void> {
|
delete(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(tap(() => this.invalidate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
search(q: string): Observable<Template[]> {
|
search(q: string): Observable<Template[]> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1.25rem 0.75rem;
|
padding: 1.25rem 0.75rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
// Pas de transition sur la largeur : sinon le drag de resize "traîne" derrière la souris.
|
// Pas de transition sur la largeur : sinon le drag de resize "traîne" derrière la souris.
|
||||||
// L'animation d'expand/collapse est gérée uniquement par la classe .collapsed ci-dessous.
|
// L'animation d'expand/collapse est gérée uniquement par la classe .collapsed ci-dessous.
|
||||||
|
|||||||
Reference in New Issue
Block a user