Phase 1.1 : mise en page et edition visuelle des noeuds
Evolution incrementale : aucun changement d'architecture, aucune fonctionnalite retiree, compatibilite .mmap v1 et IndexedDB preservee. Modele (format v2) - MindMapNode gagne `layout`, `iconStyle` et `branch`, tous optionnels. - NodeStyle gagne fontFamily, verticalAlign, lineHeight et shadow. - MapSettings gagne layoutSpacing, snapToGrid, snapGrid et alignmentGuides. - Un champ absent signifie "herite du theme" : rien n'est ecrit dans le document pour materialiser un defaut, donc les cartes deja stockees restent lisibles telles quelles. Migration v1 -> v2 : style.width devient layout.width, toujours lu en repli par le resolveur. Mise en page - Nouveau resolveur de style qui fusionne theme et personnalisations. - Taille automatique fidele au contenu, taille personnalisee bornee, redimensionnement a la souris via NodeResizer. - Padding horizontal et vertical, retour a la ligne, largeur maximale du texte, interligne, police, alignement horizontal et vertical, ombre. - Icones : emojis conserves, bibliotheque Lucide ajoutee, position, taille et espacement configurables. Multi-selection - Barre contextuelle d'alignement (6 axes) et de distribution (2 axes). - Application d'un reglage a toute la selection, avec etat "Mixte". Style - Copier / coller / reinitialiser le style, au clavier et par clic droit. Auto-layout - Espacement parent/enfant et entre branches configurable, presets Compact, Normal et Aere. Le layout reste declenche uniquement a la demande. Historique - Une seule entree par redimensionnement complet, par alignement, par distribution, par application multiple et par collage de style. Panneau Proprietes - Reorganise en cinq sections repliables dont l'etat est conserve pendant la session. Vraies icones d'alignement, tooltips et aria-labels partout. Corrections - Largeur des noeuds : word-break et overflow-wrap:anywhere ramenaient la largeur min-content a un caractere, ce qui faisait s'effondrer tous les noeuds automatiques. - Largeur auto en max-content bornee, pour qu'une phrase longue occupe la largeur maximale au lieu de former une colonne etroite. Tests - 85 tests vitest (43 -> 85) et 31 tests pytest (25 -> 31). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
171
frontend/src/services/mapLayoutSerialization.test.ts
Normal file
171
frontend/src/services/mapLayoutSerialization.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MAP_FORMAT_VERSION, type MindMap } from '@/types/map';
|
||||
import { createEmptyMap } from '@/services/mapFactory';
|
||||
import { parseMapDocument, serializeMap } from '@/services/mapSerialization';
|
||||
|
||||
/** Carte v1 telle qu'elle etait exportee avant la phase 1.1. */
|
||||
const LEGACY_V1 = {
|
||||
version: 1,
|
||||
id: '11111111-1111-4111-8111-111111111111',
|
||||
name: 'Infrastructure v1',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
nodes: [
|
||||
{
|
||||
id: 'root',
|
||||
type: 'root',
|
||||
text: 'Infrastructure',
|
||||
position: { x: 0, y: 0 },
|
||||
parentId: null,
|
||||
icon: '🖥️',
|
||||
style: { backgroundColor: '#eef2ff', width: 240, textAlign: 'center' },
|
||||
},
|
||||
{ id: 'n1', type: 'standard', text: 'Reseau', position: { x: 260, y: 0 }, parentId: 'root' },
|
||||
],
|
||||
edges: [{ id: 'e-root-n1', source: 'root', target: 'n1' }],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
settings: { theme: 'default', layout: 'mindmap-right', branchStyle: 'bezier', showGrid: true },
|
||||
};
|
||||
|
||||
describe('compatibilite du format', () => {
|
||||
it('importe une carte v1 sans propriete de mise en page', () => {
|
||||
const map = parseMapDocument(LEGACY_V1);
|
||||
|
||||
expect(map.version).toBe(MAP_FORMAT_VERSION);
|
||||
expect(map.nodes).toHaveLength(2);
|
||||
expect(map.nodes[1]?.layout).toBeUndefined();
|
||||
expect(map.nodes[1]?.iconStyle).toBeUndefined();
|
||||
expect(map.settings.layoutSpacing).toBeUndefined();
|
||||
});
|
||||
|
||||
it('migre la largeur v1 vers layout.width', () => {
|
||||
const map = parseMapDocument(LEGACY_V1);
|
||||
const root = map.nodes[0];
|
||||
|
||||
expect(root?.layout?.widthMode).toBe('fixed');
|
||||
expect(root?.layout?.width).toBe(240);
|
||||
expect(root?.style?.width).toBeUndefined();
|
||||
// Les autres personnalisations v1 sont conservees.
|
||||
expect(root?.style?.backgroundColor).toBe('#eef2ff');
|
||||
expect(root?.style?.textAlign).toBe('center');
|
||||
expect(root?.icon).toBe('🖥️');
|
||||
});
|
||||
|
||||
it('refuse une version plus recente que celle supportee', () => {
|
||||
expect(() => parseMapDocument({ ...LEGACY_V1, version: MAP_FORMAT_VERSION + 1 })).toThrow(
|
||||
/plus recente/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aller-retour des nouvelles proprietes', () => {
|
||||
function richMap(): MindMap {
|
||||
const map = createEmptyMap('Mise en page');
|
||||
const root = map.nodes[0] as MindMap['nodes'][number];
|
||||
return {
|
||||
...map,
|
||||
nodes: [
|
||||
{
|
||||
...root,
|
||||
icon: 'server',
|
||||
iconStyle: { kind: 'builtin', position: 'top', size: 24, gap: 12 },
|
||||
style: {
|
||||
backgroundColor: '#112233',
|
||||
textColor: '#ffffff',
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: 1.6,
|
||||
fontFamily: 'mono',
|
||||
shadow: 'strong',
|
||||
fontSize: 18,
|
||||
bold: true,
|
||||
italic: true,
|
||||
shape: 'capsule',
|
||||
},
|
||||
layout: {
|
||||
widthMode: 'fixed',
|
||||
width: 320,
|
||||
heightMode: 'fixed',
|
||||
height: 140,
|
||||
paddingX: 28,
|
||||
paddingY: 18,
|
||||
wrap: false,
|
||||
maxTextWidth: 400,
|
||||
},
|
||||
branch: { style: 'straight', width: 4, color: '#ff8800' },
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
...map.settings,
|
||||
layoutSpacing: { parentGap: 140, siblingGap: 80 },
|
||||
snapToGrid: true,
|
||||
snapGrid: 10,
|
||||
alignmentGuides: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('conserve toute la mise en page apres export puis import', () => {
|
||||
const original = richMap();
|
||||
const restored = parseMapDocument(JSON.parse(serializeMap(original)));
|
||||
const node = restored.nodes[0];
|
||||
const source = original.nodes[0];
|
||||
|
||||
expect(node?.style).toEqual(source?.style);
|
||||
expect(node?.layout).toEqual(source?.layout);
|
||||
expect(node?.iconStyle).toEqual(source?.iconStyle);
|
||||
expect(node?.branch).toEqual(source?.branch);
|
||||
expect(node?.icon).toBe('server');
|
||||
expect(node?.position).toEqual(source?.position);
|
||||
expect(restored.settings.layoutSpacing).toEqual({ parentGap: 140, siblingGap: 80 });
|
||||
expect(restored.settings.snapToGrid).toBe(true);
|
||||
expect(restored.settings.snapGrid).toBe(10);
|
||||
expect(restored.settings.alignmentGuides).toBe(false);
|
||||
});
|
||||
|
||||
it('borne les valeurs aberrantes a l import', () => {
|
||||
const map = parseMapDocument({
|
||||
version: 2,
|
||||
id: 'x',
|
||||
nodes: [
|
||||
{
|
||||
id: 'a',
|
||||
type: 'root',
|
||||
text: 'Racine',
|
||||
layout: { width: 99999, paddingX: -20, maxTextWidth: 5 },
|
||||
branch: { width: 500 },
|
||||
iconStyle: { size: 900, gap: -5 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const node = map.nodes[0];
|
||||
expect(node?.layout?.width).toBeLessThanOrEqual(900);
|
||||
expect(node?.layout?.paddingX).toBeGreaterThanOrEqual(0);
|
||||
expect(node?.layout?.maxTextWidth).toBeGreaterThanOrEqual(80);
|
||||
expect(node?.branch?.width).toBeLessThanOrEqual(10);
|
||||
expect(node?.iconStyle?.size).toBeLessThanOrEqual(48);
|
||||
expect(node?.iconStyle?.gap).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('rejette les valeurs de mise en page invalides', () => {
|
||||
const map = parseMapDocument({
|
||||
version: 2,
|
||||
id: 'x',
|
||||
nodes: [
|
||||
{
|
||||
id: 'a',
|
||||
type: 'root',
|
||||
text: 'Racine',
|
||||
layout: { widthMode: 'elastique', wrap: 'oui' },
|
||||
iconStyle: { kind: 'svg', position: 'diagonale' },
|
||||
style: { verticalAlign: 'flottant', fontFamily: 'comic-sans' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(map.nodes[0]?.layout).toBeUndefined();
|
||||
expect(map.nodes[0]?.iconStyle).toBeUndefined();
|
||||
expect(map.nodes[0]?.style).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
importMap,
|
||||
listMaps,
|
||||
renameMap,
|
||||
saveMap,
|
||||
seedSampleMapOnce,
|
||||
} from '@/services/mapRepository';
|
||||
import { saveShareRecord } from '@/services/shareRepository';
|
||||
@@ -103,3 +104,49 @@ describe('stockage IndexedDB des cartes', () => {
|
||||
expect((await getMap(map.id))?.name).toBe('Persistante');
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistance de la mise en page', () => {
|
||||
it('conserve taille, padding et alignement apres reouverture', async () => {
|
||||
const map = await createMap('Mise en page');
|
||||
const rootNodeId = map.nodes[0]?.id as string;
|
||||
|
||||
await saveMap({
|
||||
...map,
|
||||
nodes: map.nodes.map((node) =>
|
||||
node.id === rootNodeId
|
||||
? {
|
||||
...node,
|
||||
style: { textAlign: 'center', verticalAlign: 'middle', lineHeight: 1.6 },
|
||||
layout: {
|
||||
widthMode: 'fixed',
|
||||
width: 280,
|
||||
heightMode: 'fixed',
|
||||
height: 120,
|
||||
paddingX: 26,
|
||||
paddingY: 16,
|
||||
wrap: false,
|
||||
},
|
||||
iconStyle: { kind: 'builtin', position: 'top', size: 24 },
|
||||
icon: 'server',
|
||||
}
|
||||
: node,
|
||||
),
|
||||
});
|
||||
|
||||
// Fermeture puis reouverture de la base, comme un redemarrage du navigateur.
|
||||
db.close();
|
||||
await db.open();
|
||||
|
||||
const stored = await getMap(map.id);
|
||||
const node = stored?.nodes.find((candidate) => candidate.id === rootNodeId);
|
||||
|
||||
expect(node?.layout?.width).toBe(280);
|
||||
expect(node?.layout?.height).toBe(120);
|
||||
expect(node?.layout?.paddingX).toBe(26);
|
||||
expect(node?.layout?.wrap).toBe(false);
|
||||
expect(node?.style?.textAlign).toBe('center');
|
||||
expect(node?.style?.verticalAlign).toBe('middle');
|
||||
expect(node?.icon).toBe('server');
|
||||
expect(node?.iconStyle?.position).toBe('top');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,15 +2,25 @@ import { APP_CONFIG } from '@/config/app';
|
||||
import {
|
||||
MAP_FORMAT_VERSION,
|
||||
type BranchStyle,
|
||||
type FontFamilyId,
|
||||
type IconPosition,
|
||||
type LayoutKind,
|
||||
type MapSettings,
|
||||
type MindMap,
|
||||
type MindMapNode,
|
||||
type NodeBranchStyle,
|
||||
type NodeIconStyle,
|
||||
type NodeKind,
|
||||
type NodeLayout,
|
||||
type NodeShape,
|
||||
type NodeStyle,
|
||||
type ShadowStyle,
|
||||
type SizeMode,
|
||||
type TextAlign,
|
||||
type VerticalAlign,
|
||||
} from '@/types/map';
|
||||
import { LAYOUT_LIMITS, clampTo } from '@/features/editor/nodeStyleResolver';
|
||||
import { SPACING_LIMITS } from '@/features/editor/layout';
|
||||
import { DEFAULT_SETTINGS } from '@/services/mapFactory';
|
||||
import { createId } from '@/utils/id';
|
||||
import { rebuildEdges, rebuildHierarchyFromEdges } from '@/utils/tree';
|
||||
@@ -31,6 +41,12 @@ const MAX_NAME_LENGTH = 200;
|
||||
const NODE_KINDS: NodeKind[] = ['root', 'standard', 'note', 'image', 'group', 'link', 'task'];
|
||||
const NODE_SHAPES: NodeShape[] = ['rectangle', 'rounded', 'capsule', 'circle'];
|
||||
const TEXT_ALIGNS: TextAlign[] = ['left', 'center', 'right'];
|
||||
const VERTICAL_ALIGNS: VerticalAlign[] = ['top', 'middle', 'bottom'];
|
||||
const SIZE_MODES: SizeMode[] = ['auto', 'fixed'];
|
||||
const SHADOW_STYLES: ShadowStyle[] = ['theme', 'none', 'soft', 'strong'];
|
||||
const FONT_FAMILIES: FontFamilyId[] = ['theme', 'sans', 'serif', 'mono', 'rounded'];
|
||||
const ICON_POSITIONS: IconPosition[] = ['left', 'right', 'top'];
|
||||
const ICON_KINDS = ['emoji', 'builtin'] as const;
|
||||
const BRANCH_STYLES: BranchStyle[] = ['straight', 'curved', 'bezier'];
|
||||
const LAYOUTS: LayoutKind[] = [
|
||||
'mindmap-right',
|
||||
@@ -95,10 +111,107 @@ function parseStyle(value: unknown): NodeStyle | undefined {
|
||||
if (typeof value.textAlign === 'string' && TEXT_ALIGNS.includes(value.textAlign as TextAlign)) {
|
||||
style.textAlign = value.textAlign as TextAlign;
|
||||
}
|
||||
if (
|
||||
typeof value.verticalAlign === 'string' &&
|
||||
VERTICAL_ALIGNS.includes(value.verticalAlign as VerticalAlign)
|
||||
) {
|
||||
style.verticalAlign = value.verticalAlign as VerticalAlign;
|
||||
}
|
||||
if (
|
||||
typeof value.fontFamily === 'string' &&
|
||||
FONT_FAMILIES.includes(value.fontFamily as FontFamilyId)
|
||||
) {
|
||||
style.fontFamily = value.fontFamily as FontFamilyId;
|
||||
}
|
||||
if (typeof value.shadow === 'string' && SHADOW_STYLES.includes(value.shadow as ShadowStyle)) {
|
||||
style.shadow = value.shadow as ShadowStyle;
|
||||
}
|
||||
if (typeof value.lineHeight === 'number' && Number.isFinite(value.lineHeight)) {
|
||||
style.lineHeight = Math.min(
|
||||
LAYOUT_LIMITS.lineHeight.max,
|
||||
Math.max(LAYOUT_LIMITS.lineHeight.min, Math.round(value.lineHeight * 100) / 100),
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(style).length > 0 ? style : undefined;
|
||||
}
|
||||
|
||||
/** Mise en page d'un noeud (v2). Chaque valeur est bornee. */
|
||||
function parseLayout(value: unknown): NodeLayout | undefined {
|
||||
if (!isRecord(value)) return undefined;
|
||||
|
||||
const layout: NodeLayout = {};
|
||||
if (typeof value.widthMode === 'string' && SIZE_MODES.includes(value.widthMode as SizeMode)) {
|
||||
layout.widthMode = value.widthMode as SizeMode;
|
||||
}
|
||||
if (typeof value.heightMode === 'string' && SIZE_MODES.includes(value.heightMode as SizeMode)) {
|
||||
layout.heightMode = value.heightMode as SizeMode;
|
||||
}
|
||||
if (typeof value.width === 'number' && Number.isFinite(value.width)) {
|
||||
layout.width = clampTo(value.width, LAYOUT_LIMITS.width);
|
||||
}
|
||||
if (typeof value.height === 'number' && Number.isFinite(value.height)) {
|
||||
layout.height = clampTo(value.height, LAYOUT_LIMITS.height);
|
||||
}
|
||||
if (typeof value.paddingX === 'number' && Number.isFinite(value.paddingX)) {
|
||||
layout.paddingX = clampTo(value.paddingX, LAYOUT_LIMITS.paddingX);
|
||||
}
|
||||
if (typeof value.paddingY === 'number' && Number.isFinite(value.paddingY)) {
|
||||
layout.paddingY = clampTo(value.paddingY, LAYOUT_LIMITS.paddingY);
|
||||
}
|
||||
if (typeof value.maxTextWidth === 'number' && Number.isFinite(value.maxTextWidth)) {
|
||||
layout.maxTextWidth = clampTo(value.maxTextWidth, LAYOUT_LIMITS.maxTextWidth);
|
||||
}
|
||||
if (typeof value.wrap === 'boolean') layout.wrap = value.wrap;
|
||||
|
||||
return Object.keys(layout).length > 0 ? layout : undefined;
|
||||
}
|
||||
|
||||
function parseIconStyle(value: unknown): NodeIconStyle | undefined {
|
||||
if (!isRecord(value)) return undefined;
|
||||
|
||||
const iconStyle: NodeIconStyle = {};
|
||||
if (
|
||||
typeof value.kind === 'string' &&
|
||||
(ICON_KINDS as readonly string[]).includes(value.kind)
|
||||
) {
|
||||
iconStyle.kind = value.kind as NodeIconStyle['kind'];
|
||||
}
|
||||
if (
|
||||
typeof value.position === 'string' &&
|
||||
ICON_POSITIONS.includes(value.position as IconPosition)
|
||||
) {
|
||||
iconStyle.position = value.position as IconPosition;
|
||||
}
|
||||
if (typeof value.size === 'number' && Number.isFinite(value.size)) {
|
||||
iconStyle.size = clampTo(value.size, LAYOUT_LIMITS.iconSize);
|
||||
}
|
||||
if (typeof value.gap === 'number' && Number.isFinite(value.gap)) {
|
||||
iconStyle.gap = clampTo(value.gap, LAYOUT_LIMITS.iconGap);
|
||||
}
|
||||
|
||||
return Object.keys(iconStyle).length > 0 ? iconStyle : undefined;
|
||||
}
|
||||
|
||||
function parseBranch(value: unknown): NodeBranchStyle | undefined {
|
||||
if (!isRecord(value)) return undefined;
|
||||
|
||||
const branch: NodeBranchStyle = {};
|
||||
if (
|
||||
typeof value.style === 'string' &&
|
||||
BRANCH_STYLES.includes(value.style as BranchStyle)
|
||||
) {
|
||||
branch.style = value.style as BranchStyle;
|
||||
}
|
||||
if (typeof value.width === 'number' && Number.isFinite(value.width)) {
|
||||
branch.width = clampTo(value.width, LAYOUT_LIMITS.branchWidth);
|
||||
}
|
||||
const color = asColor(value.color);
|
||||
if (color) branch.color = color;
|
||||
|
||||
return Object.keys(branch).length > 0 ? branch : undefined;
|
||||
}
|
||||
|
||||
function parseNode(value: unknown): MindMapNode | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
@@ -124,9 +237,13 @@ function parseNode(value: unknown): MindMapNode | null {
|
||||
|
||||
if (value.collapsed === true) node.collapsed = true;
|
||||
|
||||
const icon = asString(value.icon, 8);
|
||||
// 32 caracteres : suffisant pour un emoji compose ou un identifiant d'icone integree.
|
||||
const icon = asString(value.icon, 32);
|
||||
if (icon) node.icon = icon;
|
||||
|
||||
const iconStyle = parseIconStyle(value.iconStyle);
|
||||
if (iconStyle) node.iconStyle = iconStyle;
|
||||
|
||||
// Les URL sont assainies des l'import : aucun javascript: ne peut survivre.
|
||||
const url = sanitizeUrl(asString(value.url, 2048));
|
||||
if (url) node.url = url;
|
||||
@@ -137,6 +254,12 @@ function parseNode(value: unknown): MindMapNode | null {
|
||||
const style = parseStyle(value.style);
|
||||
if (style) node.style = style;
|
||||
|
||||
const layout = parseLayout(value.layout);
|
||||
if (layout) node.layout = layout;
|
||||
|
||||
const branch = parseBranch(value.branch);
|
||||
if (branch) node.branch = branch;
|
||||
|
||||
if (isRecord(value.meta)) node.meta = value.meta;
|
||||
|
||||
return node;
|
||||
@@ -157,22 +280,66 @@ function parseSettings(value: unknown): MapSettings {
|
||||
settings.branchStyle = value.branchStyle as BranchStyle;
|
||||
}
|
||||
if (typeof value.showGrid === 'boolean') settings.showGrid = value.showGrid;
|
||||
if (typeof value.snapToGrid === 'boolean') settings.snapToGrid = value.snapToGrid;
|
||||
if (typeof value.alignmentGuides === 'boolean') settings.alignmentGuides = value.alignmentGuides;
|
||||
if (typeof value.snapGrid === 'number' && Number.isFinite(value.snapGrid)) {
|
||||
settings.snapGrid = Math.min(100, Math.max(2, Math.round(value.snapGrid)));
|
||||
}
|
||||
|
||||
if (isRecord(value.layoutSpacing)) {
|
||||
const raw = value.layoutSpacing;
|
||||
const parentGap = typeof raw.parentGap === 'number' ? raw.parentGap : undefined;
|
||||
const siblingGap = typeof raw.siblingGap === 'number' ? raw.siblingGap : undefined;
|
||||
if (parentGap !== undefined && siblingGap !== undefined) {
|
||||
settings.layoutSpacing = {
|
||||
parentGap: clampTo(parentGap, SPACING_LIMITS.parentGap),
|
||||
siblingGap: clampTo(siblingGap, SPACING_LIMITS.siblingGap),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Point d'extension des migrations de format.
|
||||
* Chaque version future ajoutera son etape ici, de la plus ancienne a la plus
|
||||
* recente, avant la normalisation finale.
|
||||
* Migrations de format, appliquees de la plus ancienne a la plus recente.
|
||||
*
|
||||
* v1 -> v2 : la largeur fixe vivait dans `style.width`. Elle rejoint
|
||||
* `layout.width` avec un `widthMode` explicite. Les autres nouveautes de la v2
|
||||
* sont facultatives : leur absence signifie "herite du theme", il n'y a donc
|
||||
* rien a ecrire dans le document.
|
||||
*/
|
||||
function migrate(raw: Record<string, unknown>, version: number): Record<string, unknown> {
|
||||
const document = raw;
|
||||
const current = version;
|
||||
function migrateV1ToV2(document: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!Array.isArray(document.nodes)) return { ...document, version: 2 };
|
||||
|
||||
// Exemple d'evolution future :
|
||||
// if (current === 1) { document = migrateV1toV2(document); current = 2; }
|
||||
const nodes = document.nodes.map((rawNode) => {
|
||||
if (!isRecord(rawNode) || !isRecord(rawNode.style)) return rawNode;
|
||||
|
||||
const { width, ...styleRest } = rawNode.style;
|
||||
if (typeof width !== 'number' || !Number.isFinite(width)) return rawNode;
|
||||
|
||||
const existingLayout = isRecord(rawNode.layout) ? rawNode.layout : {};
|
||||
return {
|
||||
...rawNode,
|
||||
style: styleRest,
|
||||
layout: { widthMode: 'fixed', width, ...existingLayout },
|
||||
};
|
||||
});
|
||||
|
||||
return { ...document, nodes, version: 2 };
|
||||
}
|
||||
|
||||
function migrate(raw: Record<string, unknown>, version: number): Record<string, unknown> {
|
||||
let document = raw;
|
||||
let current = version;
|
||||
|
||||
if (current === 1) {
|
||||
document = migrateV1ToV2(document);
|
||||
current = 2;
|
||||
}
|
||||
|
||||
if (current !== MAP_FORMAT_VERSION) {
|
||||
// Version intermediaire inconnue : lecture au mieux, sans perdre de donnee.
|
||||
return { ...document, version: MAP_FORMAT_VERSION };
|
||||
}
|
||||
return document;
|
||||
|
||||
Reference in New Issue
Block a user