From 41dd1192004ced8b66bce0c1bbb74afc7d2919ec Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Mon, 17 Oct 2022 15:42:53 +0200 Subject: [PATCH] Added Application.md + add more TODOs --- docs/#Project/Pages/Application.md | 179 +++++++++++++++++++++++++++++ src/Interfaces/IContainerModel.ts | 4 + src/utils/saveload.ts | 2 + 3 files changed, 185 insertions(+) create mode 100644 docs/#Project/Pages/Application.md diff --git a/docs/#Project/Pages/Application.md b/docs/#Project/Pages/Application.md new file mode 100644 index 0000000..cb731f3 --- /dev/null +++ b/docs/#Project/Pages/Application.md @@ -0,0 +1,179 @@ +# Cycle de vie de l'application + +# Menu principal + +Lorsque l'on lance l'application pour la première fois, +le premier composant qui s'affiche est `App.tsx`. + +En fonction de la valeur de `FAST_BOOT`, +il peut soit afficher un menu principal (composant `MainMenu`) si `FAST_BOOT=false` +ou soit afficher l'editeur directement (composant `Editor`). + +Lorsque le menu principal est affiché il y a 3 états : *Main*, *Load* ou *Loading*. + +Lorsqu'on est dans Main, nous avons les deux boutons principaux affiché : *Start from scratch* ou *Load* + +## Start from scratch + +Lorsque l'on clique sur *Start from scratch*, +l'action qui se fait est de charger une configuration depuis l'API +(ou d'utiliser une configuration préchargée via un event custom). +On passe à l'état *Loading*. + +Après chargement, `isLoaded` est `true` et le composant `Editor` est affiché. + +## Load + +Quand on veut charger un json, on clique sur *Load* et l'état passe à *Load*. + +Ici, le composant `MainMenu` affichera un bouton pour charger un fichier. +Charger le fichier change l'état de la configuration dans `App` +et active `isLoaded` qui enfin affiche `Editor`. + + +# Editor + +Lorsque `Editor` est affiché, +il reçoit en props la configuration que l'on a chargé au menu principal. + +Il reçoit également un historique par défaut pour avoir au moins quelque chose d'affiché. + +Il reçoit `root` un element HTML où l'on insert SVGLayoutDesigner par `main.tsx`. +`root` est utilisé pour lui donner des events qui seront utilisé pour communiquer avec le SmartComponent. + +Plusieurs sous-composant sont affichés ensuite. +Vous pouvez en savoir plus sur [la structure de composants](./ComponentStructure.drawio) +avec dragrams.net. + + +# Save and load + +Pour enregistrer le travail fait, il existe plusieurs méthode de le faire. + +## SmartComponent + +La première, en passant par le SmartComponent `svg-layout-designer.ts`, est de stringifier l'état de l'éditeur +et de le sauvegarder quelque part. + +On utilise `GetEditorAsString` pour obtenir une version stringifié du projet +que l'on peut ensuite sauvegarder dans un fichier, bdd ou autre. +On peut également utiliser GetEditorState et le sauvegarder tel quel dans le JS +mais on ne peut pas le stringifier à un object json ne peut pas avoir deux références +(sauf si on a le code source de saveload.ts, +utilisant un *replacer* éliminant les références circulaires, +voir [JSON par interaction](#json-par-interaction). + +Pour charger l'état de l'éditeur, il faut en premier le parser avec `JSON.parse` (pas de soucis à ce niveau là); +Enfin, on peut ensuite utiliser `LoadEditor` du SmartComponent pour charger l'état. + +`LoadEditor` est une macro de trois autres appels : +- `ReviveEditorState` qui fait revivre tous les doublons de références +- `SetEditor` qui charge la configuration de l'application +- `SetHistory` qui charge l'historique de l'editeur + +La raison que l'on utilise `SetHistory` en plus de `SetEditor` est parce que, +`SetEditor` ne fait que charger une configuration par défaut de `App.tsx` +(exemple: lorsque l'on crée un conteneur, l'éditeur va lire cette configuration). +Si l'application est déjà chargée, c'est-à-dire que *isLoaded* de `App` est `true`, +alors l'application ne va pas relire `history` et `historyCurrentStep` et l'application n'aura pas *chargé*. +C'est pourquoi on a besoin de `SetHistory` pour charger l'état courant dans `Editor.tsx` (à l'opposé de `App.tsx`). + +Note: Pour rappel, *App* n'est qu'un menu principal. +Comme dans un menu principal de jeu vidéo, +écraser une sauvegarde ne fait pas charger la sauvegarde. +C'est en chargeant la sauvegarde en cours de jeu ou dans le menu principal, +que la partie change. + + +## JSON par interaction + +On peut charger un fichier JSON manuellement. + +Pour cela il faut que `FAST_BOOT` de `default.ts` soit désactivé. + +Dans l'éditeur, on peut exporter une configuration par fichier JSON grâce au composant `Settings.tsx`. +Cette sauvegarde, comme pour sauvegarder avec le SmartComponent, utilise `JSON.stringify`. +Et donc utilise un *replacer* pour supprimer les dépendances circulaires. + +Ce *replacer* se trouve dans `worker.js` mais +un fallback est également donné dans `saveload.ts` +dans le cas où la fonctionnalité de Web Worker n'est pas supportée par le navigateur web. +Pour corriger des bugs sur la sauvegarde, il faudra donc modifier ces deux fichiers. + +Décrivons rapidement ce qu'elle fait : + +```typescript +export function GetCircularReplacer(): (key: any, value: object | Map | null) => object | null | undefined { + return (key: any, value: object | null) => { + if (key === 'parent') { + return; + } + + if (key === 'containers') { + return Array.from((value as Map).entries()); + } + + if (key === 'symbols') { + return Array.from((value as Map).entries()); + } + + if (key === 'linkedContainers') { + return Array.from(value as Set); + } + + return value; + }; +} +``` + +Nous faisons les actions suivantes pour supprimer les types non supportés par le stringify : +- nous transformons les *Map* `containers` et `symbols` en tableau de vecteur clés-valeurs +- nous transformons les *Set* `linkedContainers` en tableau de clés. + +Nous supprimons toutes les références de `parent` qui sont déjà référencés dans `containers`. + +Enfin, nous retournons `value` si tout est bon. + +Normalement, le JSON retourné ressemble à l'objet qu'était EditorState +mais sans les références de parent et avec des tableaux partout. + + +Pour le charger, il faut revenir au menu principal et +cliquer sur Load pour charger avec l'input. + +Charger le fichier JSON, utilisera ce qu'on appelle un *reviver* +qui s'occupe de recréer les types non supportés par JSON et de remettre les références dupliquées. + +Ce reviver se trouve dans `saveload.ts` avec la fonction `Revive()`. + +`Revive` change la valeur de `historyCurrentStep` avec de faire revivre l'historique + +`ReviveHistory` itère sur chaque état de `history` pour les revivre. + +`ReviveState` fait revivre en premier les *Map* et les *Set*. +Ensuite, il va itérer sur tous les conteneurs existants, +pour remettre leur parent. + +Comme vous pouvez vous en douter cette algorithme coûte O(n) juste pour revivre les parents. +avec *n* le nombre total de conteneurs sur toute la durée de vie de l'application. +Surtout qu'il n'est plus très utile depuis que tous les conteneurs sont dans une hash *Map* +(et que donc parent est accessible avec un coût O(1)). + +On pourrait juste utiliser `GetContainerById` au lieu de la référence. + + +## JSON par requête GET HTTP + +Pour charger un JSON via HTTP GET, +il faut que le serveur web distant autorise notre domaine +dans le méchanisme *cross-origin resource sharing (CORS)* +et qu'il nous autorise à faire des requêtes *GET*. + +Après cela pour charger le JSON, +il faut que l'url de SVGLayoutDesigner soit paramétrée avec l'url de la resource : + +``` +http://localhost:5173/?state=http://other-server.com/state.json +``` + +Après cela, l'éditeur chargera directement le fichier. diff --git a/src/Interfaces/IContainerModel.ts b/src/Interfaces/IContainerModel.ts index ff24d67..8e38efa 100644 --- a/src/Interfaces/IContainerModel.ts +++ b/src/Interfaces/IContainerModel.ts @@ -2,6 +2,8 @@ import { IContainerProperties } from './IContainerProperties'; export interface IContainerModel { children: string[] + // TODO: Remove parent now that accessing the parent by id is faster. + // TODO: Use GetContainerById(container.properties.parentId) as the better alternative. parent: IContainerModel | null properties: IContainerProperties userData: Record @@ -13,6 +15,8 @@ export interface IContainerModel { */ export class ContainerModel implements IContainerModel { public children: string[]; + // TODO: Remove parent now that accessing the parent by id is faster. + // TODO: Use GetContainerById(container.properties.parentId) as the better alternative. public parent: IContainerModel | null; public properties: IContainerProperties; public userData: Record; diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index 7250500..ed1f57c 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -40,6 +40,8 @@ export function ReviveState(state: IHistoryState): void { return; } + // TODO: remove parent and remove this bloc of code + // TODO: See IContainerModel.ts for more detail const it = MakeDFSIterator(root, state.containers); for (const container of it) { const parentId = container.properties.parentId;