Слияние WT-3: oob-обновление панели действий ревью
This commit is contained in:
@@ -10,6 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.vakhrushev.me/av/jellybit/internal/layout"
|
||||||
|
"git.vakhrushev.me/av/jellybit/internal/recognize"
|
||||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||||
"git.vakhrushev.me/av/jellybit/internal/worker"
|
"git.vakhrushev.me/av/jellybit/internal/worker"
|
||||||
)
|
)
|
||||||
@@ -272,6 +274,70 @@ func TestActionErrorDownloadSurface(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reviewDataWithSource собирает ReviewData ревью с одним нейронка-источником;
|
||||||
|
// withLinks управляет наличием превью раскладки (HasLinks).
|
||||||
|
func reviewDataWithSource(withLinks bool) *worker.ReviewData {
|
||||||
|
dl := dlState(store.StateReview)
|
||||||
|
plan := recognize.Plan{Type: "movie", Title: "Fargo", Year: 1996,
|
||||||
|
Files: []recognize.PlanFile{{Src: "fargo.mkv"}}}
|
||||||
|
rd := &worker.ReviewData{
|
||||||
|
Download: dl,
|
||||||
|
Recognition: &store.Recognition{},
|
||||||
|
Plan: plan,
|
||||||
|
Sources: []worker.SourceOption{
|
||||||
|
{Kind: worker.SourceNeural, Provider: "none", Title: "Fargo", Year: 1996, Active: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if withLinks {
|
||||||
|
rd.Preview = []layout.Link{{Src: "fargo.mkv", Dst: "/movies/Fargo (1996)/fargo.mkv"}}
|
||||||
|
}
|
||||||
|
return rd
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSourceSwapUpdatesActionBarOOB: своп выбора источника отдаёт свежий
|
||||||
|
// #source-block и oob-обновление панели действий (#action-bar с hx-swap-oob),
|
||||||
|
// чтобы кнопка «Применить» синхронно отражала актуальный HasLinks.
|
||||||
|
func TestSourceSwapUpdatesActionBarOOB(t *testing.T) {
|
||||||
|
t.Run("с превью → есть кнопка Применить", func(t *testing.T) {
|
||||||
|
rd := reviewDataWithSource(true)
|
||||||
|
rv := actionReviewer{stubReviewer: stubReviewer{data: rd}}
|
||||||
|
h := testRouterAction(t, stubReader{one: &rd.Download}, rv, stubCommander{}, stubLive{})
|
||||||
|
|
||||||
|
rr := post(t, h, "/ui/downloads/"+testULID+"/nobase", nil, true)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("nobase (htmx) = %d, want 200", rr.Code)
|
||||||
|
}
|
||||||
|
body := rr.Body.String()
|
||||||
|
if !strings.Contains(body, `id="source-block"`) {
|
||||||
|
t.Errorf("ответ без свежего #source-block: %s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `id="action-bar"`) || !strings.Contains(body, `hx-swap-oob="true"`) {
|
||||||
|
t.Errorf("ответ без oob-панели действий: %s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "/apply") {
|
||||||
|
t.Errorf("HasLinks=true, но в панели нет кнопки «Применить»: %s", body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("без превью → кнопки Применить нет", func(t *testing.T) {
|
||||||
|
rd := reviewDataWithSource(false)
|
||||||
|
rv := actionReviewer{stubReviewer: stubReviewer{data: rd}}
|
||||||
|
h := testRouterAction(t, stubReader{one: &rd.Download}, rv, stubCommander{}, stubLive{})
|
||||||
|
|
||||||
|
rr := post(t, h, "/ui/downloads/"+testULID+"/nobase", nil, true)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("nobase (htmx) = %d, want 200", rr.Code)
|
||||||
|
}
|
||||||
|
body := rr.Body.String()
|
||||||
|
if !strings.Contains(body, `id="action-bar"`) || !strings.Contains(body, `hx-swap-oob="true"`) {
|
||||||
|
t.Errorf("ответ без oob-панели действий: %s", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "/apply") {
|
||||||
|
t.Errorf("HasLinks=false, но в панели осталась кнопка «Применить»: %s", body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestRetryListShowsProgress: retry из списка → карточка downloading с
|
// TestRetryListShowsProgress: retry из списка → карточка downloading с
|
||||||
// прогресс-поллером.
|
// прогресс-поллером.
|
||||||
func TestRetryListShowsProgress(t *testing.T) {
|
func TestRetryListShowsProgress(t *testing.T) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type reviewView struct {
|
|||||||
NoBase bool // выбрано «без базы»
|
NoBase bool // выбрано «без базы»
|
||||||
Sources []sourceView // единый список источников совпадения
|
Sources []sourceView // единый список источников совпадения
|
||||||
BlockError string // ошибка выбора внутри блока (htmx); не путать с Error (?err=)
|
BlockError string // ошибка выбора внутри блока (htmx); не путать с Error (?err=)
|
||||||
|
ActionBarOOB bool // рендерить панель действий как oob-фрагмент (своп источника)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceView — строка единого списка источников на экране ревью: нейронка или
|
// sourceView — строка единого списка источников на экране ревью: нейронка или
|
||||||
@@ -455,9 +456,10 @@ func isHTMX(r *http.Request) bool {
|
|||||||
|
|
||||||
// reviewBlockAction — помощник для действий выбора источника: выполнить
|
// reviewBlockAction — помощник для действий выбора источника: выполнить
|
||||||
// операцию и вернуть свежий блок источника. На htmx-запрос перечитывает
|
// операцию и вернуть свежий блок источника. На htmx-запрос перечитывает
|
||||||
// состояние и рендерит партиал `review_source_block` (ошибку кладёт в
|
// состояние и рендерит `review_source_swap` — партиал `review_source_block`
|
||||||
// BlockError, активный источник не меняется); без htmx деградирует до
|
// плюс oob-обновление панели действий `review_action_bar` (кнопка «Применить»
|
||||||
// PRG-редиректа, как reviewAction.
|
// зависит от HasLinks, а лежит вне блока), ошибку кладёт в BlockError, активный
|
||||||
|
// источник не меняется; без htmx деградирует до PRG-редиректа, как reviewAction.
|
||||||
func (s *server) reviewBlockAction(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) error) {
|
func (s *server) reviewBlockAction(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) error) {
|
||||||
id, err := pathID(r)
|
id, err := pathID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -487,7 +489,11 @@ func (s *server) reviewBlockAction(w http.ResponseWriter, r *http.Request, fn fu
|
|||||||
if actionErr != nil {
|
if actionErr != nil {
|
||||||
view.BlockError = userErr(r, actionErr, id)
|
view.BlockError = userErr(r, actionErr, id)
|
||||||
}
|
}
|
||||||
s.render(w, "review_source_block", view)
|
// Панель действий («Применить» завязана на HasLinks) лежит вне #source-block,
|
||||||
|
// поэтому обновляем её тем же ответом через hx-swap-oob — иначе кнопка
|
||||||
|
// рассинхронилась бы с превью до полной перезагрузки.
|
||||||
|
view.ActionBarOOB = true
|
||||||
|
s.render(w, "review_source_swap", view)
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchURL выбирает ссылку на подтверждённую запись метабазы. Приоритет — URL
|
// matchURL выбирает ссылку на подтверждённую запись метабазы. Приоритет — URL
|
||||||
|
|||||||
@@ -61,12 +61,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Панель действий (выходы из ревью — обычные формы, полная навигация) -->
|
<!-- Панель действий (выходы из ревью — обычные формы, полная навигация) -->
|
||||||
<div class="action-bar">
|
{{template "review_action_bar" .}}
|
||||||
<span class="grow">{{if .HasLinks}}Превью готово — можно применять.{{else}}Подтверди источник, чтобы получить превью раскладки.{{end}}</span>
|
|
||||||
<form method="post" action="/ui/downloads/{{.ID}}/defer"><button class="btn btn-lg" type="submit">🕗 Позже</button></form>
|
|
||||||
<form method="post" action="/ui/downloads/{{.ID}}/cancel"><button class="btn btn-lg btn-danger" type="submit">❌ Отклонить</button></form>
|
|
||||||
{{if .HasLinks}}<form method="post" action="/ui/downloads/{{.ID}}/apply"><button class="btn btn-lg btn-primary" type="submit">✅ Применить</button></form>{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Панель действий ревью. Живёт вне #source-block, но зависит от HasLinks
|
||||||
|
(есть ли превью раскладки), поэтому при свопе источника обновляется oob
|
||||||
|
из того же ответа (ActionBarOOB → hx-swap-oob). Выходы из ревью — обычные
|
||||||
|
POST-формы без hx-* (полная навигация, см. web-ui.md). */}}
|
||||||
|
{{define "review_action_bar"}}
|
||||||
|
<div class="action-bar" id="action-bar"{{if .ActionBarOOB}} hx-swap-oob="true"{{end}}>
|
||||||
|
<span class="grow">{{if .HasLinks}}Превью готово — можно применять.{{else}}Подтверди источник, чтобы получить превью раскладки.{{end}}</span>
|
||||||
|
<form method="post" action="/ui/downloads/{{.ID}}/defer"><button class="btn btn-lg" type="submit">🕗 Позже</button></form>
|
||||||
|
<form method="post" action="/ui/downloads/{{.ID}}/cancel"><button class="btn btn-lg btn-danger" type="submit">❌ Отклонить</button></form>
|
||||||
|
{{if .HasLinks}}<form method="post" action="/ui/downloads/{{.ID}}/apply"><button class="btn btn-lg btn-primary" type="submit">✅ Применить</button></form>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|||||||
@@ -72,3 +72,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Ответ на своп выбора источника: свежий #source-block + oob-обновление
|
||||||
|
панели действий (#action-bar), чтобы кнопка «Применить» синхронно отражала
|
||||||
|
актуальный HasLinks. Рендерится с ActionBarOOB=true (reviewBlockAction). */}}
|
||||||
|
{{define "review_source_swap"}}{{template "review_source_block" .}}
|
||||||
|
{{template "review_action_bar" .}}{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user