diff --git a/cmd/list/work_packages.go b/cmd/list/work_packages.go index 702bacf..dd88f4e 100644 --- a/cmd/list/work_packages.go +++ b/cmd/list/work_packages.go @@ -2,6 +2,9 @@ package list import ( "fmt" + "github.com/opf/openproject-cli/components/requests" + "github.com/opf/openproject-cli/components/resources" + "github.com/opf/openproject-cli/components/resources/work_packages/filters" "os" "regexp" "strconv" @@ -23,6 +26,10 @@ var typeFilter string var includeSubProjects bool var subProject string +var activeFilters = []resources.Filter{ + filters.NewTimestampFilter(), +} + var workPackagesCmd = &cobra.Command{ Use: "workpackages", Aliases: []string{"wps"}, @@ -32,12 +39,19 @@ var workPackagesCmd = &cobra.Command{ } func listWorkPackages(_ *cobra.Command, _ []string) { + // This needs to be removed, once all filters are built the "new" way if errorText := validateCommandFlags(); len(errorText) > 0 { printer.ErrorText(errorText) return } - collection, err := work_packages.All(filterOptions(), showTotal) + query, err := buildQuery() + if err != nil { + printer.ErrorText(err.Error()) + return + } + + collection, err := work_packages.All(filterOptions(), query, showTotal) switch { case err == nil && showTotal: printer.Number(collection.Total) @@ -59,6 +73,21 @@ func validateCommandFlags() (errorText string) { } } +func buildQuery() (requests.Query, error) { + var q requests.Query + + for _, filter := range activeFilters { + err := filter.ValidateInput() + if err != nil { + return requests.NewEmptyQuery(), err + } + + q = q.Merge(filter.Query()) + } + + return q, nil +} + func filterOptions() *map[work_packages.FilterOption]string { options := make(map[work_packages.FilterOption]string) diff --git a/cmd/list/work_packages_flags.go b/cmd/list/work_packages_flags.go index f718356..b48bf1e 100644 --- a/cmd/list/work_packages_flags.go +++ b/cmd/list/work_packages_flags.go @@ -1,5 +1,9 @@ package list +import ( + "github.com/opf/openproject-cli/components/resources" +) + func initWorkPackagesFlags() { workPackagesCmd.Flags().StringVarP( &assignee, @@ -71,4 +75,17 @@ the default is false.`) "", false, "Show only the total number of work packages matching the filter options.") + + for _, filter := range activeFilters { + switch filter.(type) { + case resources.StringValueFilter: + workPackagesCmd.Flags().StringVarP( + filter.Value(), + filter.Name(), + filter.ShortHand(), + filter.(resources.StringValueFilter).DefaultValue(), + filter.Usage(), + ) + } + } } diff --git a/components/requests/query.go b/components/requests/query.go index 873088e..5032b96 100644 --- a/components/requests/query.go +++ b/components/requests/query.go @@ -26,6 +26,24 @@ func (filter Filter) String() string { ) } +func (query Query) Merge(another Query) Query { + filters := append(query.filters, another.filters...) + + attributes := query.attributes + if attributes == nil { + attributes = make(map[string]string) + } + + for key, value := range another.attributes { + attributes[key] = value + } + + return Query{ + attributes: attributes, + filters: filters, + } +} + func (query Query) String() string { queryStr := filtersQueryAttribute(query.filters) for key, value := range query.attributes { @@ -60,6 +78,10 @@ func NewQuery(attributes map[string]string, filters []Filter) Query { return Query{attributes: attributes, filters: filters} } +func NewEmptyQuery() Query { + return Query{attributes: make(map[string]string), filters: []Filter{}} +} + func NewFilterQuery(filters []Filter) Query { attributes := map[string]string{ "pageSize": "100", diff --git a/components/requests/query_test.go b/components/requests/query_test.go index 9e7f44d..100522a 100644 --- a/components/requests/query_test.go +++ b/components/requests/query_test.go @@ -99,3 +99,31 @@ func TestQuery_String(t *testing.T) { t.Errorf("Expected %s to contain %s", queryString, expected) } } + +func TestQuery_Merge(t *testing.T) { + attributes1 := map[string]string{ + "pageSize": "20", + "timestamps": "PT0S", + } + filters1 := []requests.Filter{ + work_packages.StatusFilter("1,3"), + } + + attributes2 := map[string]string{ + "pageSize": "25", + "includeSubprojects": "true", + } + filters2 := []requests.Filter{ + work_packages.TypeFilter("!1"), + } + + query1 := requests.NewQuery(attributes1, filters1) + query2 := requests.NewQuery(attributes2, filters2) + + result := query1.Merge(query2) + expected := "filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%2C%223%22%5D%7D%7D%2C%7B%22type%22%3A%7B%22operator%22%3A%22%21%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&pageSize=25×tamps=PT0S&includeSubprojects=true" + + if result.String() != expected { + t.Errorf("Expected %s, but got %s", expected, result.String()) + } +} diff --git a/components/resources/filters.go b/components/resources/filters.go index 73026c0..0f8151f 100644 --- a/components/resources/filters.go +++ b/components/resources/filters.go @@ -9,3 +9,16 @@ func TypeAheadFilter(input string) requests.Filter { Values: []string{input}, } } + +type Filter interface { + Value() *string + Name() string + ShortHand() string + Usage() string + ValidateInput() error + Query() requests.Query +} + +type StringValueFilter interface { + DefaultValue() string +} diff --git a/components/resources/work_packages/filters/timestamp.go b/components/resources/work_packages/filters/timestamp.go new file mode 100644 index 0000000..ada3ca6 --- /dev/null +++ b/components/resources/work_packages/filters/timestamp.go @@ -0,0 +1,51 @@ +package filters + +import ( + "time" + + "github.com/opf/openproject-cli/components/requests" +) + +type TimestampFilter struct { + value string +} + +func (f *TimestampFilter) Value() *string { + return &f.value +} + +func (f *TimestampFilter) Name() string { + return "timestamp" +} + +func (f *TimestampFilter) ShortHand() string { + return "" +} + +func (f *TimestampFilter) Usage() string { + return `Returns the list of work packages at a specific timestamp. The timestamp should +be in the format of 'YYYY-MM-DDTHH:MM:SSZ' or date only 'YYYY-MM-DD', which +then assumes the time being 00:00:00Z.` +} + +func (f *TimestampFilter) ValidateInput() error { + _, err := time.Parse(time.DateOnly, f.value) + if err == nil { + return nil + } + + _, err = time.Parse(time.RFC3339, f.value) + return err +} + +func (f *TimestampFilter) DefaultValue() string { + return "" +} + +func (f *TimestampFilter) Query() requests.Query { + return requests.NewQuery(map[string]string{"timestamps": f.value}, nil) +} + +func NewTimestampFilter() *TimestampFilter { + return &TimestampFilter{} +} diff --git a/components/resources/work_packages/read.go b/components/resources/work_packages/read.go index 3db8291..6534940 100644 --- a/components/resources/work_packages/read.go +++ b/components/resources/work_packages/read.go @@ -19,7 +19,7 @@ func Lookup(id uint64) (*models.WorkPackage, error) { return workPackage.Convert(), nil } -func All(filterOptions *map[FilterOption]string, showOnlyTotal bool) (*models.WorkPackageCollection, error) { +func All(filterOptions *map[FilterOption]string, query requests.Query, showOnlyTotal bool) (*models.WorkPackageCollection, error) { var filters []requests.Filter var projectId *uint64 var queryAttributes = make(map[string]string) @@ -50,7 +50,8 @@ func All(filterOptions *map[FilterOption]string, showOnlyTotal bool) (*models.Wo queryAttributes["pageSize"] = "-1" } - query := requests.NewQuery(queryAttributes, filters) + legacyQuery := requests.NewQuery(queryAttributes, filters) + newQuery := legacyQuery.Merge(query) requestUrl := paths.WorkPackages() @@ -58,7 +59,7 @@ func All(filterOptions *map[FilterOption]string, showOnlyTotal bool) (*models.Wo requestUrl = paths.ProjectWorkPackages(*projectId) } - response, err := requests.Get(requestUrl, &query) + response, err := requests.Get(requestUrl, &newQuery) if err != nil { return nil, err }