Merge pull request #98 from boolean-maybe/fix/actions-in-edit-mode

fix action firing on printable character
This commit is contained in:
boolean-maybe 2026-04-19 19:48:04 -04:00 committed by GitHub
commit ef25f5ff98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 222 additions and 4 deletions

View file

@ -113,11 +113,12 @@ func (ir *InputRouter) HandleInput(event *tcell.EventKey, currentView *ViewEntry
}
}
// pre-gate: ActionOpenPalette (*) and ActionToggleHeader (F10) must fire before
// task-edit Prepare and before search/fullscreen/editor gates, so they stay truly
// global without triggering edit-session setup or focus churn.
// pre-gate: global actions that must fire before task-edit Prepare() and before
// search/fullscreen/editor gates. ActionOpenPalette is suppressed in TaskEditView
// because '*' is a typeable rune that should be inserted into fields.
if action := ir.globalActions.Match(event); action != nil {
if action.ID == ActionOpenPalette || action.ID == ActionToggleHeader {
isTaskEdit := currentView != nil && currentView.ViewID == model.TaskEditViewID
if action.ID == ActionToggleHeader || (action.ID == ActionOpenPalette && !isTaskEdit) {
return ir.handleGlobalAction(action.ID)
}
}

View file

@ -1,9 +1,13 @@
package integration
import (
"strings"
"testing"
"github.com/boolean-maybe/tiki/model"
taskpkg "github.com/boolean-maybe/tiki/task"
"github.com/boolean-maybe/tiki/testutil"
"github.com/boolean-maybe/tiki/view/taskdetail"
"github.com/gdamore/tcell/v2"
)
@ -93,3 +97,216 @@ func TestActionPalette_AsteriskFiltersInPalette(t *testing.T) {
ta.SendKey(tcell.KeyEscape, 0, tcell.ModNone)
}
// getEditView type-asserts the active view to *taskdetail.TaskEditView.
func getEditView(t *testing.T, ta *testutil.TestApp) *taskdetail.TaskEditView {
t.Helper()
v := ta.NavController.GetActiveView()
ev, ok := v.(*taskdetail.TaskEditView)
if !ok {
t.Fatalf("expected *taskdetail.TaskEditView, got %T", v)
}
return ev
}
func TestActionPalette_BlockedInTaskEdit_DraftFirstKey(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
// press 'n' to create new task (draft path, title focused)
ta.SendKey(tcell.KeyRune, 'n', tcell.ModNone)
// very first key in edit view is '*'
ta.SendKey(tcell.KeyRune, '*', tcell.ModNone)
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is pressed in task edit (draft, first key)")
}
ev := getEditView(t, ta)
if got := ev.GetEditedTitle(); got != "*" {
t.Fatalf("title = %q, want %q", got, "*")
}
}
func TestActionPalette_BlockedInTaskEdit_ExistingFirstKey(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Test", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("reload: %v", err)
}
// push task edit directly (non-draft path)
ta.NavController.PushView(model.TaskEditViewID, model.EncodeTaskEditParams(model.TaskEditParams{
TaskID: "TIKI-1",
Focus: model.EditFieldTitle,
}))
ta.Draw()
// first key is '*'
ta.SendKey(tcell.KeyRune, '*', tcell.ModNone)
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is pressed in task edit (existing, first key)")
}
ev := getEditView(t, ta)
if got := ev.GetEditedTitle(); got != "Test*" {
t.Fatalf("title = %q, want %q", got, "Test*")
}
}
func TestActionPalette_BlockedInTaskEdit_TitleMidText(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyRune, 'n', tcell.ModNone)
ta.SendText("ab*cd")
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is typed mid-title")
}
ev := getEditView(t, ta)
if got := ev.GetEditedTitle(); got != "ab*cd" {
t.Fatalf("title = %q, want %q", got, "ab*cd")
}
}
func TestActionPalette_BlockedInTaskEdit_Description(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Test", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("reload: %v", err)
}
ta.NavController.PushView(model.TaskEditViewID, model.EncodeTaskEditParams(model.TaskEditParams{
TaskID: "TIKI-1",
Focus: model.EditFieldDescription,
DescOnly: true,
}))
ta.Draw()
ta.SendText("x*y")
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is typed in description")
}
ev := getEditView(t, ta)
desc := ev.GetEditedDescription()
if !strings.Contains(desc, "x*y") {
t.Fatalf("description = %q, should contain %q", desc, "x*y")
}
}
func TestActionPalette_BlockedInTaskEdit_Tags(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Test", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("reload: %v", err)
}
ta.NavController.PushView(model.TaskEditViewID, model.EncodeTaskEditParams(model.TaskEditParams{
TaskID: "TIKI-1",
TagsOnly: true,
}))
ta.Draw()
ta.SendText("tag*val")
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is typed in tags")
}
ev := getEditView(t, ta)
tags := ev.GetEditedTags()
if len(tags) != 1 || tags[0] != "tag*val" {
t.Fatalf("tags = %v, want [tag*val]", tags)
}
}
func TestActionPalette_BlockedInTaskEdit_Assignee(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Test", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("reload: %v", err)
}
// push task edit (default title focus)
ta.NavController.PushView(model.TaskEditViewID, model.EncodeTaskEditParams(model.TaskEditParams{
TaskID: "TIKI-1",
Focus: model.EditFieldTitle,
}))
ta.Draw()
// tab 5× to Assignee: Title→Status→Type→Priority→Points→Assignee
for i := 0; i < 5; i++ {
ta.SendKey(tcell.KeyTab, 0, tcell.ModNone)
}
ta.SendKey(tcell.KeyRune, '*', tcell.ModNone)
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is pressed on Assignee field")
}
editTask := ta.EditingTask()
if editTask == nil {
t.Fatal("expected editing task to be non-nil")
}
if editTask.Assignee != "Unassigned*" {
t.Fatalf("assignee = %q, want %q ('*' appended to default value)", editTask.Assignee, "Unassigned*")
}
}
func TestActionPalette_BlockedInTaskEdit_NonTypeableField(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Test", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("reload: %v", err)
}
// push task edit (default title focus)
ta.NavController.PushView(model.TaskEditViewID, model.EncodeTaskEditParams(model.TaskEditParams{
TaskID: "TIKI-1",
Focus: model.EditFieldTitle,
}))
ta.Draw()
// tab 1× to Status (non-typeable field)
ta.SendKey(tcell.KeyTab, 0, tcell.ModNone)
ta.SendKey(tcell.KeyRune, '*', tcell.ModNone)
if ta.GetPaletteConfig().IsVisible() {
t.Fatal("palette should NOT open when '*' is pressed on Status field")
}
editTask := ta.EditingTask()
if editTask == nil {
t.Fatal("expected editing task to be non-nil")
}
if editTask.Status != taskpkg.StatusReady {
t.Fatalf("status = %v, want %v (rune should be silently ignored)", editTask.Status, taskpkg.StatusReady)
}
}