Split combos list rendering from module state
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-24 21:54:43 +02:00
parent 602486a1d7
commit 73e84ee703
3 changed files with 168 additions and 122 deletions

View file

@ -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 combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.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 calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.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, /reorderFeatures/);
assert.match(combosModule, /namespace: `combos-\$\{moduleId\}`/);
assert.match(combosModule, /getGroupBoundaryProps/);
assert.match(combosModule, /combos-category-boundary-drop-zone/);
assert.match(combosBundle, /getGroupBoundaryProps/);
assert.match(combosBundle, /combos-category-boundary-drop-zone/);
assert.match(combosUtils, /function setComboItemCategory/);
assert.match(combosModule, /setItemGroup: setComboItemCategory/);
assert.doesNotMatch(combosModule, /canMoveItem:/);

View 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>
);
}

View file

@ -7,7 +7,8 @@ import {
useGroupedReorder
} from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ComboInputToken, ComboSequence, InlineTextInput } from "./CombosParts.jsx";
import { ComboInputToken, ComboSequence } from "./CombosParts.jsx";
import { CombosList } from "./CombosList.jsx";
import {
DEVICE_OPTIONS,
createCombo,
@ -322,122 +323,6 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
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 (
<div className="combos-module">
{(editing || editingComboId) && (
@ -601,9 +486,29 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
)}
<div className="combos-category-list">
{entries.map(renderEntry)}
</div>
<CombosList
entries={entries}
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>
);
}