mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-14 00:22:03 +00:00
fix(arbre): les positions du brouillon restent toutes à l'écran
La liste des positions manuelles était mise en cache sur un hasDraft et une fonction qui ne changent plus après le premier glisser : une seule personne (la première) restait placée. Le brouillon expose maintenant un draftRevision incrémenté à chaque écriture, et le calcul des positions passe par buildManualPositionOverrides (testé). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d17cee20df
commit
de897070db
4 files changed
+88
-8
No files matched your search
@@ -19,6 +19,7 @@ import EditConfirmModal from './components/EditConfirmModal'
|
||||
import DraftRestoreBanner from './components/DraftRestoreBanner'
|
||||
import AddPersonPanel from './components/AddPersonPanel'
|
||||
import ContributionSessionModal from './components/ContributionSessionModal'
|
||||
import { buildManualPositionOverrides } from './components/galaxy/layoutPositions'
|
||||
import { ContextualNavbar } from './components/navbar'
|
||||
import ZoomControl from './components/ZoomControl'
|
||||
import { ExpandableSearch } from './components/search'
|
||||
@@ -627,16 +628,11 @@ function App() {
|
||||
treeId: tree.treeContext.treeId,
|
||||
canEdit: canEditInCurrentMode,
|
||||
})
|
||||
// draftRevision : la valeur lue par restoreDraft (une ref) ne change pas, il faut un signal qui bouge à chaque écriture
|
||||
const manualPositionOverrides = useMemo(() => {
|
||||
if (!editMode.hasDraft) return {}
|
||||
const positions = {}
|
||||
for (const [personId, changes] of Object.entries(editMode.restoreDraft().modifiedPersons || {})) {
|
||||
if (Number.isFinite(changes?.layoutX) && Number.isFinite(changes?.layoutY)) {
|
||||
positions[String(personId)] = { layoutX: changes.layoutX, layoutY: changes.layoutY }
|
||||
}
|
||||
}
|
||||
return positions
|
||||
}, [editMode.hasDraft, editMode.restoreDraft])
|
||||
return buildManualPositionOverrides(editMode.restoreDraft().modifiedPersons)
|
||||
}, [editMode.hasDraft, editMode.draftRevision, editMode.restoreDraft])
|
||||
const handlePersonPositionChange = useCallback((personId, position) => {
|
||||
const draft = editMode.restoreDraft()
|
||||
editMode.updateDraft({
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '../../i18n/index.js'
|
||||
|
||||
function readLayoutPosition(person) {
|
||||
if (!Number.isFinite(person?.layoutX) || !Number.isFinite(person?.layoutY)) return null
|
||||
return { x: person.layoutX, y: person.layoutY }
|
||||
@@ -41,6 +43,33 @@ export function applyManualPersonPositions(
|
||||
}
|
||||
}
|
||||
|
||||
// Positions manuelles en attente dans le brouillon, par id de personne
|
||||
export function buildManualPositionOverrides(modifiedPersons = {}) {
|
||||
const positions = {}
|
||||
for (const [personId, changes] of Object.entries(modifiedPersons || {})) {
|
||||
if (Number.isFinite(changes?.layoutX) && Number.isFinite(changes?.layoutY)) {
|
||||
positions[String(personId)] = { layoutX: changes.layoutX, layoutY: changes.layoutY }
|
||||
}
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
// Ligne « Position dans l'arbre » d'une contribution en relecture. Jamais de coordonnées brutes.
|
||||
export function describePositionChange(before, after) {
|
||||
if (!after || !Object.prototype.hasOwnProperty.call(after, 'layoutX')) return null
|
||||
const stateLabel = (value) => (value === null || value === undefined ? t('Replacée automatiquement') : t('Déplacée'))
|
||||
const newLabel = stateLabel(after.layoutX)
|
||||
const hasBefore = Boolean(before) && Object.prototype.hasOwnProperty.call(before, 'layoutX')
|
||||
const oldLabel = hasBefore ? (before.layoutX === null || before.layoutX === undefined ? t('Automatique') : t('Déplacée')) : undefined
|
||||
return {
|
||||
key: 'layout',
|
||||
label: t("Position dans l'arbre"),
|
||||
// Déjà déplacée avant, déplacée encore : le libellé ne change pas, on n'affiche qu'une valeur
|
||||
oldVal: oldLabel === newLabel ? undefined : oldLabel,
|
||||
newVal: newLabel,
|
||||
}
|
||||
}
|
||||
|
||||
export function positionUnionJunctions(coupleBarMeta, positionById) {
|
||||
for (const { elkId, p1Key, p2Key } of coupleBarMeta) {
|
||||
const partner1 = positionById.get(p1Key)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
applyManualPersonPositions,
|
||||
buildManualPositionOverrides,
|
||||
captureAutomaticPersonPositions,
|
||||
describePositionChange,
|
||||
positionUnionJunctions,
|
||||
} from './layoutPositions'
|
||||
|
||||
@@ -85,3 +87,51 @@ describe('dynamic union junction positions', () => {
|
||||
expect(positions.get('u-1')).toBe(originalJunction)
|
||||
})
|
||||
})
|
||||
|
||||
describe('positions manuelles du brouillon', () => {
|
||||
it('reprend toutes les personnes déplacées dans le brouillon, pas seulement la dernière', () => {
|
||||
const draft = {
|
||||
'1': { layoutX: 10, layoutY: 20 },
|
||||
'2': { layoutX: 30, layoutY: 40, firstName: 'Hugo' },
|
||||
}
|
||||
expect(buildManualPositionOverrides(draft)).toEqual({
|
||||
'1': { layoutX: 10, layoutY: 20 },
|
||||
'2': { layoutX: 30, layoutY: 40 },
|
||||
})
|
||||
})
|
||||
|
||||
it('ignore les personnes sans position complète', () => {
|
||||
expect(buildManualPositionOverrides({ 3: { layoutX: 5 }, 4: { firstName: 'Anne' } })).toEqual({})
|
||||
expect(buildManualPositionOverrides(undefined)).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("ligne « Position dans l'arbre » en relecture", () => {
|
||||
it('dit déplacée ou replacée automatiquement, sans coordonnée brute', () => {
|
||||
expect(describePositionChange(null, { layoutX: 320.5, layoutY: 110 })).toEqual({
|
||||
key: 'layout',
|
||||
label: "Position dans l'arbre",
|
||||
oldVal: undefined,
|
||||
newVal: 'Déplacée',
|
||||
})
|
||||
expect(describePositionChange({ layoutX: 320, layoutY: 110 }, { layoutX: null, layoutY: null })).toMatchObject({
|
||||
oldVal: 'Déplacée',
|
||||
newVal: 'Replacée automatiquement',
|
||||
})
|
||||
})
|
||||
|
||||
it('montre le passage automatique vers déplacée, et rien si la ligne ne bouge pas de libellé', () => {
|
||||
expect(describePositionChange({ layoutX: null, layoutY: null }, { layoutX: 1, layoutY: 2 })).toMatchObject({
|
||||
oldVal: 'Automatique',
|
||||
newVal: 'Déplacée',
|
||||
})
|
||||
expect(describePositionChange({ layoutX: 1, layoutY: 2 }, { layoutX: 3, layoutY: 4 })).toMatchObject({
|
||||
oldVal: undefined,
|
||||
newVal: 'Déplacée',
|
||||
})
|
||||
})
|
||||
|
||||
it('ne concerne pas une contribution qui ne touche pas la position', () => {
|
||||
expect(describePositionChange({ firstName: 'Anne' }, { firstName: 'Ana' })).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -21,6 +21,8 @@ export function useEditMode({ treeId, canEdit }) {
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
const [hasDraft, setHasDraft] = useState(false)
|
||||
const [draftTimestamp, setDraftTimestamp] = useState(null)
|
||||
// Change à chaque écriture du brouillon : les valeurs dérivées (positions manuelles) s'y recalculent
|
||||
const [draftRevision, setDraftRevision] = useState(0)
|
||||
const autoSaveTimerRef = useRef(null)
|
||||
const draftDataRef = useRef({})
|
||||
|
||||
@@ -151,6 +153,7 @@ export function useEditMode({ treeId, canEdit }) {
|
||||
}
|
||||
// Considérer qu'il y a désormais un draft actif (permet le bouton "Mettre à jour l'arbre")
|
||||
setHasDraft(true)
|
||||
setDraftRevision((current) => current + 1)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
@@ -164,6 +167,7 @@ export function useEditMode({ treeId, canEdit }) {
|
||||
draftDataRef.current = {}
|
||||
setHasDraft(false)
|
||||
setDraftTimestamp(null)
|
||||
setDraftRevision((current) => current + 1)
|
||||
} catch (err) {
|
||||
console.error('Failed to clear draft:', err)
|
||||
}
|
||||
@@ -181,6 +185,7 @@ export function useEditMode({ treeId, canEdit }) {
|
||||
isActive,
|
||||
hasDraft,
|
||||
draftTimestamp,
|
||||
draftRevision,
|
||||
|
||||
// Actions
|
||||
activate,
|
||||
|
||||
Reference in new issue
Block a user