Fix: Task detalj kao overlay modal umesto side panela
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b3645beea0
commit
5bf7375b50
@ -1563,7 +1563,7 @@ func TestDashboard_HasPrijavaNav(t *testing.T) {
|
|||||||
|
|
||||||
// --- T21: UI tests ---
|
// --- T21: UI tests ---
|
||||||
|
|
||||||
func TestDashboard_DetailOpenClassInJS(t *testing.T) {
|
func TestDashboard_EscapeClosesOverlay(t *testing.T) {
|
||||||
srv := setupTestServer(t)
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
@ -1571,13 +1571,12 @@ func TestDashboard_DetailOpenClassInJS(t *testing.T) {
|
|||||||
srv.Router.ServeHTTP(w, req)
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
body := w.Body.String()
|
body := w.Body.String()
|
||||||
// JS should add detail-open class when detail panel opens
|
if !containsStr(body, "Escape") {
|
||||||
if !containsStr(body, "detail-open") {
|
t.Error("expected Escape key handler for overlay")
|
||||||
t.Error("expected 'detail-open' body class in dashboard JS")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDashboard_ClickOutsideClosesDetail(t *testing.T) {
|
func TestDashboard_BackdropClickClosesOverlay(t *testing.T) {
|
||||||
srv := setupTestServer(t)
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
@ -1585,9 +1584,21 @@ func TestDashboard_ClickOutsideClosesDetail(t *testing.T) {
|
|||||||
srv.Router.ServeHTTP(w, req)
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
body := w.Body.String()
|
body := w.Body.String()
|
||||||
// Should have click-outside handler for detail panel
|
if !containsStr(body, "e.target === this") {
|
||||||
if !containsStr(body, "closest('.task-card')") {
|
t.Error("expected backdrop click handler for overlay")
|
||||||
t.Error("expected click-outside handler for detail panel")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskDetail_HasInnerWrapper(t *testing.T) {
|
||||||
|
srv := setupTestServer(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/task/T01", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.Router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !containsStr(body, "detail-inner") {
|
||||||
|
t.Error("expected detail-inner wrapper in task detail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -86,40 +86,45 @@ body {
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Task detail panel — 50% of screen */
|
/* Task detail overlay (modal) */
|
||||||
#task-detail {
|
#task-detail {
|
||||||
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -50%;
|
left: 0;
|
||||||
width: 50%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: #16213e;
|
z-index: 50;
|
||||||
border-left: 2px solid #0f3460;
|
background: rgba(0, 0, 0, 0.6);
|
||||||
padding: 20px;
|
justify-content: center;
|
||||||
overflow-y: auto;
|
align-items: center;
|
||||||
z-index: 10;
|
|
||||||
transition: right 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#task-detail.active {
|
#task-detail.active {
|
||||||
right: 0;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Board compresses when detail is open */
|
.detail-inner {
|
||||||
.board {
|
background: #16213e;
|
||||||
transition: margin-right 0.3s ease;
|
border: 1px solid #0f3460;
|
||||||
}
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
body.detail-open .board {
|
width: 700px;
|
||||||
margin-right: 50%;
|
max-width: 90%;
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-close {
|
.detail-close {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
float: right;
|
position: absolute;
|
||||||
font-size: 1.2em;
|
top: 12px;
|
||||||
|
right: 16px;
|
||||||
|
font-size: 1.4em;
|
||||||
color: #888;
|
color: #888;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-close:hover { color: #e94560; }
|
.detail-close:hover { color: #e94560; }
|
||||||
@ -822,8 +827,6 @@ textarea.form-input {
|
|||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.board { grid-template-columns: repeat(2, 1fr); }
|
.board { grid-template-columns: repeat(2, 1fr); }
|
||||||
#task-detail { width: 100%; right: -100%; }
|
|
||||||
body.detail-open .board { margin-right: 0; }
|
|
||||||
.docs-layout { grid-template-columns: 1fr; }
|
.docs-layout { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,6 @@
|
|||||||
document.body.addEventListener('htmx:afterSwap', function(e) {
|
document.body.addEventListener('htmx:afterSwap', function(e) {
|
||||||
if (e.detail.target.id === 'task-detail') {
|
if (e.detail.target.id === 'task-detail') {
|
||||||
e.detail.target.classList.add('active');
|
e.detail.target.classList.add('active');
|
||||||
document.body.classList.add('detail-open');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,9 +44,25 @@ function closeDetail() {
|
|||||||
var el = document.getElementById('task-detail');
|
var el = document.getElementById('task-detail');
|
||||||
el.classList.remove('active');
|
el.classList.remove('active');
|
||||||
el.innerHTML = '';
|
el.innerHTML = '';
|
||||||
document.body.classList.remove('detail-open');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close overlay on Escape
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
var detail = document.getElementById('task-detail');
|
||||||
|
if (detail && detail.classList.contains('active')) {
|
||||||
|
closeDetail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close overlay on backdrop click
|
||||||
|
document.getElementById('task-detail').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeDetail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function showToast(msg, type) {
|
function showToast(msg, type) {
|
||||||
var toast = document.getElementById('toast');
|
var toast = document.getElementById('toast');
|
||||||
toast.textContent = msg;
|
toast.textContent = msg;
|
||||||
@ -85,12 +100,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (wrapper && !wrapper.contains(e.target)) {
|
if (wrapper && !wrapper.contains(e.target)) {
|
||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close detail panel on click outside
|
|
||||||
var detail = document.getElementById('task-detail');
|
|
||||||
if (detail && detail.classList.contains('active') && !detail.contains(e.target) && !e.target.closest('.task-card')) {
|
|
||||||
closeDetail();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
{{define "task-detail"}}
|
{{define "task-detail"}}
|
||||||
<span class="detail-close" onclick="closeDetail()">✕</span>
|
<div class="detail-inner">
|
||||||
<h2>{{.Task.ID}}: {{.Task.Title}}</h2>
|
<span class="detail-close" onclick="closeDetail()">✕</span>
|
||||||
<div class="detail-meta">
|
<h2>{{.Task.ID}}: {{.Task.Title}}</h2>
|
||||||
|
<div class="detail-meta">
|
||||||
<p><strong>Agent:</strong> {{.Task.Agent}} · <strong>Model:</strong> {{.Task.Model}} · <strong>Status:</strong> {{.Task.Status}}</p>
|
<p><strong>Agent:</strong> {{.Task.Agent}} · <strong>Model:</strong> {{.Task.Model}} · <strong>Status:</strong> {{.Task.Status}}</p>
|
||||||
{{if .Task.DependsOn}}
|
{{if .Task.DependsOn}}
|
||||||
<p><strong>Zavisi od:</strong> {{joinDeps .Task.DependsOn}}</p>
|
<p><strong>Zavisi od:</strong> {{joinDeps .Task.DependsOn}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .HasReport}}
|
{{if .HasReport}}
|
||||||
<div class="detail-actions">
|
<div class="detail-actions">
|
||||||
<a href="/report/{{.Task.ID}}" class="btn" target="_blank">Izvestaj</a>
|
<a href="/report/{{.Task.ID}}" class="btn" target="_blank">Izvestaj</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="detail-actions">
|
<div class="detail-actions">
|
||||||
{{if eq .Task.Status "backlog"}}
|
{{if eq .Task.Status "backlog"}}
|
||||||
<button class="btn btn-success" hx-post="/task/{{.Task.ID}}/move?to=ready" hx-target="#board" hx-swap="outerHTML" onclick="closeDetail()">Odobri</button>
|
<button class="btn btn-success" hx-post="/task/{{.Task.ID}}/move?to=ready" hx-target="#board" hx-swap="outerHTML" onclick="closeDetail()">Odobri</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -23,6 +24,7 @@
|
|||||||
<button class="btn btn-success" hx-post="/task/{{.Task.ID}}/move?to=done" hx-target="#board" hx-swap="outerHTML" onclick="closeDetail()">Odobri</button>
|
<button class="btn btn-success" hx-post="/task/{{.Task.ID}}/move?to=done" hx-target="#board" hx-swap="outerHTML" onclick="closeDetail()">Odobri</button>
|
||||||
<button class="btn btn-move" hx-post="/task/{{.Task.ID}}/move?to=ready" hx-target="#board" hx-swap="outerHTML" onclick="closeDetail()">Vrati</button>
|
<button class="btn btn-move" hx-post="/task/{{.Task.ID}}/move?to=ready" hx-target="#board" hx-swap="outerHTML" onclick="closeDetail()">Vrati</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="detail-content">{{.Content}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-content">{{.Content}}</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user