diff --git a/internal/app/app.go b/internal/app/app.go index 9196f16..bec6900 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -77,26 +77,27 @@ type form struct { } type model struct { - client *semble.Client - cache *cache.Cache - screen screen - mode mode - loading bool - spinner spinner.Model - list list.Model - help help.Model - profile viewport.Model - status string - err error - user *semble.User - form form - pages [screenCount]int - pageInfo [screenCount]semble.Pagination - width int - height int - darkBg bool - bg color.Color - statusID int + client *semble.Client + cache *cache.Cache + screen screen + mode mode + loading bool + formSaving bool + spinner spinner.Model + list list.Model + help help.Model + profile viewport.Model + status string + err error + user *semble.User + form form + pages [screenCount]int + pageInfo [screenCount]semble.Pagination + width int + height int + darkBg bool + bg color.Color + statusID int } type errMsg error @@ -172,7 +173,7 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd - if m.loading { + if m.loading || m.formSaving { var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) cmds = append(cmds, cmd) @@ -269,6 +270,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case errMsg: m.loading = false + m.formSaving = false return m, m.setError(msg) case profileMsg: m.loading = false @@ -335,6 +337,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case createdMsg: m.loading = false + m.formSaving = false m.mode = modeList cmd := m.setStatus(string(msg)) return m, tea.Batch(m.load(), cmd) @@ -403,6 +406,8 @@ func (m model) footerView() string { help := m.helpText() if m.loading { help = m.spinner.View() + " loading... " + help + } else if m.formSaving { + help = m.spinner.View() + " saving... " + help } right := "" if m.err != nil { @@ -447,9 +452,13 @@ func (m model) compactHelpText() string { func (m model) keyBindings(compact bool) []key.Binding { if m.mode != modeList { + saveHelp := "save" + if m.formSaving { + saveHelp = "saving" + } bindings := []key.Binding{ key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "field")), - key.NewBinding(key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "save")), + key.NewBinding(key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", saveHelp)), key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "cancel")), } if m.form.focus == m.selectorFocusIndex() { diff --git a/internal/app/forms.go b/internal/app/forms.go index c588dab..218d211 100644 --- a/internal/app/forms.go +++ b/internal/app/forms.go @@ -101,10 +101,13 @@ func (m model) updateForm(msg tea.Msg) (tea.Model, tea.Cmd) { switch keyMsg.String() { case "esc": + if m.formSaving { + return m, nil + } m.mode = modeList return m, nil case "ctrl+s": - return m, m.submitForm() + return m.beginSubmitForm() case "up", "k": if m.mode == modeCardForm && m.form.focus == m.selectorFocusIndex() { m.moveCollectionCursor(-1) @@ -158,11 +161,17 @@ func (m model) updateForm(msg tea.Msg) (tea.Model, tea.Cmd) { } case "tab", "shift+tab", "enter": if keyMsg.String() == "enter" && m.form.focus == m.noteFocusIndex() { - return m, m.submitForm() + return m.beginSubmitForm() + } + if m.formSaving { + return m, nil } m.moveFormFocus(keyMsg.String() == "shift+tab") return m, nil } + if m.formSaving { + return m, nil + } return m.updateFocusedFormComponent(msg) } @@ -267,6 +276,9 @@ func (m model) formView() string { b.WriteString(m.accessTypeSelectorView() + "\n\n") } b.WriteString(m.noteView(m.form.focus == m.noteFocusIndex())) + if m.formSaving { + b.WriteString("\n\n" + theme.Help.Render(m.spinner.View()+" Saving… please wait")) + } return theme.FormPanel.Width(width).Render(strings.TrimRight(b.String(), "\n")) } @@ -471,6 +483,16 @@ func (m model) submitForm() tea.Cmd { return nil } +func (m model) beginSubmitForm() (tea.Model, tea.Cmd) { + if m.formSaving { + return m, nil + } + m.formSaving = true + m.err = nil + m.status = "" + return m, tea.Batch(m.submitForm(), m.spinner.Tick) +} + func (m model) selectedCollectionIDs() []string { ids := make([]string, 0, len(m.form.selectedCollectionIDs)) for id := range m.form.selectedCollectionIDs { diff --git a/internal/app/views_test.go b/internal/app/views_test.go index 99337fd..18df2bd 100644 --- a/internal/app/views_test.go +++ b/internal/app/views_test.go @@ -169,3 +169,30 @@ func TestCardFormUsesCollectionPicker(t *testing.T) { t.Fatalf("card form should not render collection IDs text input, got %q", view) } } + +func TestFormSaveShowsSavingStateAndIgnoresSecondSubmit(t *testing.T) { + m := newModel(nil, nil) + m.mode = modeCardForm + m.form = newCardForm() + + updated, cmd := m.updateForm(tea.KeyPressMsg(tea.Key{Code: 's', Mod: tea.ModCtrl})) + got := updated.(model) + if !got.formSaving { + t.Fatalf("ctrl+s should enter saving state") + } + if cmd == nil { + t.Fatalf("ctrl+s should return submit command") + } + if view := got.formView(); !strings.Contains(view, "Saving") { + t.Fatalf("saving form should render loading feedback, got %q", view) + } + + updated, cmd = got.updateForm(tea.KeyPressMsg(tea.Key{Code: 's', Mod: tea.ModCtrl})) + got = updated.(model) + if !got.formSaving { + t.Fatalf("second ctrl+s should stay in saving state") + } + if cmd != nil { + t.Fatalf("second ctrl+s should not submit again") + } +}