Refactor goal tree editor form
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
a796edcb02
commit
d9bbf40fad
3 changed files with 255 additions and 169 deletions
227
website/src/features/toolboxes/modules/GoalTreeEditor.jsx
Normal file
227
website/src/features/toolboxes/modules/GoalTreeEditor.jsx
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// Rôle : rend le formulaire d'édition des noeuds de l'arbre d'objectifs.
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { Tabs } from "../../../components/Tabs.jsx";
|
||||
import {
|
||||
GoalTreeContentEditor,
|
||||
GoalTreeFormulaInput,
|
||||
GoalTreeIconPicker,
|
||||
GoalTreePrerequisitePicker,
|
||||
GoalTreeTitleInput
|
||||
} from "./GoalTreeParts.jsx";
|
||||
import {
|
||||
KIND_LABELS,
|
||||
RESET_LABELS
|
||||
} from "./goalTreeDisplay.js";
|
||||
import {
|
||||
GOAL_TREE_COLORS,
|
||||
GOAL_TREE_COLOR_VALUES,
|
||||
GOAL_TREE_KINDS,
|
||||
GOAL_TREE_RESET_TYPES,
|
||||
GOAL_TREE_WEEK_DAYS
|
||||
} from "./goalTreeUtils.js";
|
||||
|
||||
export function GoalTreeEditor({
|
||||
selectedNode,
|
||||
selectedNodeIsStep,
|
||||
selectedProgressMode,
|
||||
formTabs,
|
||||
activeFormTab,
|
||||
evaluatedNodes,
|
||||
textContent,
|
||||
onTabChange,
|
||||
onClose,
|
||||
onDeleteNode,
|
||||
onSetKind,
|
||||
onUpdateNode,
|
||||
onSetStepTarget,
|
||||
onAddNodeItem,
|
||||
onUpdateNodeItem,
|
||||
onDeleteNodeItem,
|
||||
onToggleLinkedParent
|
||||
}) {
|
||||
if (!selectedNode) {
|
||||
return (
|
||||
<section className="goal-tree-editor calculator-card" aria-label={textContent.editorTitle || "Édition de l'arbre d'objectifs"}>
|
||||
<p className="goal-tree-empty">{textContent.emptySelection || "Sélectionnez ou créez un objectif."}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="goal-tree-editor calculator-card" aria-label={textContent.editorTitle || "Édition de l'arbre d'objectifs"}>
|
||||
<div className="goal-tree-selected-form">
|
||||
<Tabs tabs={formTabs} activeTab={activeFormTab} onChange={onTabChange} className="goal-tree-form-tabs" />
|
||||
|
||||
{activeFormTab === "base" && (
|
||||
<GoalTreeBasePanel
|
||||
selectedNode={selectedNode}
|
||||
selectedNodeIsStep={selectedNodeIsStep}
|
||||
textContent={textContent}
|
||||
onSetKind={onSetKind}
|
||||
onUpdateNode={onUpdateNode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeFormTab === "content" && !selectedNodeIsStep && (
|
||||
<div className="goal-tree-form-panel">
|
||||
<GoalTreeContentEditor
|
||||
node={selectedNode}
|
||||
textContent={textContent}
|
||||
onAdd={() => onAddNodeItem(selectedNode.id)}
|
||||
onUpdate={(itemId, updater) => onUpdateNodeItem(selectedNode.id, itemId, updater)}
|
||||
onDelete={(itemId) => onDeleteNodeItem(selectedNode.id, itemId)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFormTab === "content" && selectedNodeIsStep && (
|
||||
<div className="goal-tree-form-panel">
|
||||
<p className="goal-tree-content-hint">{textContent.stepContentUnavailable || "Les étapes n'ont pas de contenu interne."}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFormTab === "advanced" && (
|
||||
<GoalTreeAdvancedPanel
|
||||
selectedNode={selectedNode}
|
||||
selectedNodeIsStep={selectedNodeIsStep}
|
||||
selectedProgressMode={selectedProgressMode}
|
||||
evaluatedNodes={evaluatedNodes}
|
||||
textContent={textContent}
|
||||
onUpdateNode={onUpdateNode}
|
||||
onSetStepTarget={onSetStepTarget}
|
||||
onToggleLinkedParent={onToggleLinkedParent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="goal-tree-form-actions">
|
||||
<button type="button" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
<span>{textContent.closeButton || "Fermer"}</span>
|
||||
</button>
|
||||
<button className="danger" type="button" onClick={() => onDeleteNode(selectedNode.id)}>
|
||||
<Icon name="trash" />
|
||||
<span>{textContent.deleteButton || "Supprimer la branche"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function GoalTreeBasePanel({ selectedNode, selectedNodeIsStep, textContent, onSetKind, onUpdateNode }) {
|
||||
return (
|
||||
<div className="goal-tree-form-panel">
|
||||
<label>
|
||||
<span>{textContent.kindLabel || "Type de l'objectif"}</span>
|
||||
<select value={selectedNode.kind} onChange={(event) => onSetKind(selectedNode.id, event.target.value)}>
|
||||
{GOAL_TREE_KINDS.map((kind) => <option key={kind} value={kind}>{textContent[`${kind}Kind`] || KIND_LABELS[kind]}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{selectedNodeIsStep ? textContent.titleLabel || "Titre de l'objectif" : textContent.listTitleLabel || "Titre de la liste"}</span>
|
||||
<GoalTreeTitleInput
|
||||
key={selectedNode.id}
|
||||
value={selectedNode.title}
|
||||
onChange={(title) => onUpdateNode(selectedNode.id, (node) => ({ ...node, title }))}
|
||||
/>
|
||||
</label>
|
||||
{selectedNodeIsStep && (
|
||||
<>
|
||||
<GoalTreeIconPicker
|
||||
label={textContent.iconLabel || "Icône"}
|
||||
value={selectedNode.icon || "cube"}
|
||||
textContent={textContent}
|
||||
onChange={(icon) => onUpdateNode(selectedNode.id, (node) => ({ ...node, icon }))}
|
||||
/>
|
||||
<div className="goal-tree-color-field">
|
||||
<span>{textContent.iconColorLabel || "Couleur de l'icône"}</span>
|
||||
<div className="goal-tree-color-swatches" role="radiogroup" aria-label={textContent.iconColorLabel || "Couleur de l'icône"}>
|
||||
{GOAL_TREE_COLORS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className={`goal-tree-color-swatch ${selectedNode.color === color ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, color }))}
|
||||
aria-pressed={selectedNode.color === color}
|
||||
aria-label={textContent[`color${color}`] || color}
|
||||
title={textContent[`color${color}`] || color}
|
||||
style={{ "--goal-tree-swatch-color": GOAL_TREE_COLOR_VALUES[color] }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!selectedNodeIsStep && (
|
||||
<p className="goal-tree-content-hint">{textContent.contentTabHint || "Le contenu se modifie dans l'onglet Contenu."}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GoalTreeAdvancedPanel({ selectedNode, selectedNodeIsStep, selectedProgressMode, evaluatedNodes, textContent, onUpdateNode, onSetStepTarget, onToggleLinkedParent }) {
|
||||
return (
|
||||
<div className="goal-tree-form-panel">
|
||||
<label>
|
||||
<span>{textContent.resetTypeLabel || "Type de reset"}</span>
|
||||
<select value={selectedNode.resetType} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
|
||||
{GOAL_TREE_RESET_TYPES.map((resetType) => <option key={resetType} value={resetType}>{textContent[`${resetType}Reset`] || RESET_LABELS[resetType]}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
{selectedNode.resetType !== "none" && (
|
||||
<label>
|
||||
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
||||
<input type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
|
||||
</label>
|
||||
)}
|
||||
{selectedNode.resetType === "weekly" && (
|
||||
<label>
|
||||
<span>{textContent.weeklyResetDayLabel || "Jour de reset"}</span>
|
||||
<select value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
|
||||
{GOAL_TREE_WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{selectedNodeIsStep && (
|
||||
<>
|
||||
<div className="goal-tree-form-section-title">{textContent.quantitySectionTitle || "Quantité"}</div>
|
||||
<label>
|
||||
<span>{textContent.targetQuantityLabel || "Cible"}</span>
|
||||
<GoalTreeFormulaInput
|
||||
aria-label={textContent.targetQuantityLabel || "Cible"}
|
||||
hasParent={Boolean(selectedNode.parentId)}
|
||||
onChange={(value) => onSetStepTarget(selectedNode.id, value)}
|
||||
value={selectedNode.formula || selectedNode.qtyTarget}
|
||||
placeholder={textContent.formulaPlaceholder || "base * 3"}
|
||||
textContent={textContent}
|
||||
title={textContent.formulaHelp || "Utilisez base pour référencer la cible du parent."}
|
||||
/>
|
||||
</label>
|
||||
<div className="goal-tree-form-section-title">{textContent.progressSectionTitle || "Progression"}</div>
|
||||
<button
|
||||
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle ${selectedProgressMode === "auto" ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, progressMode: selectedProgressMode === "auto" ? "manual" : "auto" }))}
|
||||
aria-pressed={selectedProgressMode === "auto"}
|
||||
title={selectedProgressMode === "auto" ? textContent.autoProgressTitle || "Progression automatique" : textContent.manualProgressTitle || "Mise à jour manuelle"}
|
||||
>
|
||||
<Icon name={selectedProgressMode === "auto" ? "calculator" : "edit"} />
|
||||
<span>{selectedProgressMode === "auto" ? textContent.autoProgressLabel || "Automatique" : textContent.manualProgressLabel || "Manuelle"}</span>
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
<p className="goal-tree-progress-mode-hint">
|
||||
{selectedProgressMode === "auto"
|
||||
? textContent.autoProgressHint || "Calcule la progression depuis les enfants."
|
||||
: textContent.manualProgressHint || "Affiche une checkbox ou une quantité à mettre à jour directement."}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<GoalTreePrerequisitePicker
|
||||
nodes={evaluatedNodes}
|
||||
node={selectedNode}
|
||||
textContent={textContent}
|
||||
onToggle={(parentId) => onToggleLinkedParent(selectedNode.id, parentId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,22 +2,13 @@
|
|||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { DiagramConnectors } from "../../../components/DiagramConnectors.jsx";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { Tabs } from "../../../components/Tabs.jsx";
|
||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { GoalTreeEditor } from "./GoalTreeEditor.jsx";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
import {
|
||||
GoalTreeBranch,
|
||||
GoalTreeContentEditor,
|
||||
GoalTreeFormulaInput,
|
||||
GoalTreeIconPicker,
|
||||
GoalTreePrerequisitePicker,
|
||||
GoalTreeTitleInput
|
||||
} from "./GoalTreeParts.jsx";
|
||||
import { GoalTreeBranch } from "./GoalTreeParts.jsx";
|
||||
import {
|
||||
GOAL_TREE_VIEW_SCALE,
|
||||
KIND_ICONS,
|
||||
KIND_LABELS,
|
||||
RESET_LABELS,
|
||||
getDefaultGoalTreeItem,
|
||||
getEffectiveGoalTreeProgressMode,
|
||||
getGoalTreeConvergenceKeepOpenIds,
|
||||
|
|
@ -26,11 +17,6 @@ import {
|
|||
isSingleValueGoalTreeNode
|
||||
} from "./goalTreeDisplay.js";
|
||||
import {
|
||||
GOAL_TREE_COLORS,
|
||||
GOAL_TREE_COLOR_VALUES,
|
||||
GOAL_TREE_KINDS,
|
||||
GOAL_TREE_RESET_TYPES,
|
||||
GOAL_TREE_WEEK_DAYS,
|
||||
applyGoalTreeDueResets,
|
||||
evaluateGoalTreeNodes,
|
||||
getGoalTreeChildren,
|
||||
|
|
@ -329,158 +315,25 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
|
|||
return (
|
||||
<div className={`goal-tree-module ${editing ? "" : "is-view-only"}`}>
|
||||
{editing && (
|
||||
<section className="goal-tree-editor calculator-card" aria-label={textContent.editorTitle || "Édition de l'arbre d'objectifs"}>
|
||||
{selectedNode ? (
|
||||
<div className="goal-tree-selected-form">
|
||||
<Tabs tabs={formTabs} activeTab={activeFormTab} onChange={setActiveFormTab} className="goal-tree-form-tabs" />
|
||||
|
||||
{activeFormTab === "base" && (
|
||||
<div className="goal-tree-form-panel">
|
||||
<label>
|
||||
<span>{textContent.kindLabel || "Type de l'objectif"}</span>
|
||||
<select value={selectedNode.kind} onChange={(event) => setKind(selectedNode.id, event.target.value)}>
|
||||
{GOAL_TREE_KINDS.map((kind) => <option key={kind} value={kind}>{textContent[`${kind}Kind`] || KIND_LABELS[kind]}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{selectedNodeIsStep ? textContent.titleLabel || "Titre de l'objectif" : textContent.listTitleLabel || "Titre de la liste"}</span>
|
||||
<GoalTreeTitleInput
|
||||
key={selectedNode.id}
|
||||
value={selectedNode.title}
|
||||
onChange={(title) => updateNode(selectedNode.id, (node) => ({ ...node, title }))}
|
||||
/>
|
||||
</label>
|
||||
{selectedNodeIsStep && (
|
||||
<>
|
||||
<GoalTreeIconPicker
|
||||
label={textContent.iconLabel || "Icône"}
|
||||
value={selectedNode.icon || "cube"}
|
||||
textContent={textContent}
|
||||
onChange={(icon) => updateNode(selectedNode.id, (node) => ({ ...node, icon }))}
|
||||
/>
|
||||
<div className="goal-tree-color-field">
|
||||
<span>{textContent.iconColorLabel || "Couleur de l'icône"}</span>
|
||||
<div className="goal-tree-color-swatches" role="radiogroup" aria-label={textContent.iconColorLabel || "Couleur de l'icône"}>
|
||||
{GOAL_TREE_COLORS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className={`goal-tree-color-swatch ${selectedNode.color === color ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => updateNode(selectedNode.id, (node) => ({ ...node, color }))}
|
||||
aria-pressed={selectedNode.color === color}
|
||||
aria-label={textContent[`color${color}`] || color}
|
||||
title={textContent[`color${color}`] || color}
|
||||
style={{ "--goal-tree-swatch-color": GOAL_TREE_COLOR_VALUES[color] }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!selectedNodeIsStep && (
|
||||
<p className="goal-tree-content-hint">{textContent.contentTabHint || "Le contenu se modifie dans l'onglet Contenu."}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFormTab === "content" && !selectedNodeIsStep && (
|
||||
<div className="goal-tree-form-panel">
|
||||
<GoalTreeContentEditor
|
||||
node={selectedNode}
|
||||
textContent={textContent}
|
||||
onAdd={() => addNodeItem(selectedNode.id)}
|
||||
onUpdate={(itemId, updater) => updateNodeItem(selectedNode.id, itemId, updater)}
|
||||
onDelete={(itemId) => deleteNodeItem(selectedNode.id, itemId)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFormTab === "content" && selectedNodeIsStep && (
|
||||
<div className="goal-tree-form-panel">
|
||||
<p className="goal-tree-content-hint">{textContent.stepContentUnavailable || "Les étapes n'ont pas de contenu interne."}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFormTab === "advanced" && (
|
||||
<div className="goal-tree-form-panel">
|
||||
<label>
|
||||
<span>{textContent.resetTypeLabel || "Type de reset"}</span>
|
||||
<select value={selectedNode.resetType} onChange={(event) => updateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
|
||||
{GOAL_TREE_RESET_TYPES.map((resetType) => <option key={resetType} value={resetType}>{textContent[`${resetType}Reset`] || RESET_LABELS[resetType]}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
{selectedNode.resetType !== "none" && (
|
||||
<label>
|
||||
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
||||
<input type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => updateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
|
||||
</label>
|
||||
)}
|
||||
{selectedNode.resetType === "weekly" && (
|
||||
<label>
|
||||
<span>{textContent.weeklyResetDayLabel || "Jour de reset"}</span>
|
||||
<select value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => updateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
|
||||
{GOAL_TREE_WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{selectedNodeIsStep && (
|
||||
<>
|
||||
<div className="goal-tree-form-section-title">{textContent.quantitySectionTitle || "Quantité"}</div>
|
||||
<label>
|
||||
<span>{textContent.targetQuantityLabel || "Cible"}</span>
|
||||
<GoalTreeFormulaInput
|
||||
aria-label={textContent.targetQuantityLabel || "Cible"}
|
||||
hasParent={Boolean(selectedNode.parentId)}
|
||||
onChange={(value) => setStepTarget(selectedNode.id, value)}
|
||||
value={selectedNode.formula || selectedNode.qtyTarget}
|
||||
placeholder={textContent.formulaPlaceholder || "base * 3"}
|
||||
textContent={textContent}
|
||||
title={textContent.formulaHelp || "Utilisez base pour référencer la cible du parent."}
|
||||
/>
|
||||
</label>
|
||||
<div className="goal-tree-form-section-title">{textContent.progressSectionTitle || "Progression"}</div>
|
||||
<button
|
||||
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle ${selectedProgressMode === "auto" ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => updateNode(selectedNode.id, (node) => ({ ...node, progressMode: selectedProgressMode === "auto" ? "manual" : "auto" }))}
|
||||
aria-pressed={selectedProgressMode === "auto"}
|
||||
title={selectedProgressMode === "auto" ? textContent.autoProgressTitle || "Progression automatique" : textContent.manualProgressTitle || "Mise à jour manuelle"}
|
||||
>
|
||||
<Icon name={selectedProgressMode === "auto" ? "calculator" : "edit"} />
|
||||
<span>{selectedProgressMode === "auto" ? textContent.autoProgressLabel || "Automatique" : textContent.manualProgressLabel || "Manuelle"}</span>
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
<p className="goal-tree-progress-mode-hint">
|
||||
{selectedProgressMode === "auto"
|
||||
? textContent.autoProgressHint || "Calcule la progression depuis les enfants."
|
||||
: textContent.manualProgressHint || "Affiche une checkbox ou une quantité à mettre à jour directement."}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<GoalTreePrerequisitePicker
|
||||
nodes={evaluatedNodes}
|
||||
node={selectedNode}
|
||||
textContent={textContent}
|
||||
onToggle={(parentId) => toggleLinkedParent(selectedNode.id, parentId)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="goal-tree-form-actions">
|
||||
<button type="button" onClick={() => setEditing(false)}>
|
||||
<Icon name="close" />
|
||||
<span>{textContent.closeButton || "Fermer"}</span>
|
||||
</button>
|
||||
<button className="danger" type="button" onClick={() => deleteNode(selectedNode.id)}>
|
||||
<Icon name="trash" />
|
||||
<span>{textContent.deleteButton || "Supprimer la branche"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="goal-tree-empty">{textContent.emptySelection || "Sélectionnez ou créez un objectif."}</p>
|
||||
)}
|
||||
</section>
|
||||
<GoalTreeEditor
|
||||
selectedNode={selectedNode}
|
||||
selectedNodeIsStep={selectedNodeIsStep}
|
||||
selectedProgressMode={selectedProgressMode}
|
||||
formTabs={formTabs}
|
||||
activeFormTab={activeFormTab}
|
||||
evaluatedNodes={evaluatedNodes}
|
||||
textContent={textContent}
|
||||
onTabChange={setActiveFormTab}
|
||||
onClose={() => setEditing(false)}
|
||||
onDeleteNode={deleteNode}
|
||||
onSetKind={setKind}
|
||||
onUpdateNode={updateNode}
|
||||
onSetStepTarget={setStepTarget}
|
||||
onAddNodeItem={addNodeItem}
|
||||
onUpdateNodeItem={updateNodeItem}
|
||||
onDeleteNodeItem={deleteNodeItem}
|
||||
onToggleLinkedParent={toggleLinkedParent}
|
||||
/>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue