diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index c13036c..1f47e1c 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -60,7 +60,10 @@ func (c *CommandController) Alias(commandName string) Command { return events.SetError(errors.New("no such command: " + commandName)) } - return command(args[1:]) + if len(args) > 1 { + return command(args[1:]) + } + return command([]string{}) } } diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index a02d6b8..54cc56b 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/awstools/internal/common/sliceutils" "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" @@ -112,12 +113,15 @@ func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Cmd OnDone: func(value string) tea.Cmd { return func() tea.Msg { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberS{Value: value}) - if err != nil { + if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error { + if err := attr.setAt(item, &types.AttributeValueMemberS{Value: value}); err != nil { + return err + } + set.SetDirty(idx, true) + return nil + }); err != nil { return err } - - set.SetDirty(idx, true) set.RefreshColumns() return nil }); err != nil { @@ -130,6 +134,19 @@ func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Cmd } } +func (twc *TableWriteController) applyToItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error { + if markedItems := rs.MarkedItems(); len(markedItems) > 0 { + for _, mi := range markedItems { + if err := applyFn(mi.Index, mi.Item); err != nil { + return err + } + } + return nil + } + + return applyFn(selectedIndex, rs.Items()[selectedIndex]) +} + func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Cmd { return func() tea.Msg { return events.PromptForInputMsg{ @@ -137,12 +154,15 @@ func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Cmd OnDone: func(value string) tea.Cmd { return func() tea.Msg { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberN{Value: value}) - if err != nil { + if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error { + if err := attr.setAt(item, &types.AttributeValueMemberN{Value: value}); err != nil { + return err + } + set.SetDirty(idx, true) + return nil + }); err != nil { return err } - - set.SetDirty(idx, true) set.RefreshColumns() return nil }); err != nil { @@ -167,12 +187,15 @@ func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Cmd { } if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberBOOL{Value: b}) - if err != nil { + if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error { + if err := attr.setAt(item, &types.AttributeValueMemberBOOL{Value: b}); err != nil { + return err + } + set.SetDirty(idx, true) + return nil + }); err != nil { return err } - - set.SetDirty(idx, true) set.RefreshColumns() return nil }); err != nil { @@ -188,12 +211,15 @@ func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Cmd { func (twc *TableWriteController) setNullValue(idx int, attr attrPath) tea.Cmd { return func() tea.Msg { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberNULL{Value: true}) - if err != nil { + if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error { + if err := attr.setAt(item, &types.AttributeValueMemberNULL{Value: true}); err != nil { + return err + } + set.SetDirty(idx, true) + return nil + }); err != nil { return err } - - set.SetDirty(idx, true) set.RefreshColumns() return nil }); err != nil { @@ -260,21 +286,29 @@ func (twc *TableWriteController) PutItem(idx int) tea.Cmd { func (twc *TableWriteController) PutItems() tea.Cmd { return func() tea.Msg { var ( - expectedPuts int - markedItems int + markedItemCount int ) + var itemsToPut []models.ItemIndex twc.state.withResultSet(func(rs *models.ResultSet) { - markedItems = len(rs.MarkedItems()) - for i := range rs.Items() { - if rs.IsDirty(i) && (markedItems == 0 || rs.Marked(i)) { - expectedPuts++ + if markedItems := rs.MarkedItems(); len(markedItems) > 0 { + for _, mi := range markedItems { + markedItemCount += 1 + if rs.IsDirty(mi.Index) { + itemsToPut = append(itemsToPut, mi) + } + } + } else { + for i, itm := range rs.Items() { + if rs.IsDirty(i) { + itemsToPut = append(itemsToPut, models.ItemIndex{Item: itm, Index: i}) + } } } }) - if expectedPuts == 0 { - if markedItems > 0 { + if len(itemsToPut) == 0 { + if markedItemCount > 0 { return events.StatusMsg("no marked items are modified") } else { return events.StatusMsg("no items are modified") @@ -282,10 +316,10 @@ func (twc *TableWriteController) PutItems() tea.Cmd { } var promptMessage string - if markedItems > 0 { - promptMessage = applyToN("put ", expectedPuts, "marked item", "marked items", "? ") + if markedItemCount > 0 { + promptMessage = applyToN("put ", len(itemsToPut), "marked item", "marked items", "? ") } else { - promptMessage = applyToN("put ", expectedPuts, "item", "items", "? ") + promptMessage = applyToN("put ", len(itemsToPut), "item", "items", "? ") } return events.PromptForInputMsg{ @@ -297,13 +331,9 @@ func (twc *TableWriteController) PutItems() tea.Cmd { return func() tea.Msg { if err := twc.state.withResultSetReturningError(func(rs *models.ResultSet) error { - updated, err := twc.tableService.PutSelectedItems(context.Background(), rs, func(idx int) bool { - return rs.IsDirty(idx) && (markedItems == 0 || rs.Marked(idx)) - }) + err := twc.tableService.PutSelectedItems(context.Background(), rs, itemsToPut) if err != nil { return err - } else if updated != expectedPuts { - return errors.Errorf("expected %d updates but only %d were applied", expectedPuts, updated) } return nil }); err != nil { @@ -311,7 +341,7 @@ func (twc *TableWriteController) PutItems() tea.Cmd { } return ResultSetUpdated{ - statusMessage: applyToN("", expectedPuts, "item", "item", " put to table"), + statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"), } } }, @@ -395,7 +425,9 @@ func (twc *TableWriteController) DeleteMarked() tea.Cmd { return func() tea.Msg { ctx := context.Background() - if err := twc.tableService.Delete(ctx, resultSet.TableInfo, markedItems); err != nil { + if err := twc.tableService.Delete(ctx, resultSet.TableInfo, sliceutils.Map(markedItems, func(index models.ItemIndex) models.Item { + return index.Item + })); err != nil { return events.Error(err) } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 11316e5..abdccc9 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -77,22 +77,37 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { } for _, scenario := range scenarios { - t.Run(fmt.Sprintf("should set value of field %v", scenario.attrKey), func(t *testing.T) { + t.Run(fmt.Sprintf("should set value of field: %v", scenario.attrKey), func(t *testing.T) { state := controllers.NewState() readController := controllers.NewTableReadController(state, service, "alpha-table") writeController := controllers.NewTableWriteController(state, service, readController) invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") - assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.UnsetItemType, scenario.attrKey), scenario.attrValue) after, _ := state.ResultSet().Items()[0][scenario.attrKey] assert.Equal(t, scenario.expected, after) assert.True(t, state.ResultSet().IsDirty(0)) }) + + t.Run(fmt.Sprintf("should set value of marked fields: %v", scenario.attrKey), func(t *testing.T) { + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + invokeCommand(t, writeController.ToggleMark(0)) + invokeCommand(t, writeController.ToggleMark(2)) + invokeCommandWithPrompt(t, writeController.SetAttributeValue(1, models.UnsetItemType, scenario.attrKey), scenario.attrValue) + + after1, _ := state.ResultSet().Items()[0][scenario.attrKey] + assert.Equal(t, scenario.expected, after1) + assert.True(t, state.ResultSet().IsDirty(0)) + + after2, _ := state.ResultSet().Items()[2][scenario.attrKey] + assert.Equal(t, scenario.expected, after2) + assert.True(t, state.ResultSet().IsDirty(2)) + }) } }) diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go index d49d0d6..efca00f 100644 --- a/internal/dynamo-browse/models/items.go +++ b/internal/dynamo-browse/models/items.go @@ -5,6 +5,11 @@ import ( "github.com/lmika/awstools/internal/dynamo-browse/models/itemrender" ) +type ItemIndex struct { + Index int + Item Item +} + type Item map[string]types.AttributeValue // Clone creates a clone of the current item diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index ab82aa0..7bd9469 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -70,11 +70,11 @@ func (rs *ResultSet) IsNew(idx int) bool { return rs.attributes[idx].New } -func (rs *ResultSet) MarkedItems() []Item { - items := make([]Item, 0) +func (rs *ResultSet) MarkedItems() []ItemIndex { + items := make([]ItemIndex, 0) for i, itemAttr := range rs.attributes { if itemAttr.Marked && !itemAttr.Hidden { - items = append(items, rs.items[i]) + items = append(items, ItemIndex{Index: i, Item: rs.items[i]}) } } return items diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index e5e58a6..eb4e97e 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -115,34 +115,22 @@ func (s *Service) PutItemAt(ctx context.Context, resultSet *models.ResultSet, in return nil } -func (s *Service) PutSelectedItems(ctx context.Context, resultSet *models.ResultSet, shouldPut func(idx int) bool) (int, error) { - type dirtyItem struct { - item models.Item - idx int +func (s *Service) PutSelectedItems(ctx context.Context, resultSet *models.ResultSet, markedItems []models.ItemIndex) error { + if len(markedItems) == 0 { + return nil } - dirtyItems := make([]dirtyItem, 0) - for i, item := range resultSet.Items() { - if shouldPut(i) { - dirtyItems = append(dirtyItems, dirtyItem{item, i}) - } - } - - if len(dirtyItems) == 0 { - return 0, nil - } - - if err := s.provider.PutItems(ctx, resultSet.TableInfo.Name, sliceutils.Map(dirtyItems, func(t dirtyItem) models.Item { - return t.item + if err := s.provider.PutItems(ctx, resultSet.TableInfo.Name, sliceutils.Map(markedItems, func(t models.ItemIndex) models.Item { + return t.Item })); err != nil { - return 0, err + return err } - for _, di := range dirtyItems { - resultSet.SetDirty(di.idx, false) - resultSet.SetNew(di.idx, false) + for _, di := range markedItems { + resultSet.SetDirty(di.Index, false) + resultSet.SetNew(di.Index, false) } - return len(dirtyItems), nil + return nil } func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items []models.Item) error {