diff --git a/frontend/src/components/Timeline.jsx b/frontend/src/components/Timeline.jsx index 2a2fd6e..1d072f8 100644 --- a/frontend/src/components/Timeline.jsx +++ b/frontend/src/components/Timeline.jsx @@ -153,7 +153,23 @@ export default function Timeline({ dragRef.current = null setDragPreview(null) if (drag && preview && preview.size > 0) { - onCommit(preview, drag.mode) + // Простой клик (pointerdown+pointerup без реального перетаскивания, либо + // движение мыши, округлившееся до 0 минут) не должен попадать в + // несохранённые изменения - beginDrag заранее кладёт в preview сам + // редактируемый блок с ЕГО ЖЕ текущими start/end (нужно для подсветки + // "тянущегося" блока с первого кадра), и если движения так и не было, + // preview долетает до сюда без единого реального изменения. Коммитим + // только те записи, где start/end реально отличаются от исходных. + const changed = new Map() + for (const [id, { start, end }] of preview) { + const original = itemsRef.current.find((it) => it.id === id) + if (!original || original.start !== start || original.end !== end) { + changed.set(id, { start, end }) + } + } + if (changed.size > 0) { + onCommit(changed, drag.mode) + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [onCommit]) diff --git a/frontend/src/scheduleGeometry.js b/frontend/src/scheduleGeometry.js index aa1e395..e42ccaf 100644 --- a/frontend/src/scheduleGeometry.js +++ b/frontend/src/scheduleGeometry.js @@ -133,12 +133,16 @@ export function computeCascade(items, id, mode, proposedStart, proposedEnd, { ca let start = proposedStart let end = proposedEnd - if (startChanged && prev) { - // Без каскада (или если предыдущий блок locked) предыдущий никто не - // подвинет/не растянет - значит, нельзя залезать поверх его занятого времени. - // С каскадом ограничение мягче: только не раньше НАЧАЛА предыдущего блока - - // его конец мы всё равно сейчас подтянем следующим шагом. - const prevIsAnchor = !cascadeEnabled || prev.locked + if (startChanged && prev && !prev.locked) { + // FIXED-блоки НЕ считаются "стеной" для редактируемого блока - с ними + // разрешено визуально пересекаться (раскладка по колонкам), поэтому + // ограничение по предыдущему блоку действует только когда сам предыдущий + // блок не locked. Без каскада предыдущий (не locked) блок никто не + // подвинет/не растянет - значит, нельзя залезать поверх его занятого + // времени. С каскадом ограничение мягче: только не раньше НАЧАЛА + // предыдущего блока - его конец мы всё равно сейчас подтянем следующим + // шагом. + const prevIsAnchor = !cascadeEnabled const floor = prevIsAnchor ? prev.end : prev.start if (start < floor) { const delta = floor - start @@ -164,8 +168,10 @@ export function computeCascade(items, id, mode, proposedStart, proposedEnd, { ca result.set(it.id, { start: it.start + delta, end: it.end + delta }) } } - } else if (next && end > next.start) { - // Без каскада не подвигаем следующий блок, но и не даём на него залезть. + } else if (next && !next.locked && end > next.start) { + // Без каскада не подвигаем следующий блок, но и не даём на него залезть - + // если только следующий блок сам не FIXED: с FIXED пересечение допустимо + // (раскладка по колонкам), он тут не "стена". end = next.start result.set(id, { start, end }) }