Allow linking an existing toolbox from the game drawer
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s

This commit is contained in:
Shinuwa 2026-10-01 09:33:18 +02:00
parent 962816bbb9
commit c3bab44a4b
7 changed files with 107 additions and 1 deletions

View file

@ -54,6 +54,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
- Vérifier la page `/games/<gameId>`.
- Vérifier le chargement à la première visite, la conservation des données pendant la navigation et les erreurs réseau ; sous StrictMode, les requêtes abandonnées ne doivent pas remplacer les données actuelles ni afficher d'erreur d'annulation.
- Vérifier que le bouton texte « Afficher la toolbox » devient « Masquer la toolbox » avec le panneau latéral ouvert.
- Sans toolbox liée, vérifier la création ainsi que l'association directe d'une toolbox existante ; le sélecteur ne doit proposer que les toolboxes qui ne sont liées à aucun jeu.
- Vérifier le breadcrumb.
- Vérifier les textes avec retours à la ligne.
- Pour les guides à cartes, vérifier le toggle de lien à côté du « + » dans l'en-tête de chaque checklist compatible : il affiche les commandes, masquées par défaut. Vérifier l'association exclusive, le masquage désactivé sur les autres listes et la mise à jour immédiate depuis le panneau latéral. Refermer les commandes conserve l'association. La checkbox des cartes met à jour toutes les lignes exactes correspondantes, respecte les quantités cibles et ne retourne pas les cartes.

View file

@ -219,6 +219,29 @@ test("creates a game-linked toolbox from the drawer", async ({ page }) => {
await expect(page.getByRole("button", { name: "Masquer la toolbox", exact: true })).toBeVisible();
});
test("links an available toolbox directly from the empty game drawer", async ({ page }) => {
await createToolbox(page, "Toolbox déjà liée");
await page.goto("/#/games/mhwilds");
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
const drawer = page.locator("#toolbox-drawer .drawer-panel");
await expect(drawer.getByRole("heading", { name: "Ou associer une toolbox déjà existante" })).toBeVisible();
await drawer.getByLabel("Toolbox disponible").selectOption({ label: "Toolbox déjà liée" });
await drawer.getByRole("button", { name: "Associer", exact: true }).click();
await expect(drawer.getByRole("heading", { name: "Toolbox déjà liée", exact: true })).toBeVisible();
await createToolbox(page, "Toolbox libre");
await page.goto("/#/games/diablo4");
await page.getByRole("button", { name: "Afficher la toolbox", exact: true }).click();
await expect(drawer.getByLabel("Toolbox disponible").getByRole("option", { name: "Toolbox déjà liée" })).toHaveCount(0);
await drawer.getByLabel("Toolbox disponible").selectOption({ label: "Toolbox libre" });
await drawer.getByRole("button", { name: "Associer", exact: true }).click();
await expect(drawer.getByRole("heading", { name: "Toolbox libre", exact: true })).toBeVisible();
await expect(drawer.getByRole("heading", { name: "Ou associer une toolbox déjà existante" })).toHaveCount(0);
});
test("imports and exports a toolbox payload", async ({ page }) => {
await page.goto("/#/toolboxes");
await expect(page.getByRole("main")).toBeVisible();

View file

@ -248,6 +248,13 @@ export function validateSiteContent(site) {
"toolboxes.spaces.moveSuccess",
"toolboxes.spaces.emptyText",
"toolboxes.spaces.limitReached",
"toolboxes.drawer.emptyText",
"toolboxes.drawer.createLabel",
"toolboxes.drawer.linkExistingTitle",
"toolboxes.drawer.selectLabel",
"toolboxes.drawer.selectPlaceholder",
"toolboxes.drawer.noAvailable",
"toolboxes.drawer.linkLabel",
"toolboxes.title",
"toolboxes.newButton",
"toolboxes.libraryLink",

View file

@ -1794,6 +1794,15 @@
]
},
"toolboxes": {
"drawer": {
"emptyText": "Aucune toolbox associée à cette page jeu.",
"createLabel": "Créer et associer",
"linkExistingTitle": "Ou associer une toolbox déjà existante",
"selectLabel": "Toolbox disponible",
"selectPlaceholder": "Choisir une toolbox",
"noAvailable": "Aucune toolbox disponible",
"linkLabel": "Associer"
},
"storageStatus": {
"loadingTitle": "Chargement des données locales",
"mediaLoadingMessage": "Chargement des médias…",

View file

@ -486,7 +486,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
<button className="drawer-close-button nds-icon-button" data-size="sm" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
</header>
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} storageReady={storageReady} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} guideChecklist={guideChecklist} /> : (
<div className="empty nds-empty-state"><p>Aucune toolbox associée à cette page jeu.</p><button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
<ToolboxDrawerEmptyState gameId={gameId} toolboxes={toolboxes} links={links} actions={actions} content={siteContent.toolboxes.drawer} />
)}
<StorageQuota usage={storageUsage} />
</div>
@ -495,6 +495,42 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
);
}
function ToolboxDrawerEmptyState({ gameId, toolboxes, links, actions, content }) {
const linkedToolboxIds = useMemo(() => new Set(Object.values(links).filter(Boolean)), [links]);
const availableToolboxes = useMemo(
() => toolboxes.filter((toolbox) => !linkedToolboxIds.has(toolbox.id)),
[linkedToolboxIds, toolboxes]
);
const [selectedId, setSelectedId] = useState("");
const validSelectedId = availableToolboxes.some((toolbox) => toolbox.id === selectedId) ? selectedId : "";
const hasAvailableToolboxes = availableToolboxes.length > 0;
return (
<div className="drawer-empty-toolbox">
<section className="empty nds-empty-state">
<p>{content.emptyText}</p>
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={() => actions.setCreateModal({ gameId })}>{content.createLabel}</button>
</section>
<section className="drawer-link-existing nds-empty-state" aria-labelledby="drawer-link-existing-title">
<h2 id="drawer-link-existing-title">{content.linkExistingTitle}</h2>
<form onSubmit={(event) => {
event.preventDefault();
if (validSelectedId) actions.linkToolboxToGame(gameId, validSelectedId);
}}>
<label className="drawer-link-existing-field">
<span>{content.selectLabel}</span>
<select className="nds-select" data-size="sm" value={validSelectedId} disabled={!hasAvailableToolboxes} onChange={(event) => setSelectedId(event.target.value)}>
<option value="">{hasAvailableToolboxes ? content.selectPlaceholder : content.noAvailable}</option>
{availableToolboxes.map((availableToolbox) => <option key={availableToolbox.id} value={availableToolbox.id}>{availableToolbox.name}</option>)}
</select>
</label>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit" disabled={!validSelectedId}>{content.linkLabel}</button>
</form>
</section>
</div>
);
}
function EditableTitle({ value, fallback, onSave, className = "module-title", tutorialTarget = "" }) {
const inlineEdit = useInlineEdit({
value,

View file

@ -429,6 +429,10 @@
max-width: none;
}
.drawer-link-existing form {
grid-template-columns: 1fr;
}
.drawer-resize-handle {
display: none;
}

View file

@ -66,6 +66,32 @@ body.is-resizing-drawer * {
opacity: 0.56;
}
.drawer-empty-toolbox {
display: grid;
gap: var(--space-4);
}
.drawer-link-existing h2 {
font-size: var(--font-size-md);
}
.drawer-link-existing form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-3);
align-items: end;
}
.drawer-link-existing-field {
display: grid;
gap: var(--space-2);
}
.drawer-link-existing-field,
.drawer-link-existing .nds-select {
width: 100%;
}
.drawer-close-button {
flex: 0 0 40px;
}