Split combos list rendering from module state
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
602486a1d7
commit
73e84ee703
3 changed files with 168 additions and 122 deletions
|
|
@ -141,6 +141,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
||||||
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
||||||
const combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8");
|
const combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8");
|
||||||
|
const combosList = await readFile("website/src/features/toolboxes/modules/CombosList.jsx", "utf8");
|
||||||
|
const combosBundle = `${combosModule}\n${combosParts}\n${combosList}`;
|
||||||
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
|
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
|
||||||
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||||
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
|
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
|
||||||
|
|
@ -249,8 +251,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(combosModule, /useGroupedReorder/);
|
assert.match(combosModule, /useGroupedReorder/);
|
||||||
assert.match(combosModule, /reorderFeatures/);
|
assert.match(combosModule, /reorderFeatures/);
|
||||||
assert.match(combosModule, /namespace: `combos-\$\{moduleId\}`/);
|
assert.match(combosModule, /namespace: `combos-\$\{moduleId\}`/);
|
||||||
assert.match(combosModule, /getGroupBoundaryProps/);
|
assert.match(combosBundle, /getGroupBoundaryProps/);
|
||||||
assert.match(combosModule, /combos-category-boundary-drop-zone/);
|
assert.match(combosBundle, /combos-category-boundary-drop-zone/);
|
||||||
assert.match(combosUtils, /function setComboItemCategory/);
|
assert.match(combosUtils, /function setComboItemCategory/);
|
||||||
assert.match(combosModule, /setItemGroup: setComboItemCategory/);
|
assert.match(combosModule, /setItemGroup: setComboItemCategory/);
|
||||||
assert.doesNotMatch(combosModule, /canMoveItem:/);
|
assert.doesNotMatch(combosModule, /canMoveItem:/);
|
||||||
|
|
|
||||||
139
website/src/features/toolboxes/modules/CombosList.jsx
Normal file
139
website/src/features/toolboxes/modules/CombosList.jsx
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
// Rôle : rend les combos enregistrés, leurs catégories et les contrôles de réorganisation.
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { ComboSequence, InlineTextInput } from "./CombosParts.jsx";
|
||||||
|
import { getComboCategory } from "./combosUtils.js";
|
||||||
|
|
||||||
|
export function CombosList({
|
||||||
|
entries,
|
||||||
|
data,
|
||||||
|
textContent,
|
||||||
|
reorder,
|
||||||
|
onComboUpdate,
|
||||||
|
onComboEdit,
|
||||||
|
onComboDelete,
|
||||||
|
onCategoryToggle
|
||||||
|
}) {
|
||||||
|
const categoryCount = entries.filter((entry) => entry.type === "group").length;
|
||||||
|
|
||||||
|
function renderCombo(combo) {
|
||||||
|
const category = getComboCategory(combo);
|
||||||
|
const comboClassName = [
|
||||||
|
"combo-card",
|
||||||
|
"is-editing",
|
||||||
|
reorder.isItemDragging(combo.id) ? "is-dragging" : "",
|
||||||
|
reorder.isItemDropTarget(combo.id) ? "is-drop-target" : "",
|
||||||
|
reorder.getDropPlacement("item", combo.id) === "after" ? "drop-after" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
return (
|
||||||
|
<article className={comboClassName} key={combo.id} {...reorder.getItemProps({ itemId: combo.id, groupId: category })}>
|
||||||
|
<button
|
||||||
|
className="task-planner-drag-handle combo-drag-handle"
|
||||||
|
type="button"
|
||||||
|
onPointerDown={(event) => reorder.itemReorder.startDrag(event, combo.id)}
|
||||||
|
aria-label={`${textContent.reorderComboTitle || "Déplacer le combo"} ${combo.name}`}
|
||||||
|
title={textContent.reorderComboTitle || "Déplacer le combo"}
|
||||||
|
>
|
||||||
|
<Icon name="drag" />
|
||||||
|
</button>
|
||||||
|
<div className="combo-card-main">
|
||||||
|
<InlineTextInput
|
||||||
|
className="tool-split-entry-label combo-title-input"
|
||||||
|
value={combo.name}
|
||||||
|
onCommit={(name) => onComboUpdate(combo.id, (current) => ({ ...current, name: name || current.name }))}
|
||||||
|
ariaLabel={textContent.comboNameLabel || "Nom"}
|
||||||
|
/>
|
||||||
|
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
|
||||||
|
</div>
|
||||||
|
<div className="combo-card-actions">
|
||||||
|
<button type="button" className="checklist-delete-button" onClick={() => onComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
|
||||||
|
<Icon name="edit" />
|
||||||
|
</button>
|
||||||
|
<button type="button" className="checklist-delete-button danger" onClick={() => onComboDelete(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
|
||||||
|
<Icon name="trash" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCategory(group, index, groupCount) {
|
||||||
|
const category = group.category;
|
||||||
|
const isCollapsed = data.collapsedCategories.includes(category);
|
||||||
|
const isLastCategory = index === groupCount - 1;
|
||||||
|
const categoryClassName = [
|
||||||
|
"combos-category checklist-section is-grouped",
|
||||||
|
isCollapsed ? "is-collapsed" : "",
|
||||||
|
reorder.isGroupDragging(category) ? "is-dragging" : "",
|
||||||
|
reorder.isGroupDropTarget(category) ? "is-drop-target" : "",
|
||||||
|
reorder.getDropPlacement("group", category) === "after" ? "drop-after" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
const showBoundaryDropZones = reorder.shouldShowGroupBoundaries();
|
||||||
|
const beforeDropZone = showBoundaryDropZones ? (
|
||||||
|
<div
|
||||||
|
className={`combos-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: category, placement: "before" }) ? "is-drop-target" : ""}`}
|
||||||
|
key={`${category}:before-drop`}
|
||||||
|
{...reorder.getGroupBoundaryProps({ groupId: category, placement: "before" })}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
const afterDropZone = showBoundaryDropZones ? (
|
||||||
|
<div
|
||||||
|
className={`combos-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: category, placement: "after" }) ? "is-drop-target" : ""}`}
|
||||||
|
key={`${category}:after-drop`}
|
||||||
|
{...reorder.getGroupBoundaryProps({ groupId: category, placement: "after" })}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
return [
|
||||||
|
beforeDropZone,
|
||||||
|
<section className={categoryClassName} key={category} {...reorder.getGroupProps({ groupId: category })}>
|
||||||
|
<div className="checklist-section-header combos-category-header">
|
||||||
|
<div className="combos-category-title">
|
||||||
|
<button
|
||||||
|
className="task-planner-category-drag-handle"
|
||||||
|
type="button"
|
||||||
|
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })}
|
||||||
|
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category}`}
|
||||||
|
title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}
|
||||||
|
>
|
||||||
|
<Icon name="drag" />
|
||||||
|
</button>
|
||||||
|
<h3>{category}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="checklist-section-actions">
|
||||||
|
<span>{group.combos.length}</span>
|
||||||
|
<button
|
||||||
|
className="checklist-section-collapse-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onCategoryToggle(category)}
|
||||||
|
aria-expanded={!isCollapsed}
|
||||||
|
aria-label={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||||
|
title={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||||
|
>
|
||||||
|
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="combos-list">
|
||||||
|
{group.combos.map((combo) => renderCombo(combo))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>,
|
||||||
|
showBoundaryDropZones && isLastCategory ? afterDropZone : null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryIndex(entryIndex) {
|
||||||
|
return entries.slice(0, entryIndex + 1).filter((entry) => entry.type === "group").length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEntry(entry, entryIndex) {
|
||||||
|
if (entry.type !== "group") return renderCombo(entry.item);
|
||||||
|
return renderCategory(entry.group, getCategoryIndex(entryIndex), categoryCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="combos-category-list">
|
||||||
|
{entries.map(renderEntry)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,8 @@ import {
|
||||||
useGroupedReorder
|
useGroupedReorder
|
||||||
} from "../../../hooks/useGroupedReorder.js";
|
} from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import { ComboInputToken, ComboSequence, InlineTextInput } from "./CombosParts.jsx";
|
import { ComboInputToken, ComboSequence } from "./CombosParts.jsx";
|
||||||
|
import { CombosList } from "./CombosList.jsx";
|
||||||
import {
|
import {
|
||||||
DEVICE_OPTIONS,
|
DEVICE_OPTIONS,
|
||||||
createCombo,
|
createCombo,
|
||||||
|
|
@ -322,122 +323,6 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
|
||||||
save({ ...data, collapsedCategories });
|
save({ ...data, collapsedCategories });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCombo(combo) {
|
|
||||||
const category = getComboCategory(combo);
|
|
||||||
const comboClassName = [
|
|
||||||
"combo-card",
|
|
||||||
"is-editing",
|
|
||||||
isItemDragging(combo.id) ? "is-dragging" : "",
|
|
||||||
isItemDropTarget(combo.id) ? "is-drop-target" : "",
|
|
||||||
getDropPlacement("item", combo.id) === "after" ? "drop-after" : ""
|
|
||||||
].filter(Boolean).join(" ");
|
|
||||||
return (
|
|
||||||
<article className={comboClassName} key={combo.id} {...getItemProps({ itemId: combo.id, groupId: category })}>
|
|
||||||
<button
|
|
||||||
className="task-planner-drag-handle combo-drag-handle"
|
|
||||||
type="button"
|
|
||||||
onPointerDown={(event) => itemReorder.startDrag(event, combo.id)}
|
|
||||||
aria-label={`${textContent.reorderComboTitle || "Déplacer le combo"} ${combo.name}`}
|
|
||||||
title={textContent.reorderComboTitle || "Déplacer le combo"}
|
|
||||||
>
|
|
||||||
<Icon name="drag" />
|
|
||||||
</button>
|
|
||||||
<div className="combo-card-main">
|
|
||||||
<InlineTextInput
|
|
||||||
className="tool-split-entry-label combo-title-input"
|
|
||||||
value={combo.name}
|
|
||||||
onCommit={(name) => updateCombo(combo.id, (current) => ({ ...current, name: name || current.name }))}
|
|
||||||
ariaLabel={textContent.comboNameLabel || "Nom"}
|
|
||||||
/>
|
|
||||||
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
|
|
||||||
</div>
|
|
||||||
<div className="combo-card-actions">
|
|
||||||
<button type="button" className="checklist-delete-button" onClick={() => startComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
|
|
||||||
<Icon name="edit" />
|
|
||||||
</button>
|
|
||||||
<button type="button" className="checklist-delete-button danger" onClick={() => deleteCombo(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
|
|
||||||
<Icon name="trash" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCategory(group, index, groupCount) {
|
|
||||||
const category = group.category;
|
|
||||||
const isCollapsed = data.collapsedCategories.includes(category);
|
|
||||||
const isLastCategory = index === groupCount - 1;
|
|
||||||
const categoryClassName = [
|
|
||||||
"combos-category checklist-section is-grouped",
|
|
||||||
isCollapsed ? "is-collapsed" : "",
|
|
||||||
isGroupDragging(category) ? "is-dragging" : "",
|
|
||||||
isGroupDropTarget(category) ? "is-drop-target" : "",
|
|
||||||
getDropPlacement("group", category) === "after" ? "drop-after" : ""
|
|
||||||
].filter(Boolean).join(" ");
|
|
||||||
const showBoundaryDropZones = shouldShowGroupBoundaries();
|
|
||||||
const beforeDropZone = showBoundaryDropZones ? (
|
|
||||||
<div
|
|
||||||
className={`combos-category-boundary-drop-zone ${isBoundaryDropTarget({ groupId: category, placement: "before" }) ? "is-drop-target" : ""}`}
|
|
||||||
key={`${category}:before-drop`}
|
|
||||||
{...getGroupBoundaryProps({ groupId: category, placement: "before" })}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
const afterDropZone = showBoundaryDropZones ? (
|
|
||||||
<div
|
|
||||||
className={`combos-category-boundary-drop-zone ${isBoundaryDropTarget({ groupId: category, placement: "after" }) ? "is-drop-target" : ""}`}
|
|
||||||
key={`${category}:after-drop`}
|
|
||||||
{...getGroupBoundaryProps({ groupId: category, placement: "after" })}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
return [
|
|
||||||
beforeDropZone,
|
|
||||||
<section className={categoryClassName} key={category} {...getGroupProps({ groupId: category })}>
|
|
||||||
<div className="checklist-section-header combos-category-header">
|
|
||||||
<div className="combos-category-title">
|
|
||||||
<button
|
|
||||||
className="task-planner-category-drag-handle"
|
|
||||||
type="button"
|
|
||||||
onPointerDown={(event) => groupReorder.startDrag(event, { groupId: category })}
|
|
||||||
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category}`}
|
|
||||||
title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}
|
|
||||||
>
|
|
||||||
<Icon name="drag" />
|
|
||||||
</button>
|
|
||||||
<h3>{category}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="checklist-section-actions">
|
|
||||||
<span>{group.combos.length}</span>
|
|
||||||
<button
|
|
||||||
className="checklist-section-collapse-button"
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleCategoryCollapsed(category)}
|
|
||||||
aria-expanded={!isCollapsed}
|
|
||||||
aria-label={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
|
||||||
title={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
|
||||||
>
|
|
||||||
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="combos-list">
|
|
||||||
{group.combos.map((combo) => renderCombo(combo))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>,
|
|
||||||
showBoundaryDropZones && isLastCategory ? afterDropZone : null
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const categoryCount = entries.filter((entry) => entry.type === "group").length;
|
|
||||||
let renderedCategoryIndex = -1;
|
|
||||||
|
|
||||||
function renderEntry(entry) {
|
|
||||||
if (entry.type !== "group") return renderCombo(entry.item);
|
|
||||||
renderedCategoryIndex += 1;
|
|
||||||
return renderCategory(entry.group, renderedCategoryIndex, categoryCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="combos-module">
|
<div className="combos-module">
|
||||||
{(editing || editingComboId) && (
|
{(editing || editingComboId) && (
|
||||||
|
|
@ -601,9 +486,29 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
|
||||||
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="combos-category-list">
|
<CombosList
|
||||||
{entries.map(renderEntry)}
|
entries={entries}
|
||||||
</div>
|
data={data}
|
||||||
|
textContent={textContent}
|
||||||
|
reorder={{
|
||||||
|
itemReorder,
|
||||||
|
groupReorder,
|
||||||
|
getItemProps,
|
||||||
|
getGroupProps,
|
||||||
|
getGroupBoundaryProps,
|
||||||
|
isItemDragging,
|
||||||
|
isItemDropTarget,
|
||||||
|
isGroupDragging,
|
||||||
|
isGroupDropTarget,
|
||||||
|
isBoundaryDropTarget,
|
||||||
|
getDropPlacement,
|
||||||
|
shouldShowGroupBoundaries
|
||||||
|
}}
|
||||||
|
onComboUpdate={updateCombo}
|
||||||
|
onComboEdit={startComboEdit}
|
||||||
|
onComboDelete={deleteCombo}
|
||||||
|
onCategoryToggle={toggleCategoryCollapsed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue