Skip to content

feat(UX): improve document associations, previews and deletion navigation - #1552

Merged
Paul-AUB merged 17 commits into
developfrom
feat/improvements-ux
Oct 6, 2026
Merged

Paul-AUB merged 17 commits into
developfrom
feat/improvements-ux

Conversation

@Paul-AUB

@Paul-AUB Paul-AUB commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

feat(UX): improve document associations, previews and deletion navigation

🤔 What

Related to #1156.

  1. Associate entrances from a document page. Authenticated users can open an entrance search from the Linked entities section, select multiple entrances across result pages, and associate them with the current document. The section is also available when the document has no linked entities. Successful submission refreshes the associations and closes the search; failed submission preserves the selection. The action is disabled offline and hidden for deleted documents and read-only previews. Association is available from document details; extending document creation/edit forms remains follow-up work for [DOCUMENT] ajout des entrées #1156.
  2. Preview documents before associating them. In the document search used for association, clicking a desktop result row opens the document details in a dialog, while checkboxes control selection. Closing the preview preserves the pending selection. In card view, a short tap selects the document; the preview button or a long press opens its details. Previewing works before selection and hides document actions.
  3. Improve document validation on narrow screens. Detail and edit dialogs now become fullscreen according to viewport width. This works on phones, tablets with narrow viewports, and resized desktop windows. Detail previews use reduced horizontal padding to leave more space for document content; association previews use the same layout.
  4. Prevent an entity from replacing itself during deletion. Replacement search filters out the entity being deleted. Selection and confirmation also guard against self-replacement, including when IDs are represented as strings or numbers. This applies to entrances, networks, massifs, organizations, documents, and persons.
  5. Navigate to the replacement after successful deletion. Permanent deletion opens the selected replacement, falls back to an existing redirect, then to the homepage when neither exists. Navigation waits for API success. Failed requests keep the current page and restore the displayed deletion state. Person deletion also navigates only after success.
  6. Keep deleted entrance section titles readable. Struck-through titles can wrap across lines, including long unbroken words, so they remain readable on narrow screens.
  7. Make network descriptions easier to discover. The notice linking an entrance to its network descriptions appears in an outlined callout with a colored border and light background. It retains links to the network and its descriptions, and accommodates long names.
  8. Link the published version to release notes. The homepage footer links a version in major.minor.patch format to its matching GitHub release. Development and preview version labels remain plain text.

The new association labels, empty-state message, and selection guidance are translated across all 15 supported language files.

🤷‍♂️ Why

Contributors need to inspect documents before linking them and manage entrance associations directly while viewing a document, supporting the document verification workflow described in #1156. These changes also make document review easier on small screens, keep replacement navigation consistent after deletion, and improve the visibility of descriptions and release notes.

🔍 How

  • Reuse the existing advanced entrance search, result table, section action, and document detail view. Document previews are lazy-loaded, with independent preview and selection state.
  • Add a React Query mutation that uses the existing document-to-entrance association endpoint for each selected entrance. It waits for every request and invalidates the document and affected entrances even after partial failure, so completed links are reflected in the UI.
  • Add a dense option to StandardDialog and use MUI's sm viewport breakpoint for fullscreen document dialogs. Mobile selection cards expose a separate preview button and reuse the long-press hook.
  • Centralize replacement URLs in deletedEntityRedirect.js, reuse that utility in deleted entity cards, and navigate from successful deletion callbacks.

🔗 Related API PR

None required. The association flow uses an existing API endpoint.

🧪 Testing

  • Full repository ESLint check: yarn lint completed with no errors or warnings.
  • Targeted Vitest checks: 46 tests passed across 13 files covering association forms and mutations, document preview state, desktop/mobile tables, replacement filtering and redirects, person deletion, section titles, network descriptions, and footer release links.
  • Translation synchronization: node scripts/translations/sync-with-en.js --check.
  • Translation ordering: node scripts/translations/sort.js --check.
  • Patch whitespace: git diff --check origin/develop...HEAD.
  • Cypress: associate-document-preview.cy.js passed in headless Electron, checking desktop row preview, mobile card preview, and independent selection.
  • On a document page, associate several entrances across result pages and verify the refreshed linked entities. Check the empty state, offline behavior, and failed submission.
  • On a narrow viewport, verify validation detail/edit dialogs, preview padding, and long-press preview without changing selection.
  • Delete an entity with a replacement and verify navigation after success; check the existing-redirect and homepage fallbacks and a failed request.
  • Visually verify wrapping deleted titles, the network-description callout, and the published-version release link.

📸 Previews

No screenshots attached.

@Paul-AUB
Paul-AUB requested review from ClemRz and urien October 1, 2026 15:54
@Paul-AUB Paul-AUB self-assigned this Oct 1, 2026
@Paul-AUB Paul-AUB linked an issue Oct 1, 2026 that may be closed by this pull request
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
<CardActionArea
onClick={handleClick}
{...(longPressHandlers ?? {})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press handlers are spread onto the whole CardActionArea, which also contains the selection Checkbox (line 116). That checkbox only guards onClick with stopPropagation — it has no touch-event guard, and onTouchStart/onTouchEnd still bubble up to the action area.

Scenario: in the document-association list, the user presses and holds on a row checkbox for 500 ms or more. At 500 ms useLongPress fires, longPressFiredRef is set and onRowClick(doc) opens the full document preview dialog. On release, the checkbox's own onChange still fires, so the single gesture both opens the preview and toggles the row's selection — one deliberate action produces two unrelated state changes.

Stopping touch propagation on the checkbox (or attaching the long-press handlers to the CardContent rather than the action area) would keep the two gestures separate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — the checkbox now stops touch start/move/end/cancel propagation, so holding it cannot start the card's preview gesture. Added unit and Cypress coverage for a 600 ms checkbox press followed by selection.

Commit: 09ceb9eda.

}
: null;

const handleClick = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

longPressFiredRef is only cleared in two places: the wrapped onTouchStart and this handleClick early return. Neither runs if the long-press is not followed by a click on this card.

Scenario: a long press fires and the preview dialog opens. The synthetic click that follows is retargeted to the dialog/backdrop, so handleClick never runs and the ref stays true. The dialog is closed, and the user then activates the same card without a new touch sequence — Enter/Space on the CardActionArea (it is a ButtonBase, so it is keyboard-focusable) or a mouse click on a hybrid touch/trackpad device. handleClick sees the stale true, swallows that first activation and only resets the flag, so the selection toggle silently does nothing and the user has to click twice.

Clearing the ref on onTouchEnd/onTouchCancel (after the click has had a chance to fire) or keying it off onMouseDown/onKeyDown as well would avoid the stale state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — a fresh mouse/pen pointer press or keyboard activation resets the long-press suppression flag; touch cancellation also clears it. The compatibility click from the original touch remains suppressed. Regression tests cover the intercepted-click scenario followed by keyboard and mouse activation.

Commit: 09ceb9eda.

});
};

export const useLinkDocumentToEntrances = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useLinkDocumentToEntrances re-implements, line for line, the linkDocuments helper already defined at the top of this same file (lines 19-39): Promise.allSettled over the pairs, filter the rejections, attach error.rejections, rethrow the first reason. Only the URL builder and the loop variable differ.

The risk is divergence rather than a crash: a future fix to the partial-failure contract (e.g. aggregating messages, or switching to AggregateError) will be applied to one copy and silently missed in the other, so the two association flows will start reporting different things for the same kind of partial failure.

Worth extracting a single associateAll(pairs, urlFor) helper in this module and having both linkDocuments and this new hook call it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — both batch association mutations now call one associateAll helper. It waits for every request, attaches all rejection reasons to the first error, and preserves the existing cache invalidation behavior. The association tests cover success and partial failure in both directions.

Commit: 09ceb9eda.

onSuccess: () => {
if (isPermanent) {
navigate(
getPostDeletionUrl(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This onDeletePress body is now duplicated verbatim in five places — components/appli/Entry/index.jsx:117, components/appli/Massif/Massif.jsx:100, components/appli/Network/index.jsx:99, components/appli/Organization/index.jsx:101 and pages/DocumentDetails/index.jsx:215 — with only the entity id, the DELETED_ENTITIES key and the redirectTo accessor changing. Person.jsx is a sixth, slightly drifted copy (always permanent, no onError rollback).

That drift is the concrete risk: Person.jsx already lacks the onError handler that resets wantedDeletedState, so a failed person deletion leaves the card stuck in its optimistic "deleted" state while the other five recover. Any further change to the post-deletion contract has to be made six times and will drift again.

Since getPostDeletionUrl was extracted anyway, consider going one step further and extracting the whole thing — e.g. usePostDeletion(entityType, id, entity) returning onDeletePress plus the wantedDeletedState pair — so there is a single implementation to keep correct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — extracted usePostDeletion and reused it in all six entity pages. It owns the desired deletion state, error rollback, and navigation after successful permanent deletion; each page still supplies its own mutation hook. Person deletion defaults to permanent. Regression tests cover replacement/existing-redirect/homepage navigation, failures, soft deletion, and state synchronization.

One factual clarification: the current Person component had no optimistic wantedDeletedState, so the specific stuck-deleted-card scenario was not present there. It now shares the same deletion callback contract as the other pages.

Commit: 09ceb9eda.

startIcon={<DeleteForeverIcon />}
disabled={isSearchMandatory && !selectedEntity}
disabled={
isCurrentEntity(selectedEntity, entityId) ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things about this guard.

First, isCurrentEntity(selectedEntity, entityId) is unreachable: handleSelection (line 239) refuses to store a self-selection, and the suggestion list is already filtered at line 231, so selectedEntity can never be the current entity by the time it reaches this disabled expression. It reads as a safety net but it is dead code, which makes the real invariant harder to see.

Second — and this is the user-visible part — because handleSelection silently drops a self-selection, picking your own entity in the autocomplete clears the input and leaves the form in exactly the state it was in, with no message. A moderator who types the current entity's name (an easy mistake when merging near-duplicates) sees the field blank itself and no explanation of why nothing happened.

Keeping the guard in one place and surfacing it — reject in handleSelection and show a short "cannot redirect to itself" helper text — would be both simpler and clearer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — removed the redundant self-selection check from the confirmation button and kept rejection at the selection boundary, alongside suggestion filtering. A helper explains that an entity cannot redirect to itself; a stale self-selection highlights the field/helper and no longer clears the input silently. The message is translated in all 15 language files, and the regression test verifies rejection feedback and valid replacement selection.

Commit: 09ceb9eda.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae9afa87b. I chose the permanent constraint hint: removed the unreachable hasSelfSelectionError state, its error colouring, and aria-live from that hint. The current entity is still filtered out, and the selection boundary keeps a defensive guard without inventing a user-visible error state.

Search failures now display a separate localized error message with role="alert", which disappears when the query recovers. The new message is translated in all 15 language files.

Removed the artificial stale-result button from the test mock. The tests now use the real AutoCompleteSearch, covering filtering with numeric/string IDs, selecting an available replacement, and search failure/recovery. All 3 tests pass; full repository lint passes with no errors or warnings.

/>

{linkedEntities.length > 0 && (
{(linkedEntities.length > 0 ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

permissions.isAuth && !documentData.isDeleted && !hideActions is evaluated twice (here for section visibility, again at line 810 for the icon), and linkedEntities.length > 0 three times. A canAssociateEntrances const would remove the chance of the two copies drifting.

More concretely, the icon prop: when the condition is false this expression evaluates to false, not undefined. ScrollableContent renders the icon slot as {!isNil(icon) && <div role="presentation" onClick={...}>{icon}</div>}, and isNil(false) is false — so an empty presentational <div> with a click handler is injected into the section header for every anonymous visitor and every hideActions render (i.e. every time this page is shown inside the search preview dialog). Harmless visually, but it is a clickable empty element in the header. icon={cond ? <SectionCreateButton ... /> : undefined} avoids it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — introduced canAssociateEntrances and hasLinkedEntities, and the unavailable action now passes undefined to the icon slot. Applied the same icon-slot correction to Entry/Documents. The entrance-section regression tests verify that the action slot disappears when editing/authentication is no longer allowed.

Commit: 09ceb9eda.

}
content={
<>
{isEntranceSearchVisible && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isEntranceSearchVisible is only ever reset by the toggle button and by onSuccess. Nothing resets it when the document stops being associable.

Scenario: a moderator opens the Associate panel, then soft-deletes the document from the action bar on the same page. documentData.isDeleted flips to true, so the icon condition at line 810 goes false and the toggle button disappears — but isEntranceSearchVisible is still true, so the SearchEntranceForm stays mounted with no way to dismiss it, and submitting it fires useLinkDocumentToEntrances against a now-deleted document.

Resetting the flag when the entity becomes deleted (or gating the content on the same condition as the icon) closes both the stuck-UI and the submit-after-delete paths. Note Entry/Documents has the same shape, so this may be a pre-existing pattern worth fixing in one place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed — the entrance search form is rendered only while the document is associable, using the same condition as its toggle. Pending deletion also closes the form. Entry/Documents now gates its search the same way when editing/authentication is lost. Cypress verifies closing an open association panel after document soft deletion; unit tests cover the entrance panel's permission changes.

Commit: 09ceb9eda.

@Paul-AUB
Paul-AUB requested a review from ClemRz October 2, 2026 17:54
@ClemRz

ClemRz commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Re-review of 09ceb9ed

Six of the seven findings are resolved. Verified individually:

  • Long-press vs. checkbox — the Checkbox now stops onTouchStart/Move/End/Cancel, so the synthetic touch events never reach CardActionArea and the long-press timer never starts on the checkbox. Fixed.
  • Stale longPressFiredRef — onTouchCancel plus the non-touch onPointerDown and onKeyDown resets cover every re-entry path. useLongPress does export onTouchCancel, so the new delegation is safe.
  • associateAll — both mutations now share one partial-failure contract.
  • usePostDeletion — all six copies collapsed into the hook. The isPermanent = true default is right for Person, the only call site that omits it (useDeletePerson ignores the flag and the API has no soft delete for cavers); the other five pass it explicitly. The [id, entity?.isDeleted] deps are also an improvement on the old [entity] identity check, which clobbered the optimistic state on any refetch.
  • icon rendering false — : undefined in both DocumentDetails and Entry/Documents stops the empty role="presentation" div.
  • Association panel after soft delete — canAssociateEntrances gates the panel, so it closes on optimistic delete.

The new An entity cannot redirect to itself. key is present in all 15 language files.


Remaining: the self-selection error is unreachable

hasSelfSelectionError can never become true through the UI.

suggestions already filters the current entity out before the list reaches the autocomplete:

const suggestions = (data?.results ?? []).filter(
suggestion => !isCurrentEntity(suggestion, entityId)
);

AutoCompleteSearch passes that filtered array straight through as MUI's options (filterOptions={x => x}, no freeSolo, no autoSelect), so onSelection can only ever receive one of those options or null — and isCurrentEntity(null, entityId) is false because of the candidate?.id != null guard. The setHasSelfSelectionError(true) branch is dead:

if (isCurrentEntity(selection, entityId)) {
setHasSelfSelectionError(true);
return;
}

The test passes only because the AutoCompleteSearch mock adds a hardcoded Select stale current result button that calls onSelection with the current entity — a call the real component cannot make. The mock's own suggestions.map right above it shows why:

vi.mock('../AutoCompleteSearch', () => ({
default: ({ suggestions, onSelection }) => (
<div>
{suggestions.map(suggestion => (
<button
key={suggestion.id}
type="button"
onClick={() => onSelection(suggestion)}>
{suggestion.name}
</button>
))}
<button
type="button"
onClick={() =>
onSelection({ id: 42, name: 'Current massif', _type: 'massifs' })
}>
Select stale current result
</button>
</div>
)

So the guard is harmless defence in depth, but it isn't what users hit, and the Mui-error assertion doesn't cover a reachable state.

Side effect: the helper text misdiagnoses search failures

Because the error never fires, the only user-visible change is that FormHelperText renders unconditionally whenever nothing is selected — error only controls its colour:

<FormHelperText
error={hasSelfSelectionError}
aria-live="polite">
{formatMessage({
id: 'An entity cannot redirect to itself.'
})}
</FormHelperText>

When useQuickSearch fails, hasError={!!error || ...} turns the field red and shows the ErrorIcon, while the only text in that area reads An entity cannot redirect to itself. in grey — pointing at the wrong cause. aria-live="polite" also never announces anything, since the text content is static and only the colour changes.

Worth deciding which of the two this is meant to be: a permanent constraint hint (then drop aria-live and the error colouring, and give the query failure its own message), or an actual error (then render it only when hasSelfSelectionError is set, and keep a separate message for !!error).

@Paul-AUB

Paul-AUB commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the re-review in ae9afa87b.

  • Unreachable self-selection error: removed hasSelfSelectionError and made the existing text a permanent, neutral constraint hint without aria-live or error colouring. Filtering and the defensive selection guard remain.
  • Search failure feedback: added a distinct translated error message, announced only when a query fails and removed after recovery.
  • Test fidelity: replaced the autocomplete mock and its impossible stale-result button with the real component. Tests exercise the actual available options, numeric/string IDs, and failure/recovery.

Validation:

  • 3 targeted Vitest tests passed: yarn.cmd test src/components/common/card/Deleted.test.jsx.
  • Full repository lint: yarn.cmd lint, no errors or warnings.
  • Translation synchronization and ordering checks passed for all 15 language files.
  • git diff --check passed.

Also replied in the original inline thread.

@Paul-AUB
Paul-AUB requested a review from ClemRz October 2, 2026 20:27
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net

@ClemRz

ClemRz commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Re-review of ae9afa87

This resolves the last finding. Verified:

  • hasSelfSelectionError is gone, so there is no longer a state that can't be reached. The isCurrentEntity check in handleSelection stays as a bare return — unreachable through the UI because suggestions already filters self out, but harmless defence in depth that no longer implies a user-facing path.
  • The constraint text is now an unconditional hint with no error colouring and no aria-live, which is what it actually is.
  • Search failures get their own message, conditionally mounted with role="alert" — so it does announce on appearance, unlike the previous static aria-live node.
  • Unable to search for a replacement. Please try again. is present and translated in all 15 language files.

The test rewrite is the part worth calling out: dropping the AutoCompleteSearch mock means these assertions now run against the real MUI Autocomplete, so queryByRole('option', { name: /Current massif/ }) is genuine evidence that the filter holds end to end rather than an artifact of the mock. Covering both 42 and '42' pins the String() coercion in isCurrentEntity, and the error show/clear cycle asserts aria-invalid on the combobox rather than just the text.

I was able to run the suite this time (the earlier pdfjs-dist failure was a local install gap, not a branch problem):

  • vitest run — 177 files, 1203 tests, all passing
  • eslint . — clean

Optional, not a blocker

Neither FormHelperText is wired to the combobox via aria-describedby, since AutoCompleteSearch has no helperText passthrough. role="alert" covers the moment the error appears, but someone who focuses the field afterwards gets aria-invalid="true" with no accessible reason, and the constraint hint is never announced at all. Worth considering as a separate change to the shared component rather than here, as it would touch its other consumers.

@Paul-AUB

Paul-AUB commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

@urien cette PR est prete pour etre testée à toutes fins utiles. Il y a plusieurs fonctionnalités d'amélioration UI/UX pour les contributeurs et admins/moderateurs

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Associate entrances from a document page.
J'ai pu associer des entrées a un document existant depuis la page du document mais il n'y a pas de bouton pour dissocier comme utilisateur, responsable ou modérateur

Depuis le formulaire d'édition d'un document je n'ai pas vu de champ avec les entrées associées

Test fait avec https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net/ui/documents/234372/edit

De même quand je créé un nouveau document il n'y a pas de champ pour lier une entrée

Pour tester les différents rôles j'ai utilisé l'outil disponible pour les admins

@Paul-AUB

Paul-AUB commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Depuis le formulaire d'édition d'un document je n'ai pas vu de champ avec les entrées associées

Oui, par garder la même logique que sur les autres pages, j'ai rajouté le bouton d'association ici :

image

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Preview documents before associating them
Tout fonctionne, c'est très pratique

@Paul-AUB

Paul-AUB commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

mais il n'y a pas de bouton pour dissocier comme utilisateur, responsable ou modérateur

En effet, je n'y ait pas pensé, bonne remarque. Pour garder la même UX je peux rajouter ici un bouton réservé aux admins :

image

Comme ça on garde la même logique par rapport aux autres pages. Est-ce que cela convient ?

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Improve document validation on narrow screens
Cela fonctionne. Eventuellement, pour améliorer le fonctionnement pour les modérateurs mettre sur la fenetre de visualisation les boutons de validation, refus ou édition du document.
Il y a nécessité de permettre la validation en masse des documents mais actuellement il y a pas mal de validations qui se font sans que les informations soient vérifiées : erreur de type de document, auteur indiqué qui n'est pas l'auteur indiqué sur le document.
On sort du scope mais si on accepte qu'il faille voir un document avant de le valider on aurait peut être moins de problèmes

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

mais il n'y a pas de bouton pour dissocier comme utilisateur, responsable ou modérateur

En effet, je n'y ait pas pensé, bonne remarque. Pour garder la même UX je peux rajouter ici un bouton réservé aux admins :
image

Comme ça on garde la même logique par rapport aux autres pages. Est-ce que cela convient ?

Oui, mais a priori tous les utilisateurs connectés peuvent le faire (comme c'est le cas pour les associations des entrées avec des organisations par exemple)

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Prevent an entity from replacing itself during deletion
Très belle fonctionnalité
Personnes : ok mais j'ai pu acceder au service comme modérateur
image
pas comme responsable et utilisateur
Massif, Document, Organisation, réseau : idem que pour personnes

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Navigate to the replacement after successful deletion
Test réalisé avec une organisation > OK

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Keep deleted entrance section titles readable
OK (test sur description)
image

@urien

urien commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Make network descriptions easier to discover

Je suis allé sur https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net/ui/entrances/1 qui est lié au réseau https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net/ui/caves/75262
Cela fonctionne bien mais il faut sans doute améliorer le lien vers les descriptions de la page du réseau : on a le mot description de la page réseau qui est un peu "mangé" sur mon ordinateur
image

@ClemRz ClemRz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 5eb6e0eb and 179775f4

Thanks, both changes are correct.

Verified

  • The blocking bug from the last round is fixed. Caves are now canUnlink: false (index.jsx#L366), and onUnlink dispatches on entity.type (L852-L864). A massif can no longer be sent to the entrance endpoint.
  • Massif unlinking is wired up correctly. useUnlinkDocumentToMassif gets { massifId, documentId }, which matches its signature, and invalidates both massifKeys.detail and documentKeys.detail.
  • Gating both types behind isModerator is correct. In the API, entrance/unlink-document.js and massif/unlink-document.js both require hasGroup(MODERATOR).
  • yarn test: 180 files, 1234 tests pass. yarn lint is clean.

Blocking

No test covers the page-level fix. The new test in LinkedEntityCards.test.jsx#L70 passes hand-written entities with canUnlink already set. It tests that the component respects the flag, not that DocumentDetails sets it correctly. The bug was in the page's config and callback.

To check, I put back the pre-fix DocumentDetails/index.jsx from fd28e7bd and ran the full suite. All 1234 tests still passed. So nothing in the suite would catch this bug if it came back.

DocumentDetails/index.jsx has no test file. Please add one, even a narrow one, covering the behaviour this PR changes. As a moderator viewing a document with a massif, a cave and an entrance linked:

  • the cave has no unlink button;
  • unlinking the massif calls the massif mutation with { massifId, documentId };
  • unlinking the entrance calls the entrance mutation with { entranceId, documentId };
  • a non-moderator sees no unlink buttons.

Mocking the two mutation hooks and the document query is enough. No real API calls are needed.

Nits (non-blocking)

  • The comment above canUnlinkLinkedEntities (L198) still mentions only entrance/unlink-document.js. It now covers massif/unlink-document.js too.
  • The return null for other types (L864) can't be reached today, because caves are opted out. If it ever is reached, LinkedEntityCards awaits it and closes the dialog as if the unlink worked. Throwing there would make that case fail loudly.

@Paul-AUB
Paul-AUB force-pushed the feat/improvements-ux branch from 179775f to 651d5f6 Compare October 5, 2026 16:21
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net

@Paul-AUB

Paul-AUB commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the latest review in 510321e78.

  • Added DocumentDetails/index.test.jsx, rendering the real page, linked cards and confirmation dialog with mocked queries and mutations. The fixture contains a massif, cave and entrance with overlapping IDs and no precomputed canUnlink flags, so the page supplies the configuration being tested.
  • Covered all four requested behaviors: caves have no unlink button; confirming a massif unlink calls only the massif mutation with { massifId, documentId }; confirming an entrance unlink calls only the entrance mutation with { entranceId, documentId }; non-moderators see no unlink buttons, both authenticated and anonymous.
  • Updated the permission comment to name both backend unlink handlers.
  • Replaced the unsupported-type return null with an explicit throw, so an unsupported callback cannot be treated as a successful unlink and dismiss the confirmation dialog.

Validation:

  • 17 tests passed across the new page test, LinkedEntityCards.test.jsx and useDocumentAssociations.test.jsx: yarn.cmd test src/pages/DocumentDetails/index.test.jsx src/components/common/entitiesList/LinkedEntityCards.test.jsx src/hooks/mutations/useDocumentAssociations.test.jsx.
  • Regression check: temporarily restored the pre-fix page from fd28e7bd and ran the new page tests. Two tests failed specifically on the cave unlink button and the massif mutation routing; the other three passed. Restored the current page and reran the targeted checks successfully.
  • Full repository lint: yarn.cmd lint, no errors or warnings.
  • git diff --cached --check passed before committing.

@Paul-AUB
Paul-AUB requested a review from ClemRz October 5, 2026 16:33
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net

@ClemRz ClemRz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 510321e7

The last blocker is resolved. Approving.

Verified

  • Rebase is clean. git range-diff shows all 16 earlier PR commits are identical after the rebase onto develop. The only new work is 510321e7.
  • The new page test catches the original bug. I put back the pre-fix DocumentDetails/index.jsx (from d97adff5) and ran index.test.jsx. Two of its five tests fail: "offers unlink actions for massifs and entrances, but not caves" and "unlinks Vercors through its own mutation". Last round, the whole suite stayed green against that same file.
  • The test is well built. Using id 7 for the massif, the cave and the entrance means that calling the wrong endpoint fails the test instead of passing by accident. Checking otherMutation before and after confirming also pins down which endpoint gets called.
  • Both nits are addressed. The comment now cites both API controllers. The unsupported-type branch now throws. LinkedEntityCards.handleUnlink catches the error and keeps the dialog open, so the unlink no longer looks like it succeeded.
  • yarn test: 183 files, 1265 tests pass. yarn lint is clean.

Nit (non-blocking)

  • The test names its fixture document (L64), which hides jsdom's global document in this file. Nothing breaks today, because RTL uses its own reference. A name like documentData would avoid surprises if someone later adds a direct DOM call here.

@Paul-AUB

Paul-AUB commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Est-ce que les corrections effectuées et l'état actuel est acceptable pour toi @urien ? En sachant que l'amélioration de la partie validation sera traitée dans #1558

@urien urien left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est parfait,
@paul, merci pour ces belles avancées

Paul-AUB and others added 17 commits October 6, 2026 17:44
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #1156

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The document page already lets users associate entrances but had no way
to remove one once added. Wire the existing `useUnlinkDocumentToEntrance`
mutation through `LinkedEntityCards` (which already renders the unlink
button and confirmation dialog) and expose it on entrance entries only.

The backend restricts the unlink endpoint to moderators in
`entrance/unlink-document.js` (controller-level `hasGroup(MODERATOR)`
check on top of the `tokenAuth` policy), so the gating here is narrower
than the Associate side — the two must not be aligned on `isAuth`.

Add an opt-out `canUnlink` field on `LinkedEntityCards` entities so
massif and cave entries (whose unlink hooks are not wired yet) don't
pick up the button; existing callers default to the previous behaviour.
On a deleted Description / Comment / History row, the three action
buttons used icon + colour choices that no other deletion UI in the app
shares with it:

- "Permanently delete" rendered the `RemoveCircleRounded` glyph (red
  barred circle), while the entity-level `DeletedCard` and the shared
  `DeleteConfirmationDialog` use `DeleteForeverRounded`. Converge on the
  majority spelling.
- "Restore" rendered in primary, giving it the same visual weight as the
  History button next to it and leaving the destructive "Permanently
  delete" without a hierarchy to stand against. `DeletedCard` sets its
  restore button to secondary for the same reason; mirror that.

Icon-only density is preserved; only the shared tokens (icon + colour)
move.
The opt-out canUnlink default meant massif and cave cards picked up the
unlink button and reached the entrance mutation with a foreign id,
usually a silent no-op and occasionally unlinking an unrelated entrance
whose id happened to collide. Set canUnlink: false on massif and cave
entries and guard the page callback on entity.type === 'entrance'.
The dispatch now routes on entity.type: entrance entries hit
useUnlinkDocumentToEntrance, massif entries hit useUnlinkDocumentToMassif.
Cave stays opt-out until its hook exists.
@Paul-AUB
Paul-AUB force-pushed the feat/improvements-ux branch from 510321e to 1c3e9d6 Compare October 6, 2026 15:44
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1552.westeurope.azurestaticapps.net

@Paul-AUB
Paul-AUB merged commit c45df73 into develop Oct 6, 2026
15 checks passed
@Paul-AUB
Paul-AUB deleted the feat/improvements-ux branch October 6, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DOCUMENT] ajout des entrées

3 participants