Skip to content

feat(filter): expose view options (board/list, group by, sort) - #463

Open
alejandro-goffa wants to merge 1 commit into
Doist:mainfrom
alejandro-goffa:feat/filter-view-options
Open

feat(filter): expose view options (board/list, group by, sort)#463
alejandro-goffa wants to merge 1 commit into
Doist:mainfrom
alejandro-goffa:feat/filter-view-options

Conversation

@alejandro-goffa

Copy link
Copy Markdown

Implements Option A from #462 — flags on the existing commands, since that was the smallest surface that covers the common cases. Happy to reshape into a dedicated subcommand (Option B) if you'd rather reach TODAY/UPCOMING too; the parsing and API layers are already separate from the filter commands, so moving them is cheap.

What this adds

td filter create --name "Team" --query "(p1 | p2 | !no deadline)" \
    --view-mode board --group-by assignee --sort-by priority --sort-order desc

td filter update "Team" --view-mode list

Four new flags on filter create and filter update: --view-mode, --group-by, --sort-by, --sort-order.

Notes on the implementation

  • Wiring, not new capability. @doist/todoist-sdk already types view_options_set and re-exports VIEW_MODES / GROUPED_BY_OPTIONS / SORTED_BY_OPTIONS / SORT_ORDERS. Allowed values are derived from those constants rather than restated, so the CLI can't drift from the SDK.
  • Casing is handled for the user. The Sync API requires uppercase enums and rejects anything else with error_code: 20, while the API reference documents lowercase. Flags accept lowercase kebab-case and normalise (due-dateDUE_DATE). Invalid values fail before any request with the allowed list echoed back.
  • update with only view flags no longer trips NO_CHANGES, and skips the filter_update call when nothing about the filter itself changed — one command, one request.
  • src/lib/api/view-options.ts takes a viewType/objectId rather than being filter-specific, so project/label can reuse it.
  • View options are stored per user; the doc comment says so, since it is easy to assume they travel with a shared project.

Verification

  • npm run check, npm run type-check, npm run build — clean

  • npm test — 1784 passing (5 added)

  • Exercised end-to-end against the live API: after filter create ... --view-mode board --group-by assignee --sort-by priority --sort-order desc, a view_options sync read returns

    {"view_type": "FILTER", "object_id": "...", "view_mode": "BOARD",
     "grouped_by": "ASSIGNEE", "sorted_by": "PRIORITY", "sort_order": "DESC"}
  • Dry-run and validation output:

    $ td filter create --name "Team" --query today --view-mode board --group-by assignee --dry-run
    [dry-run] Would create filter:
      Name: Team
      Query: today
      View: viewMode=board, groupedBy=assignee
    
    $ td filter create --name X --query today --group-by colour
    Error: INVALID_OPTIONS
    Invalid value "colour" for --group-by.
      - Allowed: assignee, added-date, due-date, deadline, label, priority, project, workspace
    

Closes #462

Filters could be created and updated from the CLI, but their presentation
could not. A query like "(p1 | p2 | !no deadline)" is only readable as a
board grouped by assignee, and that layout was unreachable without
hand-rolling a Sync request.

The SDK already types the view_options_set command and re-exports the enum
constants, and the CLI already drives Sync commands the same way in
lib/api/filters.ts, so this wires up the existing pieces.

The Sync API requires these enums in UPPERCASE and rejects anything else
with error_code 20, while the public API reference documents them in
lowercase. Flag values are accepted in lowercase kebab-case and normalized
(due-date -> DUE_DATE), so callers never meet that discrepancy. Allowed
values come from the SDK constants rather than being restated.

View options are stored per user, so this affects only the authenticated
account even when the filter's underlying projects are shared.

Refs Doist#462

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@scottlovegrove scottlovegrove left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for creating the PR, just a few small things to take care of

.option('--query <query>', 'Filter query (required, e.g., "today | overdue")')
.option('--color <color>', 'Filter color')
.option('--favorite', 'Mark as favorite')
.option('--view-mode <mode>', 'Layout: list, board or calendar')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] These new view-option flags need adding to SKILL_CONTENT in src/lib/skills/content.ts, then regenerate skills/todoist-cli/SKILL.md with npm run sync:skill. The installed agent skill is the command reference, and right now it documents filter create / filter update with no way to discover --view-mode, --group-by, --sort-by or --sort-order.

.option('--query <query>', 'Filter query (required, e.g., "today | overdue")')
.option('--color <color>', 'Filter color')
.option('--favorite', 'Mark as favorite')
.option('--view-mode <mode>', 'Layout: list, board or calendar')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Worth centralising the registration of these four flags. The same definitions are duplicated for create and update, and the accepted values are separately derived from the SDK arrays in view-options.ts, so a future change to the options can easily leave one help block stale. Extracting a shared registration helper and deriving the choice descriptions from the same option metadata would keep them in step.


export async function updateFilterCmd(nameOrId: string, options: UpdateOptions): Promise<void> {
const filter = await resolveFilterRef(nameOrId)
const viewOptions = parseViewOptionFlags(options)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Parse the view flags before resolving the filter. An invalid value currently triggers resolveFilterRef, which fetches the entire filter list, before failing locally — e.g. td filter update Work --group-by colour performs a Sync request that was never needed.

}

await updateFilter(filter.id, args)
if (Object.keys(args).length > 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] When both filter fields and view flags are supplied this fires two independent Sync requests serially, adding a full network round trip to every combined update. Batching filter_update and view_options_set into a single api.sync call (or at least running them concurrently) would avoid that.

})
})

describe('filter view options', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Could we add dry-run coverage with a view flag, for both create and update, asserting setViewOptions is not called? The existing dry-run tests only exercise the filter fields, so a regression that persisted view settings during --dry-run would pass while quietly changing the user's layout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: expose view options (board/list, group by, sort) — SDK supports view_options_set but the CLI never calls it

2 participants