Ajoute les blocs de logique : organise le graphe de flow en sous-graphes nommés
Le graphe de logique d'une scène s'affichait jusqu'ici sur un seul canevas plat (toutes les scènes accumulant leurs nœuds sur la même grille), ce qui ne tient pas à l'échelle dès qu'une scène évolue au fil de l'avancée du joueur et accumule des centaines/milliers de nœuds. Ajoute les "Blocs de logique" : un bloc regroupe un sous-ensemble de nœuds/arêtes d'un écran sous un nom et une description (comme une fonction). L'onglet "Logique de la scène" devient une liste de blocs (nom, description tronquée à 3 phrases, éléments concernés, nombre de nœuds, bouton "Ouvrir"). Ouvrir un bloc affiche SON graphe dans une modale plein écran, redimensionnable et déplaçable (patron déjà mûr dans game_dashboard.html, porté tel quel : makeFloatPanelDraggable/ Resizable/Fullscreenable). Décision d'architecture : un bloc est un automate FERMÉ — impossible de relier un nœud d'un bloc à un nœud d'un autre bloc (rejeté côté serveur dans flow_edge_add.py). Toute communication entre deux blocs passe par le système d'événements personnalisés déjà en place (declencher_evenement / trigger_event="evenement"). Détails techniques : - Nouvelle colonne _flow_nodes.block_id (nullable, sans FK — même rationale que trigger_element_id/target_element_id, voir screens/elements/delete_element.py) et nouvelle table _flow_blocks (screens/flow/ensure_flow_schema.py, screens/flow/blocks/ensure_flow_blocks_schema.py). - Migration douce et automatique : les nœuds posés avant l'existence des blocs (block_id NULL) sont rattachés, à la première ouverture de l'onglet, à un "Bloc principal" auto-créé (screens/flow/blocks/ list_flow_blocks.py) — aucun script de migration séparé, aucune donnée perdue. - Suppression d'un bloc = cascade complète (bloc + tous ses nœuds/ arêtes), patron identique à screens/custom_events/delete_custom_event.py mais scopé à un seul bloc plutôt que game-wide. - Routes CRUD sous routes/flow_blocks/, montées comme routes/custom_events/. - templates/screen_edit.html : FLOW (global unique) renommé en ALL_FLOW (toutes les données de l'écran) ; un seul bloc ouvert à la fois (modale unique, à la Unity) — currentBlockNodes()/currentBlockEdges() filtrent ALL_FLOW par CURRENT_BLOCK_ID à chaque rendu, sans tenir de seconde copie à synchroniser manuellement. Vérifié : 215 tests passent (7 nouveaux dans tests/test_flow_blocks.py, dont un qui verrouille l'ordre d'appel list_flow_blocks()/ list_flow_nodes() dans screen_edit.py — la migration douce doit tourner AVANT le chargement des nœuds, sinon le compte de nœuds affiché juste après une migration est périmé), syntaxe JS validée (script de screen_edit.html rendu via le client de test puis node --check). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a7a315cce7
commit
1b7706b357
+212
-21
@@ -170,7 +170,7 @@
|
||||
{% set current_aspect = screen.preview_aspect or '9/16' %}
|
||||
<div class="builderTabs" id="builderTabs">
|
||||
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Écran</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="flow" onclick="switchBuilderTab('flow')">🔀 Logique de la scène</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="blocks" onclick="switchBuilderTab('blocks')">🧩 Blocs de logique</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="anim" onclick="switchBuilderTab('anim')">🎬 Timeline d'animation</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="events" onclick="switchBuilderTab('events')">📣 Événements</button>
|
||||
</div>
|
||||
@@ -190,7 +190,70 @@
|
||||
<p class="hint" style="text-align:center;">Glisse pour déplacer, tire le coin pour redimensionner, clique pour sélectionner.</p>
|
||||
</div>
|
||||
|
||||
<div class="builderTabPanel" id="flowTabPanel" data-tab-panel="flow">
|
||||
<div class="builderTabPanel" id="blocksTabPanel" data-tab-panel="blocks">
|
||||
<div class="dashPanelHeader">
|
||||
<h2>Blocs de logique</h2>
|
||||
<button type="button" class="button is-small primary" onclick="toggleDashCreate('blocks')">+ Créer un bloc</button>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('flow_block_new', slug=game.slug, screen_id=screen.id) }}" class="dashCreateBar" id="dashCreate-blocks" style="display:none;">
|
||||
<input type="text" name="name" placeholder="Nom du bloc (ex. Ouvrir la porte)" required autocomplete="off">
|
||||
<input type="text" name="description" placeholder="Description (optionnel)">
|
||||
<button type="submit" class="primary">Créer</button>
|
||||
</form>
|
||||
|
||||
{% if blocks_view %}
|
||||
{% for b in blocks_view %}
|
||||
<form method="post" action="{{ url_for('flow_block_edit', slug=game.slug, screen_id=screen.id, block_id=b.id) }}" id="blockEditForm{{ b.id }}"></form>
|
||||
{% endfor %}
|
||||
<div class="fieldsTableWrap">
|
||||
<table class="table is-fullwidth is-hoverable fieldsTable">
|
||||
<thead><tr><th>Nom</th><th>Description</th><th>Éléments concernés</th><th>Nœuds</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in blocks_view %}
|
||||
<tr>
|
||||
<td><strong>{{ b.name }}</strong></td>
|
||||
<td>{{ b.description|truncatesentences(3) }}</td>
|
||||
<td>{% if b.element_labels %}{{ b.element_labels|join(', ') }}{% else %}<span class="hint">—</span>{% endif %}</td>
|
||||
<td>{{ b.node_count }}</td>
|
||||
<td class="fieldsTableActions">
|
||||
<button type="button" class="button is-small is-primary" onclick="openLogicBlockPanel({{ b.id }}, {{ b.name|tojson }})" title="Ouvrir le graphe de ce bloc">🧩 Ouvrir</button>
|
||||
<button type="button" class="button is-small" onclick="toggleBlockEditRow({{ b.id }})" title="Modifier le nom/la description">✏️</button>
|
||||
<form method="post" action="{{ url_for('flow_block_delete', slug=game.slug, screen_id=screen.id, block_id=b.id) }}"
|
||||
onsubmit="return confirm('Supprimer le bloc « {{ b.name }} » ? Cela supprime aussi ses {{ b.node_count }} nœud(s) et leurs connexions.');" style="display:inline;">
|
||||
<button type="submit" class="button is-small is-danger" title="Supprimer">🗑️</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hidden" id="blockEditRow{{ b.id }}">
|
||||
<td colspan="5">
|
||||
<div class="dashCreateBar" style="display:flex;">
|
||||
<input type="text" form="blockEditForm{{ b.id }}" name="name" value="{{ b.name }}" required>
|
||||
<input type="text" form="blockEditForm{{ b.id }}" name="description" value="{{ b.description or '' }}" placeholder="Description">
|
||||
<button type="submit" form="blockEditForm{{ b.id }}" class="primary">Enregistrer</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="hint">Aucun bloc pour l'instant — crée le premier ci-dessus, puis ouvre-le pour poser des déclencheurs/conditions/actions.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Modale plein écran/redimensionnable/déplaçable d'un bloc de logique
|
||||
(voir openLogicBlockPanel()) — patron makeFloatPanelDraggable/
|
||||
Resizable/Fullscreenable, identique à game_dashboard.html. Un seul
|
||||
bloc ouvert à la fois : ce même #nodeForm/#flowCanvas est réutilisé
|
||||
pour n'importe quel bloc (voir CURRENT_BLOCK_ID). -->
|
||||
<div class="floatPanel floatPanel--center hidden" id="logicBlockPanel">
|
||||
<div class="floatPanelHeader" id="logicBlockPanelHeader">
|
||||
<span class="floatPanelTitle">⋮⋮ <span id="logicBlockPanelTitle">Bloc</span></span>
|
||||
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
|
||||
<button type="button" class="button is-small" onclick="closeLogicBlockPanel()">✕</button>
|
||||
</div>
|
||||
<div class="floatPanelBody logicBlockPanelBody">
|
||||
<div class="flowToolbar">
|
||||
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('trigger')">➕ Déclencheur</button>
|
||||
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('condition')">➕ Condition</button>
|
||||
@@ -408,6 +471,8 @@
|
||||
<svg id="flowEdges" class="flowEdgesSvg"></svg>
|
||||
<div id="flowCanvas" class="flowCanvas"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floatPanelResizeHandle" id="logicBlockPanelResizeHandle"></div>
|
||||
</div>
|
||||
|
||||
<div class="builderTabPanel" id="animTabPanel" data-tab-panel="anim">
|
||||
@@ -2161,10 +2226,26 @@ document.querySelectorAll('.uploadInput').forEach(function(input){
|
||||
// ---------- Logique de la scène : éditeur de flow à nœuds ----------
|
||||
|
||||
var FLOW_SCREEN_ID = {{ screen.id }};
|
||||
var FLOW = {
|
||||
// Blocs de logique (voir screens/flow/blocks/) : ALL_FLOW porte TOUS les
|
||||
// nœuds/arêtes de cet écran, tous blocs confondus (chargement inchangé) ;
|
||||
// un seul bloc est ouvert à la fois (modale unique, à la Unity) — pas
|
||||
// besoin d'une seconde copie filtrée à tenir synchronisée, currentBlockNodes()/
|
||||
// currentBlockEdges() recalculent la vue du bloc ouvert à chaque rendu.
|
||||
var ALL_FLOW = {
|
||||
nodes: {{ flow_nodes_json|safe }},
|
||||
edges: {{ flow_edges_json|safe }},
|
||||
};
|
||||
var FLOW_BLOCKS = {{ flow_blocks_json|safe }};
|
||||
var CURRENT_BLOCK_ID = null;
|
||||
|
||||
function currentBlockNodes() {
|
||||
return ALL_FLOW.nodes.filter(function(n) { return n.block_id === CURRENT_BLOCK_ID; });
|
||||
}
|
||||
function currentBlockEdges() {
|
||||
var ids = {};
|
||||
currentBlockNodes().forEach(function(n) { ids[n.id] = true; });
|
||||
return ALL_FLOW.edges.filter(function(e) { return ids[e.from_node_id] && ids[e.to_node_id]; });
|
||||
}
|
||||
var ELEMENTS_LABELS = {{ elements_labels_json|safe }};
|
||||
var SCREENS_MAP = {{ screens_map_json|safe }};
|
||||
var ACTION_TYPE_LABELS_MAP = {{ action_type_labels_json|safe }};
|
||||
@@ -2256,7 +2337,7 @@ function renderFlow() {
|
||||
canvas.innerHTML = '';
|
||||
svg.innerHTML = '';
|
||||
|
||||
FLOW.nodes.forEach(function(node) {
|
||||
currentBlockNodes().forEach(function(node) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'flowNode flowNode-' + node.node_type;
|
||||
div.style.left = node.x + 'px';
|
||||
@@ -2322,7 +2403,7 @@ function portPosition(nodeId, side, sub) {
|
||||
function drawFlowEdges() {
|
||||
var svg = document.getElementById('flowEdges');
|
||||
svg.innerHTML = '';
|
||||
FLOW.edges.forEach(function(edge) {
|
||||
currentBlockEdges().forEach(function(edge) {
|
||||
var from = portPosition(edge.from_node_id, 'out', edge.from_port);
|
||||
var to = portPosition(edge.to_node_id, 'in', null);
|
||||
if (!from || !to) return;
|
||||
@@ -2348,7 +2429,7 @@ function onPortClick(nodeId, side, port) {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId })
|
||||
}).then(function(r){ return r.json(); }).then(function(edge){
|
||||
FLOW.edges.push({ id: edge.id, screen_id: FLOW_SCREEN_ID, from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId });
|
||||
ALL_FLOW.edges.push({ id: edge.id, screen_id: FLOW_SCREEN_ID, from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId });
|
||||
flowPendingPort = null;
|
||||
drawFlowEdges();
|
||||
});
|
||||
@@ -2356,15 +2437,15 @@ function onPortClick(nodeId, side, port) {
|
||||
|
||||
function deleteFlowEdge(edgeId) {
|
||||
fetch(flowUrl('/flow/edges/' + edgeId + '/delete'), { method: 'POST' }).then(function(){
|
||||
FLOW.edges = FLOW.edges.filter(function(e){ return e.id !== edgeId; });
|
||||
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.id !== edgeId; });
|
||||
drawFlowEdges();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFlowNode(nodeId) {
|
||||
fetch(flowUrl('/flow/nodes/' + nodeId + '/delete'), { method: 'POST' }).then(function(){
|
||||
FLOW.nodes = FLOW.nodes.filter(function(n){ return n.id !== nodeId; });
|
||||
FLOW.edges = FLOW.edges.filter(function(e){ return e.from_node_id !== nodeId && e.to_node_id !== nodeId; });
|
||||
ALL_FLOW.nodes = ALL_FLOW.nodes.filter(function(n){ return n.id !== nodeId; });
|
||||
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.from_node_id !== nodeId && e.to_node_id !== nodeId; });
|
||||
renderFlow();
|
||||
});
|
||||
}
|
||||
@@ -2372,7 +2453,7 @@ function deleteFlowNode(nodeId) {
|
||||
function onFlowNodeMouseDown(e, nodeId) {
|
||||
if (e.target.classList.contains('flowPort') || e.target.classList.contains('flowNodeDelete')) return;
|
||||
e.preventDefault();
|
||||
var node = FLOW.nodes.find(function(n){ return n.id === nodeId; });
|
||||
var node = ALL_FLOW.nodes.find(function(n){ return n.id === nodeId; });
|
||||
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
|
||||
var startX = e.clientX, startY = e.clientY;
|
||||
var startLeft = node.x, startTop = node.y;
|
||||
@@ -2659,7 +2740,7 @@ function toggleFlowElementToggleMode() {
|
||||
}
|
||||
|
||||
function submitNodeForm() {
|
||||
var payload = { node_type: currentNodeFormType, x: 20, y: 20 + FLOW.nodes.length * 10 };
|
||||
var payload = { node_type: currentNodeFormType, x: 20, y: 20 + currentBlockNodes().length * 10, block_id: CURRENT_BLOCK_ID };
|
||||
|
||||
if (currentNodeFormType === 'trigger') {
|
||||
payload.trigger_event = document.getElementById('nf-trigger-event').value;
|
||||
@@ -2759,17 +2840,12 @@ function submitNodeForm() {
|
||||
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/nodes/add'), {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
|
||||
}).then(function(r){ return r.json(); }).then(function(node){
|
||||
FLOW.nodes.push(node);
|
||||
ALL_FLOW.nodes.push(node);
|
||||
closeNodeForm();
|
||||
renderFlow();
|
||||
});
|
||||
}
|
||||
|
||||
// Panneau de logique de scène absent sur un écran-modèle (élément de jeu) —
|
||||
// voir le commentaire Jinja au-dessus de .builderTabs.
|
||||
var HAS_FLOW_PANEL = !!document.getElementById('flowTabPanel');
|
||||
if (HAS_FLOW_PANEL) { renderFlow(); }
|
||||
|
||||
// ---------- Onglets du centre : Écran / Logique de la scène / Timeline
|
||||
// d'animation — un seul visible à la fois, chacun occupant tout l'espace
|
||||
// disponible (remplace l'ancien panneau du bas rétractable/redimensionnable
|
||||
@@ -2782,7 +2858,6 @@ function switchBuilderTab(tab) {
|
||||
document.querySelectorAll('.builderTabPanel').forEach(function (panel) {
|
||||
panel.classList.toggle('active', panel.dataset.tabPanel === tab);
|
||||
});
|
||||
if (tab === 'flow') requestAnimationFrame(drawFlowEdges);
|
||||
}
|
||||
|
||||
function toggleDashCreate(tab) {
|
||||
@@ -2794,13 +2869,129 @@ function toggleDashCreate(tab) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBlockEditRow(blockId) {
|
||||
document.getElementById('blockEditRow' + blockId).classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// ---------- Panneaux flottants : glisser-déposer + redimensionnement +
|
||||
// plein écran ---------- Générique, porté tel quel depuis game_dashboard.html
|
||||
// (mêmes gestes partout dans l'outil plutôt qu'une variante par panneau).
|
||||
function makeFloatPanelDraggable(panel, header) {
|
||||
var dragging = false, offsetX = 0, offsetY = 0;
|
||||
header.addEventListener('mousedown', function (e) {
|
||||
if (e.target.closest('button')) return;
|
||||
dragging = true;
|
||||
if (panel.classList.contains('floatPanel--fullscreen')) panel._fsToggle.exit();
|
||||
var rect = panel.getBoundingClientRect();
|
||||
panel.style.left = rect.left + 'px';
|
||||
panel.style.top = rect.top + 'px';
|
||||
panel.style.right = 'auto';
|
||||
panel.style.bottom = 'auto';
|
||||
panel.style.transform = 'none';
|
||||
offsetX = e.clientX - rect.left;
|
||||
offsetY = e.clientY - rect.top;
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!dragging) return;
|
||||
panel.style.left = (e.clientX - offsetX) + 'px';
|
||||
panel.style.top = (e.clientY - offsetY) + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', function () {
|
||||
dragging = false;
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
}
|
||||
function makeFloatPanelResizable(panel, handle) {
|
||||
var resizing = false, startX = 0, startY = 0, startW = 0, startH = 0;
|
||||
handle.addEventListener('mousedown', function (e) {
|
||||
resizing = true;
|
||||
if (panel.classList.contains('floatPanel--fullscreen')) panel._fsToggle.exit();
|
||||
var rect = panel.getBoundingClientRect();
|
||||
startX = e.clientX; startY = e.clientY;
|
||||
startW = rect.width; startH = rect.height;
|
||||
panel.style.width = startW + 'px';
|
||||
panel.style.maxWidth = 'none';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!resizing) return;
|
||||
panel.style.width = Math.max(320, startW + (e.clientX - startX)) + 'px';
|
||||
panel.style.height = Math.max(200, startH + (e.clientY - startY)) + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', function () {
|
||||
resizing = false;
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
}
|
||||
function makeFloatPanelFullscreenable(panel, toggleBtn) {
|
||||
var saved = null;
|
||||
function enter() {
|
||||
saved = {
|
||||
left: panel.style.left, top: panel.style.top, right: panel.style.right,
|
||||
width: panel.style.width, height: panel.style.height,
|
||||
maxWidth: panel.style.maxWidth, transform: panel.style.transform,
|
||||
};
|
||||
panel.classList.add('floatPanel--fullscreen');
|
||||
panel.style.left = '12px'; panel.style.top = '12px'; panel.style.right = '12px'; panel.style.bottom = '12px';
|
||||
panel.style.width = 'auto'; panel.style.height = 'auto'; panel.style.maxWidth = 'none'; panel.style.transform = 'none';
|
||||
toggleBtn.title = 'Quitter le plein écran';
|
||||
}
|
||||
function exit() {
|
||||
panel.classList.remove('floatPanel--fullscreen');
|
||||
panel.style.bottom = '';
|
||||
if (saved) {
|
||||
panel.style.left = saved.left; panel.style.top = saved.top; panel.style.right = saved.right;
|
||||
panel.style.width = saved.width; panel.style.height = saved.height;
|
||||
panel.style.maxWidth = saved.maxWidth; panel.style.transform = saved.transform;
|
||||
}
|
||||
toggleBtn.title = 'Plein écran';
|
||||
}
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
if (panel.classList.contains('floatPanel--fullscreen')) exit(); else enter();
|
||||
});
|
||||
panel._fsToggle = { enter: enter, exit: exit };
|
||||
return panel._fsToggle;
|
||||
}
|
||||
|
||||
// ---------- Modale d'un bloc de logique (voir screens/flow/blocks/) : un
|
||||
// seul bloc ouvert à la fois, réutilise le même #nodeForm/#flowCanvas pour
|
||||
// n'importe quel bloc — voir currentBlockNodes()/currentBlockEdges(). ----------
|
||||
(function () {
|
||||
var panel = document.getElementById('logicBlockPanel');
|
||||
makeFloatPanelDraggable(panel, document.getElementById('logicBlockPanelHeader'));
|
||||
makeFloatPanelResizable(panel, document.getElementById('logicBlockPanelResizeHandle'));
|
||||
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
|
||||
window.openLogicBlockPanel = function (blockId, blockName) {
|
||||
CURRENT_BLOCK_ID = blockId;
|
||||
closeNodeForm();
|
||||
document.getElementById('logicBlockPanelTitle').textContent = blockName || '?';
|
||||
panel.classList.remove('hidden');
|
||||
panel._fsToggle.enter();
|
||||
renderFlow();
|
||||
requestAnimationFrame(drawFlowEdges);
|
||||
};
|
||||
window.closeLogicBlockPanel = function () {
|
||||
panel.classList.add('hidden');
|
||||
CURRENT_BLOCK_ID = null;
|
||||
};
|
||||
})();
|
||||
|
||||
// Onglet initial : ?tab=... (utilisé par les routes de CRUD des
|
||||
// événements, routes/custom_events/, pour revenir directement sur
|
||||
// l'onglet "Événements" après création/modification/suppression plutôt
|
||||
// que de laisser deviner où cliquer — même patron que game_dashboard.html).
|
||||
// événements/blocs, pour revenir directement sur le bon onglet après
|
||||
// création/modification/suppression plutôt que de laisser deviner où
|
||||
// cliquer — même patron que game_dashboard.html). ?block=<id> ouvre en
|
||||
// plus directement le bloc concerné (même idiome que ?edit=&subtab= dans
|
||||
// game_dashboard.html).
|
||||
(function () {
|
||||
var requestedTab = {{ (request.args.get('tab') or '')|tojson }};
|
||||
if (requestedTab) switchBuilderTab(requestedTab);
|
||||
var requestedBlock = {{ request.args.get('block', type=int)|tojson }};
|
||||
if (requestedBlock) {
|
||||
var block = FLOW_BLOCKS.find(function (b) { return b.id === requestedBlock; });
|
||||
if (block) openLogicBlockPanel(block.id, block.name);
|
||||
}
|
||||
})();
|
||||
|
||||
// ---------- Timeline d'animation : piste par élément, clips Animate.css ou
|
||||
|
||||
Reference in New Issue
Block a user