feat: v4.3.0 - cross-day drag (blocks can move between day rows)
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Some checks failed
Build & Push Docker / build (push) Has been cancelled
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
"""Application configuration."""
|
"""Application configuration."""
|
||||||
|
|
||||||
VERSION = "4.2.0"
|
VERSION = "4.3.0"
|
||||||
MAX_FILE_SIZE_MB = 10
|
MAX_FILE_SIZE_MB = 10
|
||||||
DEFAULT_COLOR = "#ffffff"
|
DEFAULT_COLOR = "#ffffff"
|
||||||
|
|||||||
@@ -765,6 +765,20 @@ body {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow blocks to visually leave their row during cross-day drag */
|
||||||
|
.day-rows.dragging .day-row,
|
||||||
|
.day-rows.dragging .day-timeline {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight the row being dragged over */
|
||||||
|
.day-timeline.drag-target {
|
||||||
|
background: #eff6ff;
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-label {
|
.day-label {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1 class="header-title">Scénář Creator</h1>
|
<h1 class="header-title">Scénář Creator</h1>
|
||||||
<span class="header-version">v4.2</span>
|
<span class="header-version">v4.3</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<label class="btn btn-secondary btn-sm" id="importJsonBtn">
|
<label class="btn btn-secondary btn-sm" id="importJsonBtn">
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<div class="tab-content hidden" id="tab-docs">
|
<div class="tab-content hidden" id="tab-docs">
|
||||||
<div class="docs-container">
|
<div class="docs-container">
|
||||||
<h2>Scénář Creator — Dokumentace</h2>
|
<h2>Scénář Creator — Dokumentace</h2>
|
||||||
<p class="docs-version">Verze 4.2.0 | <a href="/docs" target="_blank">Swagger API</a> | <a href="/api/sample">Vzorový JSON</a></p>
|
<p class="docs-version">Verze 4.3.0 | <a href="/docs" target="_blank">Swagger API</a> | <a href="/api/sample">Vzorový JSON</a></p>
|
||||||
|
|
||||||
<h3>Jak začít</h3>
|
<h3>Jak začít</h3>
|
||||||
<ol>
|
<ol>
|
||||||
@@ -104,7 +104,8 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><strong>Přidání:</strong> Klikněte na „+ Přidat blok" nebo klikněte na prázdné místo v řádku dne.</li>
|
<li><strong>Přidání:</strong> Klikněte na „+ Přidat blok" nebo klikněte na prázdné místo v řádku dne.</li>
|
||||||
<li><strong>Přidání do všech dnů:</strong> V modalu nového bloku zaškrtněte „Přidat do každého dne kurzu" — vytvoří identický blok pro každý den akce (sdílená série).</li>
|
<li><strong>Přidání do všech dnů:</strong> V modalu nového bloku zaškrtněte „Přidat do každého dne kurzu" — vytvoří identický blok pro každý den akce (sdílená série).</li>
|
||||||
<li><strong>Přesun:</strong> Chytněte blok a táhněte doleva/doprava. Snap na 15 minut.</li>
|
<li><strong>Přesun v rámci dne:</strong> Chytněte blok a táhněte doleva/doprava. Snap na 15 minut.</li>
|
||||||
|
<li><strong>Přesun mezi dny:</strong> Táhněte blok nahoru/dolů — zvýrazní se cílový den (modrý rámeček). Pusťte na novém dni.</li>
|
||||||
<li><strong>Změna délky:</strong> Chytněte pravý okraj bloku a táhněte.</li>
|
<li><strong>Změna délky:</strong> Chytněte pravý okraj bloku a táhněte.</li>
|
||||||
<li><strong>Úprava:</strong> Klikněte na blok — otevře se formulář s editací jednoho bloku.</li>
|
<li><strong>Úprava:</strong> Klikněte na blok — otevře se formulář s editací jednoho bloku.</li>
|
||||||
<li><strong>Smazání jednoho bloku:</strong> V editačním formuláři klikněte na „Smazat jen tento".</li>
|
<li><strong>Smazání jednoho bloku:</strong> V editačním formuláři klikněte na „Smazat jen tento".</li>
|
||||||
|
|||||||
@@ -259,22 +259,21 @@ const Canvas = {
|
|||||||
App.openBlockModal(block.id);
|
App.openBlockModal(block.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// interact.js drag (horizontal only)
|
// interact.js drag (X = time, Y = cross-day)
|
||||||
if (window.interact) {
|
if (window.interact) {
|
||||||
interact(el)
|
interact(el)
|
||||||
.draggable({
|
.draggable({
|
||||||
axis: 'x',
|
|
||||||
listeners: {
|
listeners: {
|
||||||
|
start: (event) => this._onDragStart(event, block),
|
||||||
move: (event) => this._onDragMove(event, block),
|
move: (event) => this._onDragMove(event, block),
|
||||||
end: (event) => this._onDragEnd(event, block),
|
end: (event) => this._onDragEnd(event, block),
|
||||||
},
|
},
|
||||||
modifiers: [
|
modifiers: [
|
||||||
interact.modifiers.snap({
|
interact.modifiers.snap({
|
||||||
targets: [interact.snappers.grid({ x: this._minutesPx(this.GRID_MINUTES), y: 1000 })],
|
targets: [interact.snappers.grid({ x: this._minutesPx(this.GRID_MINUTES), y: 10000 })],
|
||||||
range: Infinity,
|
range: Infinity,
|
||||||
relativePoints: [{ x: 0, y: 0 }]
|
relativePoints: [{ x: 0, y: 0 }],
|
||||||
}),
|
}),
|
||||||
interact.modifiers.restrict({ restriction: 'parent' })
|
|
||||||
],
|
],
|
||||||
inertia: false,
|
inertia: false,
|
||||||
})
|
})
|
||||||
@@ -300,29 +299,72 @@ const Canvas = {
|
|||||||
return minutes * this.pxPerMinute;
|
return minutes * this.pxPerMinute;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onDragStart(event, block) {
|
||||||
|
const target = event.target;
|
||||||
|
target.dataset.dy = '0';
|
||||||
|
target.style.zIndex = '500';
|
||||||
|
// Enable cross-row overflow while dragging
|
||||||
|
const dayRows = document.getElementById('dayRows');
|
||||||
|
if (dayRows) dayRows.classList.add('dragging');
|
||||||
|
},
|
||||||
|
|
||||||
_onDragMove(event, block) {
|
_onDragMove(event, block) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
const x = (parseFloat(target.style.left) || 0);
|
|
||||||
// Convert delta px to minutes
|
// ── X axis: update time ──────────────────────────────────────
|
||||||
const deltaPx = event.dx;
|
const deltaMin = event.dx / this.pxPerMinute;
|
||||||
const deltaMin = deltaPx / this.pxPerMinute;
|
|
||||||
const newStartMin = this.snapMinutes(this.parseTime(block.start) + deltaMin);
|
|
||||||
const duration = this.parseTime(block.end) - this.parseTime(block.start);
|
const duration = this.parseTime(block.end) - this.parseTime(block.start);
|
||||||
|
const newStartMin = this.snapMinutes(this.parseTime(block.start) + deltaMin);
|
||||||
const clampedStart = Math.max(this._startMin, Math.min(this._endMin - duration, newStartMin));
|
const clampedStart = Math.max(this._startMin, Math.min(this._endMin - duration, newStartMin));
|
||||||
const newEnd = clampedStart + duration;
|
|
||||||
|
|
||||||
block.start = this.formatTime(clampedStart);
|
block.start = this.formatTime(clampedStart);
|
||||||
block.end = this.formatTime(newEnd);
|
block.end = this.formatTime(clampedStart + duration);
|
||||||
|
|
||||||
const totalRange = this._endMin - this._startMin;
|
const totalRange = this._endMin - this._startMin;
|
||||||
const leftPct = (clampedStart - this._startMin) / totalRange * 100;
|
target.style.left = ((clampedStart - this._startMin) / totalRange * 100) + '%';
|
||||||
target.style.left = leftPct + '%';
|
|
||||||
|
// ── Y axis: visual shift + row highlight ─────────────────────
|
||||||
|
const dy = (parseFloat(target.dataset.dy) || 0) + event.dy;
|
||||||
|
target.dataset.dy = dy;
|
||||||
|
target.style.transform = `translateY(${dy}px)`;
|
||||||
|
|
||||||
|
this._highlightTargetRow(target);
|
||||||
},
|
},
|
||||||
|
|
||||||
_onDragEnd(event, block) {
|
_onDragEnd(event, block) {
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
// Detect which day-timeline the block's vertical center is over
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
for (const row of document.querySelectorAll('.day-timeline')) {
|
||||||
|
const rr = row.getBoundingClientRect();
|
||||||
|
if (centerY >= rr.top && centerY <= rr.bottom) {
|
||||||
|
block.date = row.dataset.date;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.querySelectorAll('.day-timeline.drag-target')
|
||||||
|
.forEach(r => r.classList.remove('drag-target'));
|
||||||
|
const dayRows = document.getElementById('dayRows');
|
||||||
|
if (dayRows) dayRows.classList.remove('dragging');
|
||||||
|
target.style.transform = '';
|
||||||
|
target.style.zIndex = '';
|
||||||
|
delete target.dataset.dy;
|
||||||
|
|
||||||
App.renderCanvas();
|
App.renderCanvas();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_highlightTargetRow(el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
document.querySelectorAll('.day-timeline').forEach(row => {
|
||||||
|
const rr = row.getBoundingClientRect();
|
||||||
|
const isTarget = centerY >= rr.top && centerY <= rr.bottom;
|
||||||
|
row.classList.toggle('drag-target', isTarget);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_onResizeMove(event, block) {
|
_onResizeMove(event, block) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
const newWidthPx = event.rect.width;
|
const newWidthPx = event.rect.width;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import pytest
|
|||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.main import app
|
from app.main import app
|
||||||
|
from app.config import VERSION
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -48,7 +49,7 @@ def test_health(client):
|
|||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
data = r.json()
|
data = r.json()
|
||||||
assert data["status"] == "ok"
|
assert data["status"] == "ok"
|
||||||
assert data["version"] == "4.2.0"
|
assert data["version"] == VERSION
|
||||||
|
|
||||||
|
|
||||||
def test_root_returns_html(client):
|
def test_root_returns_html(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user