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 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:/);
|
||||
|
|
|
|||
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
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue