Skip to content

Commit

Permalink
wip: parent operations
Browse files Browse the repository at this point in the history
  • Loading branch information
JonathanHope committed Mar 29, 2024
1 parent 2a14eb4 commit a728970
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 99 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ This software is still in its early days. This roadmap lays out where it current
- [x] Add folder
- [x] Delete bookmark
- [x] Delete folder
- [ ] Update bookmark
- [ ] Update folder
- [ ] Add tag
- [ ] Remove tag
- [ ] Update bookmark (description not done yet)
- [x] Update folder
- [x] Add tag
- [x] Remove tag
- [x] List bookmarks/folders
- [ ] View bookmark
183 changes: 154 additions & 29 deletions cmd/cli/tui/booksview/books.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const TableName = "BooksTable" // name of the table
const HelpName = "BooksHelp" // name of the help screen
const TypeaheadName = "BooksTypeahead" // name of the typeahead
const AddTagOperation = "AddTag" // operation to add a tag
const RemoveTagOperation = "RemoveTag" // operation to remove tag
const RemoveTagOp = "RemoveTag" // operation to remove tag
const ChangeParentOp = "ChangeParent" // operation to change parent

// InputType is which type of input is being collected.
type inputType int
Expand Down Expand Up @@ -113,6 +114,8 @@ func InitialModel() tea.Model {
{Context: "Listing", Key: "b", Help: "Add bookmark"},
{Context: "Listing", Key: "t", Help: "Add tag"},
{Context: "Listing", Key: "T", Help: "Remove tag"},
{Context: "Listing", Key: "p", Help: "Change parent"},
{Context: "Listing", Key: "P", Help: "Remove parent"},
{Context: "Listing", Key: "q", Help: "Quit"},
{Context: "Input", Key: "left", Help: "Move to previous char"},
{Context: "Input", Key: "right", Help: "Move to next char"},
Expand Down Expand Up @@ -359,35 +362,113 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
128,
AddTagOperation,
true,
func() ([]string, error) {
func() ([]msgs.TypeaheadItem, error) {
options := armariaapi.DefaultListTagsOptions()
return armariaapi.ListTags(options)
tags, err := armariaapi.ListTags(options)

if err != nil {
return nil, err
}

items := lo.Map(tags, func(tag string, index int) msgs.TypeaheadItem {
return msgs.TypeaheadItem{Label: tag, Value: tag}
})

return items, nil
},
func(query string) ([]string, error) {
func(query string) ([]msgs.TypeaheadItem, error) {
options := armariaapi.DefaultListTagsOptions().WithQuery(query)
return armariaapi.ListTags(options)
tags, err := armariaapi.ListTags(options)

if err != nil {
return nil, err
}

items := lo.Map(tags, func(tag string, index int) msgs.TypeaheadItem {
return msgs.TypeaheadItem{Label: tag, Value: tag}
})

return items, nil
},
))
}

case "T":
cmds = append(cmds, m.typeaheadStartCmd(
"Remove Tag: ",
"",
128,
RemoveTagOperation,
false,
func() ([]string, error) {
return m.table.Selection().Tags, nil
},
func(query string) ([]string, error) {
tags := lo.Filter(m.table.Selection().Tags, func(tag string, index int) bool {
return strings.Contains(tag, query)
})
if !m.header.Busy() && !m.table.Empty() && !m.table.Selection().IsFolder {
cmds = append(cmds, m.typeaheadStartCmd(
"Remove Tag: ",
"",
128,
RemoveTagOp,
false,
func() ([]msgs.TypeaheadItem, error) {
items := lo.Map(m.table.Selection().Tags, func(tag string, index int) msgs.TypeaheadItem {
return msgs.TypeaheadItem{Label: tag, Value: tag}
})

return items, nil
},
func(query string) ([]msgs.TypeaheadItem, error) {
tags := lo.Filter(m.table.Selection().Tags, func(tag string, index int) bool {
return strings.Contains(tag, query)
})

return tags, nil
},
))
items := lo.Map(tags, func(tag string, index int) msgs.TypeaheadItem {
return msgs.TypeaheadItem{Label: tag, Value: tag}
})

return items, nil
},
))
}

case "p":
if !m.header.Busy() && !m.table.Empty() {
cmds = append(cmds, m.typeaheadStartCmd(
"Change Parent: ",
"",
2048,
ChangeParentOp,
false,
func() ([]msgs.TypeaheadItem, error) {
options := armariaapi.DefaultListBooksOptions().WithFolders(true).WithBooks(false)
books, err := armariaapi.ListBooks(options)

if err != nil {
return nil, err
}

items := lo.Map(books, func(book armaria.Book, index int) msgs.TypeaheadItem {
return msgs.TypeaheadItem{Label: book.Name, Value: book.ID}
})

return items, nil
},
func(query string) ([]msgs.TypeaheadItem, error) {
options := armariaapi.
DefaultListBooksOptions().
WithFolders(true).
WithBooks(false).
WithQuery(query)
books, err := armariaapi.ListBooks(options)

if err != nil {
return nil, err
}

items := lo.Map(books, func(book armaria.Book, index int) msgs.TypeaheadItem {
return msgs.TypeaheadItem{Label: book.Name, Value: book.ID}
})

return items, nil
},
))
}

case "P":
if !m.header.Busy() && !m.table.Empty() && m.table.Selection().ParentID != nil {
cmds = append(cmds, m.removeParentCmd())
}
}

case msgs.SelectionChangedMsg[armaria.Book]:
Expand Down Expand Up @@ -447,9 +528,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

case msgs.TypeaheadConfirmedMsg:
if msg.Name == TypeaheadName && msg.Operation == AddTagOperation {
cmds = append(cmds, m.typeaheadEndCmd(), m.addTag(msg.Value))
} else if msg.Name == TypeaheadName && msg.Operation == RemoveTagOperation {
cmds = append(cmds, m.typeaheadEndCmd(), m.removeTag(msg.Value))
cmds = append(cmds, m.typeaheadEndCmd(), m.addTagCmd(msg.Value.Value))
} else if msg.Name == TypeaheadName && msg.Operation == RemoveTagOp {
cmds = append(cmds, m.typeaheadEndCmd(), m.removeTagCmd(msg.Value.Value))
} else if msg.Name == TypeaheadName && msg.Operation == ChangeParentOp {
cmds = append(cmds, m.typeaheadEndCmd(), m.changeParentCmd(msg.Value.Value))
}

case msgs.InputChangedMsg:
Expand Down Expand Up @@ -725,7 +808,7 @@ func (m model) inputEndCmd() tea.Cmd {
}

// typeaheadStartCmd makes the necessary state updates when the typeahead mode starts.
func (m model) typeaheadStartCmd(prompt string, text string, maxChars int, operation string, includeInput bool, unfilteredQuery func() ([]string, error), filteredQuery func(query string) ([]string, error)) tea.Cmd {
func (m model) typeaheadStartCmd(prompt string, text string, maxChars int, operation string, includeInput bool, unfilteredQuery msgs.UnfilteredQueryFn, filteredQuery msgs.FilteredQueryFn) tea.Cmd {
return func() tea.Msg {
return msgs.TypeaheadModeMsg{
Name: TypeaheadName,
Expand Down Expand Up @@ -883,8 +966,8 @@ func (m model) moveBetweenCmd(previous string, next string, move msgs.Direction)
}
}

// addTag adds a tag to a bookmark.
func (m model) addTag(tag string) tea.Cmd {
// addTagCmd adds a tag to a bookmark.
func (m model) addTagCmd(tag string) tea.Cmd {
return func() tea.Msg {
options := armariaapi.DefaultAddTagsOptions()
_, err := armariaapi.AddTags(m.table.Selection().ID, []string{tag}, options)
Expand All @@ -895,8 +978,8 @@ func (m model) addTag(tag string) tea.Cmd {
}
}

// removeTag removes a tag from a bookmark.
func (m model) removeTag(tag string) tea.Cmd {
// removeTagCmd removes a tag from a bookmark.
func (m model) removeTagCmd(tag string) tea.Cmd {
return func() tea.Msg {
options := armariaapi.DefaultRemoveTagsOptions()
_, err := armariaapi.RemoveTags(m.table.Selection().ID, []string{tag}, options)
Expand All @@ -906,3 +989,45 @@ func (m model) removeTag(tag string) tea.Cmd {
return m.getBooksCmd(msgs.DirectionNone)()
}
}

// changeParentCmd changes the parent of a bookmark or folder.
func (m model) changeParentCmd(parentID string) tea.Cmd {
return func() tea.Msg {
if m.table.Selection().IsFolder {
options := armariaapi.DefaultUpdateFolderOptions().WithParentID(parentID)
_, err := armariaapi.UpdateFolder(m.table.Selection().ID, options)
if err != nil {
return msgs.ErrorMsg{Err: err}
}
} else {
options := armariaapi.DefaultUpdateBookOptions().WithParentID(parentID)
_, err := armariaapi.UpdateBook(m.table.Selection().ID, options)
if err != nil {
return msgs.ErrorMsg{Err: err}
}
}

return m.getBooksCmd(msgs.DirectionNone)()
}
}

// removeParentCmd removes the parent of a bookmark or folder.
func (m model) removeParentCmd() tea.Cmd {
return func() tea.Msg {
if m.table.Selection().IsFolder {
options := armariaapi.DefaultUpdateFolderOptions().WithoutParentID()
_, err := armariaapi.UpdateFolder(m.table.Selection().ID, options)
if err != nil {
return msgs.ErrorMsg{Err: err}
}
} else {
options := armariaapi.DefaultUpdateBookOptions().WithoutParentID()
_, err := armariaapi.UpdateBook(m.table.Selection().ID, options)
if err != nil {
return msgs.ErrorMsg{Err: err}
}
}

return m.getBooksCmd(msgs.DirectionNone)()
}
}
39 changes: 26 additions & 13 deletions cmd/cli/tui/msgs/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,38 @@ type BlinkMsg struct {
Name string // name of the target component
}

// TypeaheadItem is the model for an item in the typeahead.
type TypeaheadItem struct {
Value string // hidden identifier
Label string // visible text
New bool // if true this is a new item that didn't exist before
}

// UnfilteredQueryFn is a function that returns typeahead items when no filter is active.
type UnfilteredQueryFn func() ([]TypeaheadItem, error)

// FilteredQueryFn is a function that returns the typeahead items when a filter is active.
type FilteredQueryFn func(query string) ([]TypeaheadItem, error)

// TypeaheadModeMsg is used to change to typeahead mode.
type TypeaheadModeMsg struct {
Name string // name of the target component
InputMode bool // if true the typeahead will be in input mode
Prompt string // the prompt to show
Text string // the text to start the input with
MaxChars int // the maximum number of chars to allow
UnfilteredQuery func() ([]string, error) // returns results when there isn't enough input
FilteredQuery func(query string) ([]string, error) // returns results when there's enough input
MinFilterChars int // the minumum number of chars needed to filter
Operation string // the operation the typeahead is for
IncludeInput bool // if true include the current input as an option
Name string // name of the target component
InputMode bool // if true the typeahead will be in input mode
Prompt string // the prompt to show
Text string // the text to start the input with
MaxChars int // the maximum number of chars to allow
UnfilteredQuery UnfilteredQueryFn // returns results when there isn't enough input
FilteredQuery FilteredQueryFn // returns results when there's enough input
MinFilterChars int // the minumum number of chars needed to filter
Operation string // the operation the typeahead is for
IncludeInput bool // if true include the current input as an option
}

// TypeaheadConfirmedMsg is published when an option is selected..
type TypeaheadConfirmedMsg struct {
Name string // name of the target typeahead
Value string // the value that was selected
Operation string // the operation the typeahead is for
Name string // name of the target typeahead
Value TypeaheadItem // the value that was selected
Operation string // the operation the typeahead is for
}

// SelectionConfirmedMsg is published when an option selection is cancelled.
Expand Down
Loading

0 comments on commit a728970

Please sign in to comment.