diff --git a/.Rbuildignore b/.Rbuildignore
index 36eb8b8..0a0fb20 100644
--- a/.Rbuildignore
+++ b/.Rbuildignore
@@ -14,3 +14,7 @@
^dev$
^\.RData$
^\.Rhistory$
+^CLAUDE\.md$
+^PLAN_IMPLEMENTATION\.md$
+^REVIEW\.md$
+^\.claude$
diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml
index 65e48cb..3e23339 100644
--- a/.github/workflows/R-CMD-check.yaml
+++ b/.github/workflows/R-CMD-check.yaml
@@ -1,19 +1,15 @@
-# For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag.
-# https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions
+# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
+# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
- branches:
- - main
- - master
- - dev
+ branches: [main, master, dev]
pull_request:
- branches:
- - main
- - master
- - dev
-
+ branches: [main, master, dev]
+
name: R-CMD-check
+permissions: read-all
+
jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}
@@ -24,65 +20,33 @@ jobs:
fail-fast: false
matrix:
config:
+ - {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- - {os: macOS-latest, r: 'release'}
- - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}
- - {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}
+ - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
+ - {os: ubuntu-latest, r: 'release'}
+ - {os: ubuntu-latest, r: 'oldrel-1'}
env:
- R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
- RSPM: ${{ matrix.config.rspm }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
+ R_KEEP_PKG_SOURCE: yes
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - uses: r-lib/actions/setup-r@v1
+ - uses: r-lib/actions/setup-pandoc@v2
+
+ - uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
+ http-user-agent: ${{ matrix.config.http-user-agent }}
+ use-public-rspm: true
- - uses: r-lib/actions/setup-pandoc@v1
-
- - name: Query dependencies
- run: |
- install.packages('remotes')
- saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
- writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
- shell: Rscript {0}
-
- - name: Restore R package cache
- if: runner.os != 'Windows'
- uses: actions/cache@v2
+ - uses: r-lib/actions/setup-r-dependencies@v2
with:
- path: ${{ env.R_LIBS_USER }}
- key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }}
- restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-
-
- - name: Install system dependencies
- if: runner.os == 'Linux'
- run: |
- while read -r cmd
- do
- eval sudo $cmd
- done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')
-
- - name: Install dependencies
- run: |
- remotes::install_deps(dependencies = TRUE)
- remotes::install_cran("rcmdcheck")
- shell: Rscript {0}
-
- - name: Check
- env:
- _R_CHECK_CRAN_INCOMING_REMOTE_: false
- run: |
- options(crayon.enabled = TRUE)
- rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check")
- shell: Rscript {0}
+ extra-packages: any::rcmdcheck
+ needs: check
- - name: Upload check results
- if: failure()
- uses: actions/upload-artifact@main
+ - uses: r-lib/actions/check-r-package@v2
with:
- name: ${{ runner.os }}-r${{ matrix.config.r }}-results
- path: check
+ upload-snapshots: true
+ build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
diff --git a/.gitignore b/.gitignore
index 4648ca7..2e7bfe1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ test.R
docs/
*.xlsx
.claude/
+check/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..fddd887
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,105 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Package Overview
+
+rtodoist is an R package that wraps the Todoist API v1 for programmatic management of projects, tasks, sections, and collaborators. The package uses secure token storage via keyring.
+
+## Common Commands
+
+```r
+# Install dependencies
+remotes::install_deps(dependencies = TRUE)
+
+# Generate documentation (NAMESPACE and man/*.Rd files)
+devtools::document()
+
+# Run all tests
+devtools::test()
+
+# Run a single test file
+testthat::test_file("tests/testthat/test-tasks.R")
+
+# Run R CMD CHECK
+rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning")
+
+# Build package
+R CMD build .
+```
+
+## Architecture
+
+### Two API Communication Patterns
+
+1. **Sync API** (`call_api()` in `R/utils.R`): Used for write operations via `/api/v1/sync`. Commands are sent as JSON with UUID-based temp_id generation.
+
+2. **REST API** (`call_api_rest()` in `R/utils.R`): Used for read operations with cursor-based pagination support.
+
+### Key Design Patterns
+
+- **Lazy parameter evaluation**: Functions accept both `project_name` or `project_id`. Default parameters call lookup functions (e.g., `get_project_id()`), so use `force()` before API calls.
+
+- **Pipe-friendly returns**: Write functions return IDs invisibly for chaining:
+ ```r
+ add_project("test") %>% add_tasks_in_project(c("task1", "task2"))
+ ```
+
+- **Vector parameter matching**: `add_tasks_in_project()` accepts vectors for tasks, responsible users, due dates, and sections. Single values are recycled; otherwise lengths must match.
+
+### Source File Organization
+
+| File | Purpose |
+|------|---------|
+| `R/projects.R` | Project CRUD operations |
+| `R/tasks.R` | Task operations (add, update, get, assign) |
+| `R/section.R` | Section management |
+| `R/users.R` | User/collaboration management |
+| `R/token.R` | API token management via keyring |
+| `R/utils.R` | `call_api()`, `call_api_rest()`, `escape_json()`, `random_key()` |
+| `R/all_objects.R` | `get_all_data()` for full sync |
+| `R/clean_tools.R` | Data cleaning utilities |
+
+### Utility Functions to Reuse
+
+- `call_api(commands, token)`: Sync API calls with JSON command batch
+- `call_api_rest(endpoint, params, token)`: REST API with pagination
+- `escape_json(x)`: Escape special characters for JSON strings
+- `random_key()`: Generate UUIDs for Sync API commands
+- `clean_due()`, `clean_section()`: Normalize NULL/"" to "null" for API
+
+## Testing
+
+Uses testthat v3 with two test strategies:
+
+1. **Unit tests** (`test-unit-logic.R`): Tests pure logic functions using fixture data from `tests/testthat/fixtures/`. No API token required.
+
+2. **Integration tests**: Require a valid API token. Skip automatically on CI/CRAN via helpers in `tests/testthat/helper.R`.
+
+### Test Helpers
+
+```r
+skip_if_no_token() # Skip if no API token available
+skip_on_ci_or_cran() # Skip on CI environments
+```
+
+## API Token Setup
+
+For testing, get your token from https://www.todoist.com/prefs/integrations/developer then:
+```r
+rtodoist::set_todoist_api_token("your_token")
+```
+
+## Adding New API Functions
+
+Follow the existing pattern:
+```r
+add_xxx <- function(name, ..., verbose = TRUE, token = get_todoist_api_token()) {
+ force(token)
+ # Build JSON command using glue() and escape_json()
+ # Call via call_api() or call_api_rest()
+ # Return ID invisibly for pipe chaining
+}
+```
+
+See `PLAN_IMPLEMENTATION.md` for the roadmap of missing features (labels, comments, filters, reminders, etc.).
diff --git a/DESCRIPTION b/DESCRIPTION
index fbaba64..281bf57 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,27 +1,27 @@
Package: rtodoist
Title: Create and Manage Todolist using 'Todoist.com' API
-Version: 0.3
+Version: 0.4.0
Authors@R: c(
person("Cervan", "Girard", , "cervan@thinkr.fr", role = "aut",
- comment = c(ORCID = "0000-0002-4816-4624")),
+ comment = c(ORCID = "0000-0002-4816-4624", note = "previous maintainer")),
person("Vincent", "Guyader", , "vincent@thinkr.fr", role = c("cre", "aut"),
comment = c(ORCID = "0000-0003-0671-9270")),
person("ThinkR", role = c("cph", "fnd"))
)
Description: Allows you to interact with the API of the "Todoist"
- platform. 'Todoist' provides an online task
+ platform. 'Todoist' provides an online task
manager service for teams.
License: MIT + file LICENSE
URL: https://github.com/ThinkR-open/rtodoist
BugReports: https://github.com/ThinkR-open/rtodoist/issues
Depends:
R (>= 3.5.0)
-Imports:
+Imports:
+ curl,
digest,
dplyr,
getPass,
glue,
- httr,
httr2,
keyring,
magrittr,
@@ -31,11 +31,9 @@ Imports:
stringr,
utils
Suggests:
- httptest2,
+ covr,
jsonlite,
knitr,
- lubridate,
- mockery,
rmarkdown,
testthat (>= 3.0.0)
Config/testthat/edition: 3
diff --git a/LICENSE b/LICENSE
index f424ab0..850f78b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,2 +1,2 @@
-YEAR: 2019
-COPYRIGHT HOLDER: Cervan Girard
+YEAR: 2019-2026
+COPYRIGHT HOLDER: ThinkR
diff --git a/NAMESPACE b/NAMESPACE
index c4ced6c..b0a4e49 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,27 +1,91 @@
# Generated by roxygen2: do not edit by hand
export("%>%")
+export(accept_invitation)
+export(add_comment)
+export(add_filter)
+export(add_label)
export(add_project)
+export(add_reminder)
export(add_responsible_to_task)
export(add_section)
export(add_tasks_in_project)
export(add_tasks_in_project_from_df)
export(add_user_in_project)
export(add_users_in_project)
+export(archive_project)
+export(archive_section)
export(ask_todoist_api_token)
+export(close_task)
+export(delete_collaborator)
+export(delete_comment)
+export(delete_filter)
+export(delete_invitation)
+export(delete_label)
+export(delete_project)
+export(delete_reminder)
+export(delete_section)
+export(delete_task)
export(delete_todoist_api_token)
+export(delete_upload)
+export(download_backup)
+export(export_template)
+export(get_activity_logs)
export(get_all_data)
+export(get_all_filters)
+export(get_all_labels)
export(get_all_projects)
+export(get_all_reminders)
+export(get_all_sections)
export(get_all_users)
+export(get_all_workspaces)
+export(get_archived_projects)
+export(get_backups)
+export(get_comment)
+export(get_comments)
+export(get_completed_tasks)
+export(get_filter)
+export(get_filter_id)
+export(get_label)
+export(get_label_id)
+export(get_productivity_stats)
+export(get_project)
export(get_project_id)
+export(get_section)
export(get_section_id)
+export(get_shared_labels)
+export(get_task)
export(get_tasks)
+export(get_tasks_by_filter)
export(get_tasks_of_project)
export(get_todoist_api_token)
+export(get_user_info)
export(get_users_in_project)
+export(get_workspace_users)
+export(import_template)
+export(invite_to_workspace)
+export(leave_workspace)
+export(move_section)
+export(move_task)
export(open_todoist_website_profile)
+export(quick_add_task)
+export(reject_invitation)
+export(remove_shared_label)
+export(rename_shared_label)
+export(reopen_task)
export(set_todoist_api_token)
+export(unarchive_project)
+export(unarchive_section)
+export(update_comment)
+export(update_filter)
+export(update_label)
+export(update_project)
+export(update_reminder)
+export(update_section)
+export(update_task)
export(update_todoist_api_token)
+export(update_workspace)
+export(upload_file)
import(glue)
import(keyring)
import(purrr)
@@ -37,17 +101,22 @@ importFrom(dplyr,pull)
importFrom(getPass,getPass)
importFrom(glue,glue)
importFrom(glue,glue_collapse)
-importFrom(httr,POST)
-importFrom(httr,content)
+importFrom(httr2,req_body_json)
importFrom(httr2,req_body_multipart)
+importFrom(httr2,req_error)
importFrom(httr2,req_headers)
+importFrom(httr2,req_method)
importFrom(httr2,req_perform)
importFrom(httr2,req_url_query)
importFrom(httr2,request)
importFrom(httr2,resp_body_json)
+importFrom(httr2,resp_body_raw)
+importFrom(httr2,resp_body_string)
+importFrom(httr2,resp_status)
importFrom(magrittr,"%>%")
importFrom(purrr,flatten)
importFrom(purrr,is_empty)
+importFrom(purrr,keep)
importFrom(purrr,map)
importFrom(purrr,map_df)
importFrom(purrr,map_dfr)
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 0000000..29adc92
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,149 @@
+# rtodoist 0.4.0
+
+## Breaking changes
+* API functions now use `req_error()` from httr2 for proper HTTP error handling
+* `unarchive_project()` no longer accepts unused `project_name` parameter
+* Removed unused `httr` dependency (fully migrated to httr2)
+
+## New features
+
+### Labels module (new)
+* `add_label()`: Create a new label
+* `get_label()`: Get a single label by ID
+* `get_all_labels()`: Get all labels
+* `get_label_id()`: Get label ID by name
+* `update_label()`: Update an existing label
+* `delete_label()`: Delete a label
+* `get_shared_labels()`: Get all shared labels
+* `rename_shared_label()`: Rename a shared label
+* `remove_shared_label()`: Remove a shared label
+
+### Comments module (new)
+* `add_comment()`: Add a comment to a task or project
+* `get_comment()`: Get a single comment by ID
+* `get_comments()`: Get all comments for a task or project
+* `update_comment()`: Update an existing comment
+* `delete_comment()`: Delete a comment
+
+### Filters module (new)
+* `add_filter()`: Create a new filter
+* `get_filter()`: Get a single filter by ID
+* `get_all_filters()`: Get all filters
+* `get_filter_id()`: Get filter ID by name
+* `update_filter()`: Update an existing filter
+* `delete_filter()`: Delete a filter
+
+### Reminders module (new)
+* `add_reminder()`: Add a reminder to a task
+* `get_all_reminders()`: Get all reminders
+* `update_reminder()`: Update an existing reminder
+* `delete_reminder()`: Delete a reminder
+
+### Workspaces module (new)
+* `get_all_workspaces()`: Get all workspaces
+* `get_workspace_users()`: Get users in a workspace
+* `update_workspace()`: Update workspace settings
+* `invite_to_workspace()`: Invite a user to a workspace
+* `leave_workspace()`: Leave a workspace
+
+### Activity and stats (new)
+* `get_activity_logs()`: Get activity logs
+* `get_productivity_stats()`: Get user productivity statistics
+* `get_user_info()`: Get current user information
+
+### Backups module (new)
+* `get_backups()`: List available backups
+* `download_backup()`: Download a backup file
+
+### Templates module (new)
+* `export_template()`: Export a project as template
+* `import_template()`: Import a template into a project
+
+### Uploads module (new)
+* `upload_file()`: Upload a file attachment
+* `delete_upload()`: Delete an uploaded file
+
+### Projects enhancements
+* `get_project()`: Get a single project by ID
+* `update_project()`: Update project name, color, favorite status, or view style
+* `delete_project()`: Delete a project
+* `archive_project()`: Archive a project
+* `unarchive_project()`: Unarchive a project
+* `get_archived_projects()`: Get all archived projects
+
+### Sections enhancements
+* `get_section()`: Get a single section by ID
+* `get_all_sections()`: Get all sections across projects
+* `update_section()`: Rename a section
+* `delete_section()`: Delete a section
+* `move_section()`: Move a section to another project
+* `archive_section()`: Archive a section
+* `unarchive_section()`: Unarchive a section
+
+### Tasks enhancements
+* `get_task()`: Get a single task by ID
+* `update_task()`: Update task content, due date, priority, labels, or description
+* `delete_task()`: Delete a task
+* `close_task()`: Mark a task as complete
+* `reopen_task()`: Reopen a completed task
+* `move_task()`: Move a task to another project, section, or parent
+* `get_completed_tasks()`: Get completed tasks with date filtering
+* `get_tasks_by_filter()`: Get tasks using a filter query
+* `quick_add_task()`: Quick add a task with natural language parsing
+
+### Collaboration enhancements
+* `delete_collaborator()`: Remove a collaborator from a project
+* `accept_invitation()`: Accept a project invitation
+* `reject_invitation()`: Reject a project invitation
+* `delete_invitation()`: Delete a pending invitation
+
+## Improvements
+* Added `escape_json()` to all Sync API commands for proper JSON escaping
+* Added token validation in `call_api()` and `call_api_rest()` with clear error messages
+* API base URLs now defined as package constants (`TODOIST_SYNC_URL`, `TODOIST_REST_URL`)
+* Empty data.frames now return consistent column structure with non-empty results
+* Standardized error handling with `req_error()` across all REST endpoints
+* Replaced `print()` with `message()` for user-facing output (CRAN compliance)
+* Updated GitHub Actions workflow to use modern action versions (v2/v4)
+* Removed debug message from `call_api()` function
+* Added comprehensive test coverage for all new modules
+* Added `skip_if_test_project_missing()` helper for more robust integration tests
+
+## Bug fixes
+* Fixed `move_task()` to properly escape IDs preventing JSON injection
+* Fixed templates export/import functions failing due to glue object type issue
+* Fixed `get_comments()` empty result missing `task_id` and `project_id` columns
+* Fixed `get_all_reminders()` empty result missing `due_date` and `minute_offset` columns
+* Fixed `get_activity_logs()` empty result missing `initiator_id`, `parent_project_id`, `parent_item_id` columns
+* Fixed `get_tasks_by_filter()` empty result missing `due_date` column
+* Fixed `get_archived_projects()` empty result missing `color` and `is_favorite` columns
+* Fixed `get_all_sections()` empty result missing `order` column
+* Fixed `get_all_workspaces()` empty result missing `is_default` column
+* Fixed `get_workspace_users()` empty result missing `role` column
+* Fixed `quick_add_task()` using hardcoded URL instead of `TODOIST_REST_URL`
+* Added missing `req_error()` to `quick_add_task()` and `upload_file()`
+
+## Internal
+* Removed unused `httptest2` and `mockery` from Suggests
+* Removed unused `lubridate` from Suggests
+* Cleaned up mocking test infrastructure
+* Added `@return` tags to all exported functions for CRAN compliance
+
+# rtodoist 0.3.0
+
+* Added pagination support for REST API endpoints
+* Fixed JSON escaping for special characters
+* Fixed string ID handling for API v1 compatibility
+* Added comprehensive testthat test suite
+
+# rtodoist 0.2.1
+
+* Migration to Todoist API v1
+* Moved from httr to httr2
+
+# rtodoist 0.2.0
+
+* Initial CRAN release
+* Projects, tasks, sections management
+* User collaboration features
+* Secure token storage via keyring
diff --git a/R/activity.R b/R/activity.R
new file mode 100644
index 0000000..8b639c5
--- /dev/null
+++ b/R/activity.R
@@ -0,0 +1,81 @@
+#' Get activity logs
+#'
+#' @param object_type filter by object type (e.g., "project", "item", "note")
+#' @param object_id filter by specific object id
+#' @param event_type filter by event type (e.g., "added", "updated", "completed")
+#' @param parent_project_id filter by parent project id
+#' @param parent_item_id filter by parent item id
+#' @param initiator_id filter by user who initiated the action
+#' @param since return events since this date (format: YYYY-MM-DDTHH:MM:SS)
+#' @param until return events until this date (format: YYYY-MM-DDTHH:MM:SS)
+#' @param limit maximum number of events to return (default 30, max 100)
+#' @param offset offset for pagination
+#' @param token todoist API token
+#'
+#' @return tibble of activity events
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_activity_logs()
+#' get_activity_logs(object_type = "item", event_type = "completed")
+#' }
+get_activity_logs <- function(object_type = NULL,
+ object_id = NULL,
+ event_type = NULL,
+ parent_project_id = NULL,
+ parent_item_id = NULL,
+ initiator_id = NULL,
+ since = NULL,
+ until = NULL,
+ limit = 30,
+ offset = 0,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ params <- list(limit = limit, offset = offset)
+ if (!is.null(object_type)) params$object_type <- object_type
+ if (!is.null(object_id)) params$object_id <- object_id
+ if (!is.null(event_type)) params$event_type <- event_type
+ if (!is.null(parent_project_id)) params$parent_project_id <- parent_project_id
+ if (!is.null(parent_item_id)) params$parent_item_id <- parent_item_id
+ if (!is.null(initiator_id)) params$initiator_id <- initiator_id
+ if (!is.null(since)) params$since <- since
+ if (!is.null(until)) params$until <- until
+
+ response <- do.call(call_api_rest, c(list("activity_logs", token = token), params))
+
+ events <- response$events
+ if (is.null(events)) {
+ events <- response$results
+ }
+
+ if (is.null(events) || length(events) == 0) {
+ return(data.frame(
+ id = character(),
+ object_type = character(),
+ object_id = character(),
+ event_type = character(),
+ event_date = character(),
+ initiator_id = character(),
+ parent_project_id = character(),
+ parent_item_id = character(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(events, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ object_type = x$object_type %||% NA_character_,
+ object_id = x$object_id %||% NA_character_,
+ event_type = x$event_type %||% NA_character_,
+ event_date = x$event_date %||% NA_character_,
+ initiator_id = x$initiator_id %||% NA_character_,
+ parent_project_id = x$parent_project_id %||% NA_character_,
+ parent_item_id = x$parent_item_id %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+}
diff --git a/R/backups.R b/R/backups.R
new file mode 100644
index 0000000..88194dd
--- /dev/null
+++ b/R/backups.R
@@ -0,0 +1,92 @@
+#' Get list of backups
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of available backups
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_backups()
+#' }
+get_backups <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ response <- call_api_rest("backups", token = token)
+
+ backups <- response$results
+ if (is.null(backups)) {
+ backups <- response
+ }
+
+ if (is.null(backups) || length(backups) == 0) {
+ return(data.frame(
+ version = character(),
+ url = character(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(backups, function(x) {
+ data.frame(
+ version = x$version %||% NA_character_,
+ url = x$url %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Download a backup
+#'
+#' @param version version of the backup to download (from get_backups())
+#' @param output_file path where to save the backup file
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return path to the saved file (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_perform resp_body_raw req_error resp_status
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' backups <- get_backups()
+#' download_backup(backups$version[1], "backup.zip")
+#' }
+download_backup <- function(version,
+ output_file,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ # Get the backup URL
+ backups <- get_backups(token = token)
+ backup <- backups[backups$version == version, ]
+
+ if (nrow(backup) == 0) {
+ stop("Backup version not found: ", version)
+ }
+
+ backup_url <- backup$url[1]
+
+ if (verbose) {
+ message(glue::glue("Downloading backup version {version}"))
+ }
+
+ response <- request(backup_url) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}")
+ ) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ content <- resp_body_raw(response)
+ writeBin(content, output_file)
+
+ if (verbose) {
+ message(glue::glue("Backup saved to {output_file}"))
+ }
+
+ invisible(output_file)
+}
diff --git a/R/comments.R b/R/comments.R
new file mode 100644
index 0000000..147ae04
--- /dev/null
+++ b/R/comments.R
@@ -0,0 +1,211 @@
+#' Add a comment to a task or project
+#'
+#' @param content content of the comment
+#' @param task_id id of the task (either task_id or project_id required)
+#' @param project_id id of the project (either task_id or project_id required)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the new comment
+#' @export
+#' @importFrom glue glue
+#' @importFrom httr2 request req_headers req_body_json req_perform resp_body_json req_error resp_status
+#'
+#' @examples
+#' \dontrun{
+#' add_comment(content = "This is a comment", task_id = "12345")
+#' add_comment(content = "Project comment", project_id = "67890")
+#' }
+add_comment <- function(content,
+ task_id = NULL,
+ project_id = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (is.null(task_id) && is.null(project_id)) {
+ stop("Either task_id or project_id must be provided")
+ }
+
+ if (verbose) {
+ if (!is.null(task_id)) {
+ message(glue::glue("Adding comment to task {task_id}"))
+ } else {
+ message(glue::glue("Adding comment to project {project_id}"))
+ }
+ }
+
+ body <- list(content = content)
+ if (!is.null(task_id)) {
+ body$task_id <- task_id
+ }
+ if (!is.null(project_id)) {
+ body$project_id <- project_id
+ }
+
+ result <- request(paste0(TODOIST_REST_URL, "comments")) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}"),
+ "Content-Type" = "application/json"
+ ) %>%
+ req_body_json(body) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform() %>%
+ resp_body_json()
+
+ invisible(result$id)
+}
+
+#' Get comments
+#'
+#' @param task_id id of the task (either task_id or project_id required)
+#' @param project_id id of the project (either task_id or project_id required)
+#' @param token todoist API token
+#'
+#' @return tibble of comments
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_comments(task_id = "12345")
+#' get_comments(project_id = "67890")
+#' }
+get_comments <- function(task_id = NULL,
+ project_id = NULL,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (is.null(task_id) && is.null(project_id)) {
+ stop("Either task_id or project_id must be provided")
+ }
+
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ if (!is.null(task_id)) {
+ response <- call_api_rest("comments", token = token, task_id = task_id, cursor = cursor)
+ } else {
+ response <- call_api_rest("comments", token = token, project_id = project_id, cursor = cursor)
+ }
+
+ all_results <- c(all_results, response$results)
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(
+ id = character(),
+ content = character(),
+ task_id = character(),
+ project_id = character(),
+ posted_at = character(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(all_results, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ content = x$content %||% NA_character_,
+ task_id = x$task_id %||% NA_character_,
+ project_id = x$project_id %||% NA_character_,
+ posted_at = x$posted_at %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Get a single comment by ID
+#'
+#' @param comment_id id of the comment
+#' @param token todoist API token
+#'
+#' @return list with comment details
+#' @export
+#'
+#' @examples
+#' \dontrun{
+#' get_comment("12345")
+#' }
+get_comment <- function(comment_id, token = get_todoist_api_token()) {
+ force(token)
+ call_api_rest(glue("comments/{comment_id}"), token = token)
+}
+
+#' Update a comment
+#'
+#' @param comment_id id of the comment
+#' @param content new content for the comment
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated comment (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_body_json req_perform req_method req_error resp_status
+#'
+#' @examples
+#' \dontrun{
+#' update_comment("12345", content = "Updated comment")
+#' }
+update_comment <- function(comment_id,
+ content,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Updating comment {comment_id}"))
+ }
+
+ request(glue("{TODOIST_REST_URL}comments/{comment_id}")) %>%
+ req_method("POST") %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}"),
+ "Content-Type" = "application/json"
+ ) %>%
+ req_body_json(list(content = content)) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ invisible(comment_id)
+}
+
+#' Delete a comment
+#'
+#' @param comment_id id of the comment
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_perform req_method req_error resp_status
+#'
+#' @examples
+#' \dontrun{
+#' delete_comment("12345")
+#' }
+delete_comment <- function(comment_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting comment {comment_id}"))
+ }
+
+ request(glue("{TODOIST_REST_URL}comments/{comment_id}")) %>%
+ req_method("DELETE") %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}")
+ ) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ invisible(NULL)
+}
diff --git a/R/filters.R b/R/filters.R
new file mode 100644
index 0000000..7aaf6ff
--- /dev/null
+++ b/R/filters.R
@@ -0,0 +1,320 @@
+#' Add a new filter
+#'
+#' @param name name of the filter
+#' @param query filter query string (e.g., "today | overdue", "p1 & #Work")
+#' @param color color of the filter
+#' @param is_favorite boolean to mark as favorite
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the new filter
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' add_filter("Urgent Today", query = "today & p1")
+#' add_filter("Work Tasks", query = "#Work", color = "blue")
+#' }
+add_filter <- function(name,
+ query,
+ color = NULL,
+ is_favorite = FALSE,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Creating filter: {name}"))
+ }
+
+ args_parts <- c(
+ glue('"name": "{escape_json(name)}"'),
+ glue('"query": "{escape_json(query)}"')
+ )
+
+ if (!is.null(color)) {
+ args_parts <- c(args_parts, glue('"color": "{color}"'))
+ }
+ if (is_favorite) {
+ args_parts <- c(args_parts, '"is_favorite": true')
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+ temp_id <- random_key()
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["filters"]',
+ commands = glue('[{{"type": "filter_add", "temp_id": "{temp_id}", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ filter_id <- result$temp_id_mapping[[temp_id]]
+ invisible(filter_id)
+}
+
+#' Get all filters
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of all filters
+#' @export
+#' @importFrom purrr map_dfr pluck
+#'
+#' @examples
+#' \dontrun{
+#' get_all_filters()
+#' }
+get_all_filters <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["filters"]'
+ )
+
+ filters <- result %>% pluck("filters")
+
+ if (is.null(filters) || length(filters) == 0) {
+ return(data.frame(
+ id = character(),
+ name = character(),
+ query = character(),
+ color = character(),
+ is_favorite = logical(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(filters, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ query = x$query %||% NA_character_,
+ color = x$color %||% NA_character_,
+ is_favorite = x$is_favorite %||% FALSE,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Get a single filter by ID
+#'
+#' @param filter_id id of the filter
+#' @param token todoist API token
+#'
+#' @return list with filter details
+#' @export
+#' @importFrom purrr pluck keep
+#'
+#' @examples
+#' \dontrun{
+#' get_filter("12345")
+#' }
+get_filter <- function(filter_id, token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["filters"]'
+ )
+
+ filters <- result %>% pluck("filters")
+
+ if (is.null(filters) || length(filters) == 0) {
+ return(NULL)
+ }
+
+ matching <- keep(filters, ~ .x$id == filter_id)
+ if (length(matching) == 0) {
+ return(NULL)
+ }
+
+ matching[[1]]
+}
+
+#' Get filter id by name
+#'
+#' @param filter_name name of the filter
+#' @param all_filters result of get_all_filters (optional)
+#' @param token todoist API token
+#'
+#' @return id of the filter
+#' @export
+#' @importFrom dplyr filter pull
+#'
+#' @examples
+#' \dontrun{
+#' get_filter_id("Urgent Today")
+#' }
+get_filter_id <- function(filter_name,
+ all_filters = get_all_filters(token = token),
+ token = get_todoist_api_token()) {
+ force(token)
+
+ id <- all_filters %>%
+ dplyr::filter(name == filter_name) %>%
+ pull(id)
+
+ if (length(id) == 0) {
+ stop("Filter not found: ", filter_name)
+ }
+
+ id[1]
+}
+
+#' Update a filter
+#'
+#' @param filter_id id of the filter
+#' @param filter_name name of the filter (for lookup if filter_id not provided)
+#' @param new_name new name for the filter
+#' @param query new query string
+#' @param color new color
+#' @param is_favorite boolean to mark as favorite
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated filter (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_filter(filter_name = "Urgent Today", query = "today & p1 & #Work")
+#' }
+update_filter <- function(filter_id = get_filter_id(filter_name = filter_name, token = token),
+ filter_name,
+ new_name = NULL,
+ query = NULL,
+ color = NULL,
+ is_favorite = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(filter_id)
+ force(token)
+
+ args_parts <- c(glue('"id": "{filter_id}"'))
+
+ if (!is.null(new_name)) {
+ args_parts <- c(args_parts, glue('"name": "{escape_json(new_name)}"'))
+ }
+ if (!is.null(query)) {
+ args_parts <- c(args_parts, glue('"query": "{escape_json(query)}"'))
+ }
+ if (!is.null(color)) {
+ args_parts <- c(args_parts, glue('"color": "{color}"'))
+ }
+ if (!is.null(is_favorite)) {
+ fav_val <- ifelse(is_favorite, "true", "false")
+ args_parts <- c(args_parts, glue('"is_favorite": {fav_val}'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+
+ if (verbose) {
+ message(glue::glue("Updating filter {filter_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "filter_update", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ invisible(filter_id)
+}
+
+#' Delete a filter
+#'
+#' @param filter_id id of the filter
+#' @param filter_name name of the filter (for lookup if filter_id not provided)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_filter(filter_name = "Urgent Today")
+#' }
+delete_filter <- function(filter_id = get_filter_id(filter_name = filter_name, token = token),
+ filter_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(filter_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting filter {filter_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "filter_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(filter_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Get tasks by filter query
+#'
+#' @param query filter query string (e.g., "today", "p1 & #Work")
+#' @param token todoist API token
+#'
+#' @return tibble of tasks matching the filter
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_tasks_by_filter("today")
+#' get_tasks_by_filter("p1 & #Work")
+#' }
+get_tasks_by_filter <- function(query,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ response <- call_api_rest("tasks/filter", token = token, query = query, cursor = cursor)
+ all_results <- c(all_results, response$results)
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(
+ id = character(),
+ content = character(),
+ project_id = character(),
+ section_id = character(),
+ priority = integer(),
+ due_date = character(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(all_results, function(x) {
+ due_date <- if (!is.null(x$due)) x$due$date else NA_character_
+ data.frame(
+ id = x$id %||% NA_character_,
+ content = x$content %||% NA_character_,
+ project_id = x$project_id %||% NA_character_,
+ section_id = x$section_id %||% NA_character_,
+ priority = x$priority %||% NA_integer_,
+ due_date = due_date,
+ stringsAsFactors = FALSE
+ )
+ })
+}
diff --git a/R/labels.R b/R/labels.R
new file mode 100644
index 0000000..7c24413
--- /dev/null
+++ b/R/labels.R
@@ -0,0 +1,383 @@
+#' Add a new label
+#'
+#' @param name name of the label
+#' @param color color of the label
+#' @param is_favorite boolean to mark as favorite
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the new label
+#' @export
+#' @importFrom glue glue
+#' @importFrom purrr pluck
+#'
+#' @examples
+#' \dontrun{
+#' add_label("urgent")
+#' add_label("work", color = "red")
+#' }
+add_label <- function(name,
+ color = NULL,
+ is_favorite = FALSE,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Creating label: {name}"))
+ }
+
+ # Check if label already exists
+ existing_labels <- get_all_labels(token = token)
+ if (name %in% existing_labels$name) {
+ if (verbose) {
+ message("This label already exists")
+ }
+ return(existing_labels$id[existing_labels$name == name][1])
+ }
+
+ args_parts <- c(glue('"name": "{escape_json(name)}"'))
+
+ if (!is.null(color)) {
+ args_parts <- c(args_parts, glue('"color": "{color}"'))
+ }
+ if (is_favorite) {
+ args_parts <- c(args_parts, '"is_favorite": true')
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+ temp_id <- random_key()
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["labels"]',
+ commands = glue('[{{"type": "label_add", "temp_id": "{temp_id}", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ # Get the label id from temp_id_mapping
+ label_id <- result$temp_id_mapping[[temp_id]]
+ if (is.null(label_id)) {
+ # Fallback: get from labels list
+ labels <- get_all_labels(token = token)
+ label_id <- labels$id[labels$name == name][1]
+ }
+
+ invisible(label_id)
+}
+
+#' Get all labels
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of all labels
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_all_labels()
+#' }
+get_all_labels <- function(token = get_todoist_api_token()) {
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ response <- call_api_rest("labels", token = token, cursor = cursor)
+ all_results <- c(all_results, response$results)
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(
+ id = character(),
+ name = character(),
+ color = character(),
+ is_favorite = logical(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(all_results, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ color = x$color %||% NA_character_,
+ is_favorite = x$is_favorite %||% FALSE,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Get a single label by ID
+#'
+#' @param label_id id of the label
+#' @param token todoist API token
+#'
+#' @return list with label details
+#' @export
+#'
+#' @examples
+#' \dontrun{
+#' get_label("12345")
+#' }
+get_label <- function(label_id, token = get_todoist_api_token()) {
+ force(token)
+ call_api_rest(glue("labels/{label_id}"), token = token)
+}
+
+#' Get label id by name
+#'
+#' @param label_name name of the label
+#' @param all_labels result of get_all_labels (optional)
+#' @param token todoist API token
+#' @param create boolean create label if needed
+#'
+#' @return id of the label
+#' @export
+#' @importFrom dplyr filter pull
+#'
+#' @examples
+#' \dontrun{
+#' get_label_id("urgent")
+#' }
+get_label_id <- function(label_name,
+ all_labels = get_all_labels(token = token),
+ token = get_todoist_api_token(),
+ create = TRUE) {
+ force(token)
+
+ id <- all_labels %>%
+ filter(name == label_name) %>%
+ pull(id)
+
+ if (length(id) == 0) {
+ if (create) {
+ message("Label doesn't exist, creating it")
+ id <- add_label(name = label_name, token = token, verbose = FALSE)
+ } else {
+ stop("Label not found: ", label_name)
+ }
+ }
+
+ id[1]
+}
+
+#' Update a label
+#'
+#' @param label_id id of the label
+#' @param label_name name of the label (for lookup if label_id not provided)
+#' @param new_name new name for the label
+#' @param color new color for the label
+#' @param is_favorite boolean to mark as favorite
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated label (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_label(label_name = "urgent", new_name = "very_urgent")
+#' }
+update_label <- function(label_id = get_label_id(label_name = label_name, token = token, create = FALSE),
+ label_name,
+ new_name = NULL,
+ color = NULL,
+ is_favorite = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(label_id)
+ force(token)
+
+ args_parts <- c(glue('"id": "{label_id}"'))
+
+ if (!is.null(new_name)) {
+ args_parts <- c(args_parts, glue('"name": "{escape_json(new_name)}"'))
+ }
+ if (!is.null(color)) {
+ args_parts <- c(args_parts, glue('"color": "{color}"'))
+ }
+ if (!is.null(is_favorite)) {
+ fav_val <- ifelse(is_favorite, "true", "false")
+ args_parts <- c(args_parts, glue('"is_favorite": {fav_val}'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+
+ if (verbose) {
+ message(glue::glue("Updating label {label_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "label_update", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ invisible(label_id)
+}
+
+#' Delete a label
+#'
+#' @param label_id id of the label
+#' @param label_name name of the label (for lookup if label_id not provided)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_label(label_name = "urgent")
+#' }
+delete_label <- function(label_id = get_label_id(label_name = label_name, token = token, create = FALSE),
+ label_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(label_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting label {label_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "label_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(label_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Get shared labels
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of shared labels
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_shared_labels()
+#' }
+get_shared_labels <- function(token = get_todoist_api_token()) {
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ response <- call_api_rest("labels/shared", token = token, cursor = cursor)
+ # Shared labels endpoint returns a list directly
+ if (is.null(response$results)) {
+ all_results <- response
+ break
+ }
+ all_results <- c(all_results, response$results)
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(name = character(), stringsAsFactors = FALSE))
+ }
+
+ # Shared labels may just be a list of names
+ if (is.character(all_results[[1]])) {
+ return(data.frame(name = unlist(all_results), stringsAsFactors = FALSE))
+ }
+
+ map_dfr(all_results, function(x) {
+ if (is.character(x)) {
+ data.frame(name = x, stringsAsFactors = FALSE)
+ } else {
+ data.frame(
+ name = x$name %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ }
+ })
+}
+
+#' Rename a shared label
+#'
+#' @param old_name current name of the shared label
+#' @param new_name new name for the shared label
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_body_json req_perform req_error resp_status
+#'
+#' @examples
+#' \dontrun{
+#' rename_shared_label("old_name", "new_name")
+#' }
+rename_shared_label <- function(old_name,
+ new_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Renaming shared label from '{old_name}' to '{new_name}'"))
+ }
+
+ request(paste0(TODOIST_REST_URL, "labels/shared/rename")) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}"),
+ "Content-Type" = "application/json"
+ ) %>%
+ req_body_json(list(name = old_name, new_name = new_name)) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ invisible(NULL)
+}
+
+#' Remove a shared label
+#'
+#' @param name name of the shared label to remove
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_body_json req_perform req_error resp_status
+#'
+#' @examples
+#' \dontrun{
+#' remove_shared_label("label_name")
+#' }
+remove_shared_label <- function(name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Removing shared label: {name}"))
+ }
+
+ request(paste0(TODOIST_REST_URL, "labels/shared/remove")) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}"),
+ "Content-Type" = "application/json"
+ ) %>%
+ req_body_json(list(name = name)) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ invisible(NULL)
+}
diff --git a/R/open_todoist_website.R b/R/open_todoist_website.R
index 693a9bc..46296e6 100644
--- a/R/open_todoist_website.R
+++ b/R/open_todoist_website.R
@@ -11,7 +11,7 @@
#' open_todoist_website_profile()
open_todoist_website_profile <- function(verbose = TRUE) {
if (verbose) {
- message("opening https://todoist.com/prefs/integrations")
+ message("opening https://www.todoist.com/prefs/integrations")
}
- browseURL("https://todoist.com/prefs/integrations")
+ browseURL("https://www.todoist.com/prefs/integrations")
}
diff --git a/R/projects.R b/R/projects.R
index 65814ae..988da6b 100644
--- a/R/projects.R
+++ b/R/projects.R
@@ -65,7 +65,6 @@ get_project_id <- function(
#' @return id of the new project
#' @export
#' @importFrom purrr flatten map
-#' @importFrom httr content
#' @importFrom glue glue
#' @examples
#' \dontrun{
@@ -106,3 +105,223 @@ add_project <- function(project_name,
get_project_id(all_projects = .,project_name = project_name,token = token)
}
}
+
+#' Update a project
+#'
+#' @param project_id id of the project
+#' @param project_name name of the project (for lookup if project_id not provided)
+#' @param new_name new name for the project
+#' @param color new color for the project
+#' @param is_favorite boolean to mark as favorite
+#' @param view_style view style ("list" or "board")
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated project (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_project(project_name = "my_proj", new_name = "new_name")
+#' }
+update_project <- function(project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ new_name = NULL,
+ color = NULL,
+ is_favorite = NULL,
+ view_style = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ args_parts <- c(glue('"id": "{project_id}"'))
+
+ if (!is.null(new_name)) {
+ args_parts <- c(args_parts, glue('"name": "{escape_json(new_name)}"'))
+ }
+ if (!is.null(color)) {
+ args_parts <- c(args_parts, glue('"color": "{color}"'))
+ }
+ if (!is.null(is_favorite)) {
+ fav_val <- ifelse(is_favorite, "true", "false")
+ args_parts <- c(args_parts, glue('"is_favorite": {fav_val}'))
+ }
+ if (!is.null(view_style)) {
+ args_parts <- c(args_parts, glue('"view_style": "{view_style}"'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+
+ if (verbose) {
+ message(glue::glue("Updating project {project_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "project_update", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ invisible(project_id)
+}
+
+#' Delete a project
+#'
+#' @param project_id id of the project
+#' @param project_name name of the project (for lookup if project_id not provided)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_project(project_name = "my_proj")
+#' }
+delete_project <- function(project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting project {project_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "project_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(project_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Archive a project
+#'
+#' @param project_id id of the project
+#' @param project_name name of the project (for lookup if project_id not provided)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the archived project (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' archive_project(project_name = "my_proj")
+#' }
+archive_project <- function(project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Archiving project {project_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "project_archive", "uuid": "{random_key()}", "args": {{"id": "{escape_json(project_id)}"}}}}]')
+ )
+
+ invisible(project_id)
+}
+
+#' Unarchive a project
+#'
+#' @param project_id id of the project
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the unarchived project (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' unarchive_project(project_id = "12345")
+#' }
+unarchive_project <- function(project_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Unarchiving project {project_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "project_unarchive", "uuid": "{random_key()}", "args": {{"id": "{escape_json(project_id)}"}}}}]')
+ )
+
+ invisible(project_id)
+}
+
+#' Get archived projects
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of archived projects
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_archived_projects()
+#' }
+get_archived_projects <- function(token = get_todoist_api_token()) {
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ response <- call_api_rest("projects/archived", token = token, cursor = cursor)
+ all_results <- c(all_results, response$results)
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(
+ id = character(),
+ name = character(),
+ color = character(),
+ is_favorite = logical(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(all_results, `[`, c("id", "name", "color", "is_favorite"))
+}
+
+#' Get a single project by ID
+#'
+#' @param project_id id of the project
+#' @param token todoist API token
+#'
+#' @return list with project details
+#' @export
+#'
+#' @examples
+#' \dontrun{
+#' get_project("12345")
+#' }
+get_project <- function(project_id, token = get_todoist_api_token()) {
+ force(token)
+ call_api_rest(glue("projects/{project_id}"), token = token)
+}
diff --git a/R/reminders.R b/R/reminders.R
new file mode 100644
index 0000000..52e8e89
--- /dev/null
+++ b/R/reminders.R
@@ -0,0 +1,194 @@
+#' Add a reminder to a task
+#'
+#' @param task_id id of the task
+#' @param due_date due date for the reminder (format: YYYY-MM-DD)
+#' @param due_datetime due datetime for the reminder (format: YYYY-MM-DDTHH:MM:SS)
+#' @param minute_offset minutes before due time to remind (for relative reminders)
+#' @param type type of reminder: "absolute", "relative", or "location"
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the new reminder
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' add_reminder(task_id = "12345", due_datetime = "2024-12-25T09:00:00")
+#' add_reminder(task_id = "12345", minute_offset = 30, type = "relative")
+#' }
+add_reminder <- function(task_id,
+ due_date = NULL,
+ due_datetime = NULL,
+ minute_offset = NULL,
+ type = "absolute",
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Adding reminder to task {task_id}"))
+ }
+
+ args_parts <- c(glue('"item_id": "{escape_json(task_id)}"'), glue('"type": "{escape_json(type)}"'))
+
+ if (!is.null(due_datetime)) {
+ args_parts <- c(args_parts, glue('"due": {{"date": "{escape_json(due_datetime)}"}}'))
+ } else if (!is.null(due_date)) {
+ args_parts <- c(args_parts, glue('"due": {{"date": "{escape_json(due_date)}"}}'))
+ }
+
+ if (!is.null(minute_offset)) {
+ args_parts <- c(args_parts, glue('"minute_offset": {minute_offset}'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+ temp_id <- random_key()
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["reminders"]',
+ commands = glue('[{{"type": "reminder_add", "temp_id": "{temp_id}", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ reminder_id <- result$temp_id_mapping[[temp_id]]
+ invisible(reminder_id)
+}
+
+#' Get all reminders
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of all reminders
+#' @export
+#' @importFrom purrr map_dfr pluck
+#'
+#' @examples
+#' \dontrun{
+#' get_all_reminders()
+#' }
+get_all_reminders <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["reminders"]'
+ )
+
+ reminders <- result %>% pluck("reminders")
+
+ if (is.null(reminders) || length(reminders) == 0) {
+ return(data.frame(
+ id = character(),
+ item_id = character(),
+ type = character(),
+ due_date = character(),
+ minute_offset = integer(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(reminders, function(x) {
+ due_date <- if (!is.null(x$due)) x$due$date else NA_character_
+ data.frame(
+ id = x$id %||% NA_character_,
+ item_id = x$item_id %||% NA_character_,
+ type = x$type %||% NA_character_,
+ due_date = due_date,
+ minute_offset = x$minute_offset %||% NA_integer_,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Update a reminder
+#'
+#' @param reminder_id id of the reminder
+#' @param due_date new due date for the reminder (format: YYYY-MM-DD)
+#' @param due_datetime new due datetime for the reminder (format: YYYY-MM-DDTHH:MM:SS)
+#' @param minute_offset new minutes before due time to remind
+#' @param type new type of reminder
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated reminder (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_reminder("12345", due_datetime = "2024-12-26T10:00:00")
+#' }
+update_reminder <- function(reminder_id,
+ due_date = NULL,
+ due_datetime = NULL,
+ minute_offset = NULL,
+ type = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Updating reminder {reminder_id}"))
+ }
+
+ args_parts <- c(glue('"id": "{escape_json(reminder_id)}"'))
+
+ if (!is.null(due_datetime)) {
+ args_parts <- c(args_parts, glue('"due": {{"date": "{escape_json(due_datetime)}"}}'))
+ } else if (!is.null(due_date)) {
+ args_parts <- c(args_parts, glue('"due": {{"date": "{escape_json(due_date)}"}}'))
+ }
+
+ if (!is.null(minute_offset)) {
+ args_parts <- c(args_parts, glue('"minute_offset": {minute_offset}'))
+ }
+
+ if (!is.null(type)) {
+ args_parts <- c(args_parts, glue('"type": "{escape_json(type)}"'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "reminder_update", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ invisible(reminder_id)
+}
+
+#' Delete a reminder
+#'
+#' @param reminder_id id of the reminder
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_reminder("12345")
+#' }
+delete_reminder <- function(reminder_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting reminder {reminder_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "reminder_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(reminder_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
diff --git a/R/section.R b/R/section.R
index 8a6b7b4..817ec6d 100644
--- a/R/section.R
+++ b/R/section.R
@@ -1,10 +1,12 @@
-#' add section
+#' Add section
#'
#' @param section_name section name
#' @param token todoist API token
#' @param project_name name of the project
#' @param project_id id of the project
#' @param force boolean force section creation even if already exist
+#'
+#' @return section id (character)
#' @importFrom glue glue
#' @export
#'
@@ -40,7 +42,7 @@ add_section <- function(section_name,
get_section_id(project_id = project_id,section_name = section_name,token = token)
}
-#' get id section
+#' Get section id
#'
#' @param project_name name of the project
#' @param project_id id of the project
@@ -48,8 +50,8 @@ add_section <- function(section_name,
#' @param all_section all_section
#' @param token token
#'
+#' @return section id (character). Returns 0 if section not found.
#' @importFrom dplyr left_join
-#' @importFrom httr content
#' @importFrom purrr pluck map_dfr
#' @export
get_section_id <- function(project_id = get_project_id(project_name = project_name,token = token),
@@ -97,6 +99,225 @@ get_section_from_project <- function(project_id, token = get_todoist_api_token()
}
cursor <- response$next_cursor
}
-# browser()
map_dfr(all_results, `[`, c("id", "name"))
}
+
+#' Update a section
+#'
+#' @param section_id id of the section
+#' @param new_name new name for the section
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated section (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_section("12345", new_name = "New Section Name")
+#' }
+update_section <- function(section_id,
+ new_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Updating section {section_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "section_update", "uuid": "{random_key()}", "args": {{"id": "{section_id}", "name": "{escape_json(new_name)}"}}}}]')
+ )
+
+ invisible(section_id)
+}
+
+#' Delete a section
+#'
+#' @param section_id id of the section
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_section("12345")
+#' }
+delete_section <- function(section_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting section {section_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "section_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(section_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Move a section to another project
+#'
+#' @param section_id id of the section
+#' @param project_id target project id
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the moved section (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' move_section("12345", project_id = "67890")
+#' }
+move_section <- function(section_id,
+ project_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Moving section {section_id} to project {project_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "section_move", "uuid": "{random_key()}", "args": {{"id": "{escape_json(section_id)}", "project_id": "{escape_json(project_id)}"}}}}]')
+ )
+
+ invisible(section_id)
+}
+
+#' Archive a section
+#'
+#' @param section_id id of the section
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the archived section (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' archive_section("12345")
+#' }
+archive_section <- function(section_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Archiving section {section_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "section_archive", "uuid": "{random_key()}", "args": {{"id": "{escape_json(section_id)}"}}}}]')
+ )
+
+ invisible(section_id)
+}
+
+#' Unarchive a section
+#'
+#' @param section_id id of the section
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the unarchived section (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' unarchive_section("12345")
+#' }
+unarchive_section <- function(section_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Unarchiving section {section_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "section_unarchive", "uuid": "{random_key()}", "args": {{"id": "{escape_json(section_id)}"}}}}]')
+ )
+
+ invisible(section_id)
+}
+
+#' Get a single section by ID
+#'
+#' @param section_id id of the section
+#' @param token todoist API token
+#'
+#' @return list with section details
+#' @export
+#'
+#' @examples
+#' \dontrun{
+#' get_section("12345")
+#' }
+get_section <- function(section_id, token = get_todoist_api_token()) {
+ force(token)
+ call_api_rest(glue("sections/{section_id}"), token = token)
+}
+
+#' Get all sections
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of all sections
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_all_sections()
+#' }
+get_all_sections <- function(token = get_todoist_api_token()) {
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ response <- call_api_rest("sections", token = token, cursor = cursor)
+ all_results <- c(all_results, response$results)
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(
+ id = character(),
+ name = character(),
+ project_id = character(),
+ order = integer(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(all_results, `[`, c("id", "name", "project_id", "order"))
+}
diff --git a/R/tasks.R b/R/tasks.R
index ea461d9..bfb5bf8 100644
--- a/R/tasks.R
+++ b/R/tasks.R
@@ -205,7 +205,7 @@ add_tasks_in_project <- function(project_id = get_project_id(project_name = proj
glue('{{ "type": "{action_to_do}",
"temp_id": "{random_key()}",
"uuid": "{random_key()}",
- "args": {{ "id": "{e}", "project_id": "{project_id}", "content": "{a_escaped}",
+ "args": {{ "id": "{escape_json(e)}", "project_id": "{escape_json(project_id)}", "content": "{a_escaped}",
"responsible_uid" : {assigned_part}, "due" : {due_part},
"section_id" : {sect_part} }}
}}')
@@ -306,7 +306,7 @@ add_responsible_to_task <- function(project_id = get_project_id(project_name = p
'[{{ "type": "item_update",
"temp_id": "{random_key()}",
"uuid": "{random_key()}",
- "args": {{ "id": "{id_task}", "responsible_uid" : "{responsible_uid}"}}}}]'
+ "args": {{ "id": "{escape_json(id_task)}", "responsible_uid" : "{escape_json(responsible_uid)}"}}}}]'
)
)
invisible(res)
@@ -371,3 +371,344 @@ not_used <- setdiff(names(tasks_as_df),c("tasks","responsible","due","section_na
if (check_only) {return(out)}
project_id
}
+
+#' Delete a task
+#'
+#' @param task_id id of the task to delete
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_task("12345")
+#' }
+delete_task <- function(task_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting task {task_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "item_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(task_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Close (complete) a task
+#'
+#' @param task_id id of the task to close
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the closed task (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' close_task("12345")
+#' }
+close_task <- function(task_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Closing task {task_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "item_close", "uuid": "{random_key()}", "args": {{"id": "{escape_json(task_id)}"}}}}]')
+ )
+
+ invisible(task_id)
+}
+
+#' Reopen (uncomplete) a task
+#'
+#' @param task_id id of the task to reopen
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the reopened task (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' reopen_task("12345")
+#' }
+reopen_task <- function(task_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Reopening task {task_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "item_uncomplete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(task_id)}"}}}}]')
+ )
+
+ invisible(task_id)
+}
+
+#' Move a task to another project or section
+#'
+#' @param task_id id of the task to move
+#' @param project_id new project id (optional)
+#' @param section_id new section id (optional)
+#' @param parent_id new parent task id (optional)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the moved task (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' move_task("12345", project_id = "67890")
+#' }
+move_task <- function(task_id,
+ project_id = NULL,
+ section_id = NULL,
+ parent_id = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ # Validate that at least one destination argument is provided
+ if (is.null(project_id) && is.null(section_id) && is.null(parent_id)) {
+ stop("At least one of project_id, section_id, or parent_id must be provided")
+ }
+
+ args_parts <- c(glue('"id": "{escape_json(task_id)}"'))
+
+ if (!is.null(project_id)) {
+ args_parts <- c(args_parts, glue('"project_id": "{escape_json(project_id)}"'))
+ }
+ if (!is.null(section_id)) {
+ args_parts <- c(args_parts, glue('"section_id": "{escape_json(section_id)}"'))
+ }
+ if (!is.null(parent_id)) {
+ args_parts <- c(args_parts, glue('"parent_id": "{escape_json(parent_id)}"'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+
+ if (verbose) {
+ message(glue::glue("Moving task {task_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "item_move", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ invisible(task_id)
+}
+
+#' Get completed tasks
+#'
+#' @param project_id project id to filter by (optional)
+#' @param since return tasks completed since this date (optional, format: YYYY-MM-DDTHH:MM:SS)
+#' @param until return tasks completed until this date (optional, format: YYYY-MM-DDTHH:MM:SS)
+#' @param limit maximum number of tasks to return (default 50, max 200)
+#' @param token todoist API token
+#'
+#' @return tibble of completed tasks
+#' @export
+#' @importFrom purrr map_dfr
+#'
+#' @examples
+#' \dontrun{
+#' get_completed_tasks()
+#' get_completed_tasks(project_id = "12345")
+#' }
+get_completed_tasks <- function(project_id = NULL,
+ since = NULL,
+ until = NULL,
+ limit = 50,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ params <- list(limit = limit)
+ if (!is.null(project_id)) params$project_id <- project_id
+ if (!is.null(since)) params$since <- since
+ if (!is.null(until)) params$until <- until
+
+ all_results <- list()
+ cursor <- NULL
+
+ repeat {
+ params$cursor <- cursor
+ response <- do.call(call_api_rest, c(list("tasks/completed/by_completion_date", token = token), params))
+
+ if (!is.null(response$items)) {
+ all_results <- c(all_results, response$items)
+ }
+
+ if (is.null(response$next_cursor)) {
+ break
+ }
+ cursor <- response$next_cursor
+ }
+
+ if (length(all_results) == 0) {
+ return(data.frame(
+ id = character(),
+ content = character(),
+ project_id = character(),
+ completed_at = character(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(all_results, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ content = x$content %||% NA_character_,
+ project_id = x$project_id %||% NA_character_,
+ completed_at = x$completed_at %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Quick add a task using natural language
+#'
+#' @param text natural language text for the task
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return task object
+#' @export
+#' @importFrom httr2 request req_headers req_body_json req_perform resp_body_json req_error resp_status
+#'
+#' @examples
+#' \dontrun{
+#' quick_add_task("Buy milk tomorrow at 5pm #Shopping")
+#' }
+quick_add_task <- function(text,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Quick adding task: {text}"))
+ }
+
+ result <- request(paste0(TODOIST_REST_URL, "tasks/quick")) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}"),
+ "Content-Type" = "application/json"
+ ) %>%
+ req_body_json(list(text = text)) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform() %>%
+ resp_body_json()
+
+ result
+}
+
+#' Get a single task by ID
+#'
+#' @param task_id id of the task
+#' @param token todoist API token
+#'
+#' @return list with task details
+#' @export
+#'
+#' @examples
+#' \dontrun{
+#' get_task("12345")
+#' }
+get_task <- function(task_id, token = get_todoist_api_token()) {
+ force(token)
+ call_api_rest(glue("tasks/{task_id}"), token = token)
+}
+
+#' Update a task
+#'
+#' @param task_id id of the task to update
+#' @param content new content/title for the task
+#' @param description new description for the task
+#' @param priority priority (1-4, 4 being highest)
+#' @param due_string due date as string (e.g., "tomorrow", "every monday")
+#' @param due_date due date as date (format: YYYY-MM-DD)
+#' @param labels vector of label names
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated task (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_task("12345", content = "Updated task name")
+#' }
+update_task <- function(task_id,
+ content = NULL,
+ description = NULL,
+ priority = NULL,
+ due_string = NULL,
+ due_date = NULL,
+ labels = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ args_parts <- c(glue('"id": "{escape_json(task_id)}"'))
+
+ if (!is.null(content)) {
+ args_parts <- c(args_parts, glue('"content": "{escape_json(content)}"'))
+ }
+ if (!is.null(description)) {
+ args_parts <- c(args_parts, glue('"description": "{escape_json(description)}"'))
+ }
+ if (!is.null(priority)) {
+ args_parts <- c(args_parts, glue('"priority": {priority}'))
+ }
+ if (!is.null(due_string)) {
+ args_parts <- c(args_parts, glue('"due": {{"string": "{escape_json(due_string)}"}}'))
+ } else if (!is.null(due_date)) {
+ args_parts <- c(args_parts, glue('"due": {{"date": "{escape_json(due_date)}"}}'))
+ }
+ if (!is.null(labels)) {
+ labels_escaped <- vapply(labels, escape_json, character(1), USE.NAMES = FALSE)
+ labels_json <- paste0('"', labels_escaped, '"', collapse = ", ")
+ args_parts <- c(args_parts, glue('"labels": [{labels_json}]'))
+ }
+
+ args_json <- paste(args_parts, collapse = ", ")
+
+ if (verbose) {
+ message(glue::glue("Updating task {task_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "item_update", "uuid": "{random_key()}", "args": {{{args_json}}}}}]')
+ )
+
+ invisible(task_id)
+}
diff --git a/R/templates.R b/R/templates.R
new file mode 100644
index 0000000..e7078ff
--- /dev/null
+++ b/R/templates.R
@@ -0,0 +1,90 @@
+#' Import a template into a project
+#'
+#' @param project_id id of the project to import into
+#' @param project_name name of the project (for lookup if project_id not provided)
+#' @param file_path path to the template file (CSV format)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_body_multipart req_perform req_error req_url_query resp_status
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' import_template(project_name = "my_proj", file_path = "template.csv")
+#' }
+import_template <- function(project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ file_path,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ if (!file.exists(file_path)) {
+ stop("Template file not found: ", file_path)
+ }
+
+ if (verbose) {
+ message(glue::glue("Importing template into project {project_id}"))
+ }
+
+ request(as.character(glue("{TODOIST_REST_URL}templates/import_into_project"))) %>%
+ req_url_query(project_id = project_id) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}")
+ ) %>%
+ req_body_multipart(file = curl::form_file(file_path)) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ invisible(NULL)
+}
+
+#' Export a project as a template
+#'
+#' @param project_id id of the project to export
+#' @param project_name name of the project (for lookup if project_id not provided)
+#' @param output_file path where to save the template file
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return path to the saved file (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_perform resp_body_string req_error resp_status
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' export_template(project_name = "my_proj", output_file = "template.csv")
+#' }
+export_template <- function(project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ output_file,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Exporting project {project_id} as template"))
+ }
+
+ response <- request(as.character(glue("{TODOIST_REST_URL}templates/export_as_file?project_id={project_id}"))) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}")
+ ) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ content <- resp_body_string(response)
+ writeLines(content, output_file)
+
+ if (verbose) {
+ message(glue::glue("Template saved to {output_file}"))
+ }
+
+ invisible(output_file)
+}
diff --git a/R/todoist-package.R b/R/todoist-package.R
index a893bb3..f44a732 100644
--- a/R/todoist-package.R
+++ b/R/todoist-package.R
@@ -1,5 +1,4 @@
#' @keywords internal
-#' @importFrom httr POST content
#' @import purrr
#' @import keyring
#' @import glue
diff --git a/R/token.R b/R/token.R
index b8ab64e..71adb9a 100644
--- a/R/token.R
+++ b/R/token.R
@@ -86,8 +86,8 @@ delete_todoist_api_token <- function() {
#' @export
ask_todoist_api_token <- function(msg = "Register Todoist Api Token") {
passwd <- tryCatch({
- newpass <- getPass(msg)
- }, interrupt = NULL)
+ getPass(msg)
+ }, interrupt = function(e) NULL)
if (!length(passwd) || !nchar(passwd)) {
return(NULL)
}
diff --git a/R/uploads.R b/R/uploads.R
new file mode 100644
index 0000000..a1fd3fd
--- /dev/null
+++ b/R/uploads.R
@@ -0,0 +1,86 @@
+#' Upload a file
+#'
+#' @param file_path path to the file to upload
+#' @param file_name name for the uploaded file (defaults to basename of file_path)
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return upload object with file_url
+#' @export
+#' @importFrom httr2 request req_headers req_body_multipart req_perform resp_body_json req_error resp_status
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' upload <- upload_file("document.pdf")
+#' # Use upload$file_url in a comment
+#' }
+upload_file <- function(file_path,
+ file_name = basename(file_path),
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (!file.exists(file_path)) {
+ stop("File not found: ", file_path)
+ }
+
+ if (verbose) {
+ message(glue::glue("Uploading file: {file_name}"))
+ }
+
+ result <- request(paste0(TODOIST_REST_URL, "uploads")) %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}")
+ ) %>%
+ req_body_multipart(
+ file = curl::form_file(file_path),
+ file_name = file_name
+ ) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform() %>%
+ resp_body_json()
+
+ if (verbose) {
+ message(glue::glue("File uploaded successfully"))
+ }
+
+ result
+}
+
+#' Delete an uploaded file
+#'
+#' @param file_url URL of the file to delete
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom httr2 request req_headers req_body_json req_perform req_method req_error resp_status
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_upload("https://...")
+#' }
+delete_upload <- function(file_url,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting uploaded file"))
+ }
+
+ request(paste0(TODOIST_REST_URL, "uploads")) %>%
+ req_method("DELETE") %>%
+ req_headers(
+ Authorization = glue::glue("Bearer {token}"),
+ "Content-Type" = "application/json"
+ ) %>%
+ req_body_json(list(file_url = file_url)) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform()
+
+ invisible(NULL)
+}
diff --git a/R/users.R b/R/users.R
index d712892..e03625d 100644
--- a/R/users.R
+++ b/R/users.R
@@ -110,7 +110,7 @@ add_user_in_project <- function(
)
)
if (verbose) {
- print(res)
+ message(res)
}
invisible(project_id)
}
@@ -176,13 +176,8 @@ add_users_in_project <- function(project_id = get_project_id(project_name = proj
# )
)
if (verbose) {
- print(res)
+ message(res)
}
-
-
-
-
-
}else{
message("All users are already in this project")
}
@@ -198,7 +193,6 @@ add_users_in_project <- function(project_id = get_project_id(project_name = proj
#' @param project_id id of the project
#' @importFrom purrr pluck map_df
#' @importFrom dplyr filter
-#' @importFrom httr content
#' @return dataframe of users in projects
#' @export
#'
@@ -218,12 +212,208 @@ get_users_in_project<- function( project_id = get_project_id(project_name = proj
pluck("collaborator_states") %>%
map_df(`[`, c("project_id", "user_id")) %>%
dplyr::filter(project_id == !!project_id)
-
-
+
+
if (nrow(out) == 0) {
out <- data.frame(project_id = character(), user_id = character())
-
+
}
out
}
+#' Get current user info
+#'
+#' @param token todoist API token
+#'
+#' @return list with user information
+#' @export
+#' @importFrom purrr pluck
+#'
+#' @examples
+#' \dontrun{
+#' get_user_info()
+#' }
+get_user_info <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["user"]'
+ )
+
+ result %>% pluck("user")
+}
+
+#' Get productivity stats
+#'
+#' @param token todoist API token
+#'
+#' @return list with productivity statistics
+#' @export
+#' @importFrom purrr pluck
+#'
+#' @examples
+#' \dontrun{
+#' get_productivity_stats()
+#' }
+get_productivity_stats <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["user"]'
+ )
+
+ user <- result %>% pluck("user")
+ if (is.null(user)) {
+ return(list())
+ }
+
+ list(
+ karma = user$karma,
+ karma_trend = user$karma_trend,
+ completed_today = user$completed_today,
+ days_items = user$days_items,
+ week_items = user$week_items
+ )
+}
+
+#' Delete a collaborator from a project
+#'
+#' @param project_id id of the project
+#' @param project_name name of the project (for lookup if project_id not provided)
+#' @param email email of the collaborator to remove
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the project (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_collaborator(project_name = "my_proj", email = "user@example.com")
+#' }
+delete_collaborator <- function(project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ email,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(project_id)
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Removing {email} from project {project_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "collaborator_delete", "uuid": "{random_key()}", "args": {{"project_id": "{escape_json(project_id)}", "email": "{escape_json(email)}"}}}}]')
+ )
+
+ invisible(project_id)
+}
+
+#' Accept a project invitation
+#'
+#' @param invitation_id id of the invitation
+#' @param invitation_secret secret of the invitation
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' accept_invitation("12345", "secret123")
+#' }
+accept_invitation <- function(invitation_id,
+ invitation_secret,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Accepting invitation {invitation_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "accept_invitation", "uuid": "{random_key()}", "args": {{"invitation_id": "{escape_json(invitation_id)}", "invitation_secret": "{escape_json(invitation_secret)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Reject a project invitation
+#'
+#' @param invitation_id id of the invitation
+#' @param invitation_secret secret of the invitation
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' reject_invitation("12345", "secret123")
+#' }
+reject_invitation <- function(invitation_id,
+ invitation_secret,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Rejecting invitation {invitation_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "reject_invitation", "uuid": "{random_key()}", "args": {{"invitation_id": "{escape_json(invitation_id)}", "invitation_secret": "{escape_json(invitation_secret)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Delete an invitation
+#'
+#' @param invitation_id id of the invitation
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' delete_invitation("12345")
+#' }
+delete_invitation <- function(invitation_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Deleting invitation {invitation_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "delete_invitation", "uuid": "{random_key()}", "args": {{"invitation_id": "{escape_json(invitation_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
diff --git a/R/utils.R b/R/utils.R
index 6d16595..81b3fb0 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -1,3 +1,6 @@
+# Null coalescing operator
+`%||%` <- function(a, b) if (is.null(a)) b else a
+
#' Escape special characters for JSON
#'
#' @param x character string to escape
@@ -31,38 +34,53 @@ random_key <- function() {
digest(glue("{Sys.time()}{mdp}"))
}
-#' Call the good version of API
+# API Base URLs
+TODOIST_SYNC_URL <- "https://api.todoist.com/api/v1/sync"
+TODOIST_REST_URL <- "https://api.todoist.com/api/v1/"
+
+#' Call the Sync API
#'
#' @param ... any params of POST request
#' @param token todoist API token
#' @param url url to call
#'
#' @return list
-#' @importFrom httr2 request req_headers req_body_multipart req_perform resp_body_json
-call_api <- function(...,url= "https://api.todoist.com/api/v1/sync",token = get_todoist_api_token()){
-
- message("call_api")
+#' @importFrom httr2 request req_headers req_body_multipart req_perform resp_body_json req_error resp_status
+call_api <- function(..., url = TODOIST_SYNC_URL, token = get_todoist_api_token()) {
+ if (is.null(token) || !nzchar(token)) {
+ stop("API token is required. Use set_todoist_api_token() to configure it.")
+ }
-
- request(base_url = url) %>%
+ request(url) %>%
req_headers(
- Authorization = glue::glue("Bearer {token}"),
- ) %>%
-
+ Authorization = glue::glue("Bearer {token}")
+ ) %>%
req_body_multipart(...) %>%
- req_perform() %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
+ req_perform() %>%
resp_body_json()
-
+
}
-#' @importFrom httr2 req_headers req_url_query req_perform resp_body_json request
+#' Call the REST API
+#'
+#' @param endpoint API endpoint (e.g., "projects", "tasks")
+#' @param token todoist API token
+#' @param ... query parameters
+#'
+#' @return list
+#' @importFrom httr2 req_headers req_url_query req_perform resp_body_json request req_error resp_status
call_api_rest <- function(endpoint, token = get_todoist_api_token(), ...) {
-
- request(paste0("https://api.todoist.com/api/v1/", endpoint)) %>%
+ if (is.null(token) || !nzchar(token)) {
+ stop("API token is required. Use set_todoist_api_token() to configure it.")
+ }
+
+ request(paste0(TODOIST_REST_URL, endpoint)) %>%
req_headers(
Authorization = glue::glue("Bearer {token}")
) %>%
req_url_query(...) %>%
+ req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
req_perform() %>%
resp_body_json()
}
\ No newline at end of file
diff --git a/R/workspaces.R b/R/workspaces.R
new file mode 100644
index 0000000..dbeffac
--- /dev/null
+++ b/R/workspaces.R
@@ -0,0 +1,189 @@
+#' Get all workspaces
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of all workspaces
+#' @export
+#' @importFrom purrr map_dfr pluck
+#'
+#' @examples
+#' \dontrun{
+#' get_all_workspaces()
+#' }
+get_all_workspaces <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["workspaces"]'
+ )
+
+ workspaces <- result %>% pluck("workspaces")
+
+ if (is.null(workspaces) || length(workspaces) == 0) {
+ return(data.frame(
+ id = character(),
+ name = character(),
+ is_default = logical(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(workspaces, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ is_default = x$is_default %||% FALSE,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Get workspace users
+#'
+#' @param token todoist API token
+#'
+#' @return tibble of workspace users
+#' @export
+#' @importFrom purrr map_dfr pluck
+#'
+#' @examples
+#' \dontrun{
+#' get_workspace_users()
+#' }
+get_workspace_users <- function(token = get_todoist_api_token()) {
+ force(token)
+
+ result <- call_api(
+ token = token,
+ sync_token = "*",
+ resource_types = '["workspace_users"]'
+ )
+
+ users <- result %>% pluck("workspace_users")
+
+ if (is.null(users) || length(users) == 0) {
+ return(data.frame(
+ user_id = character(),
+ workspace_id = character(),
+ name = character(),
+ email = character(),
+ role = character(),
+ stringsAsFactors = FALSE
+ ))
+ }
+
+ map_dfr(users, function(x) {
+ data.frame(
+ user_id = x$user_id %||% NA_character_,
+ workspace_id = x$workspace_id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ email = x$email %||% NA_character_,
+ role = x$role %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+}
+
+#' Invite user to workspace
+#'
+#' @param workspace_id id of the workspace
+#' @param email email of the user to invite
+#' @param role role for the user (e.g., "member", "admin")
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' invite_to_workspace("12345", "user@example.com")
+#' }
+invite_to_workspace <- function(workspace_id,
+ email,
+ role = "member",
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Inviting {email} to workspace {workspace_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "workspace_invite", "uuid": "{random_key()}", "args": {{"workspace_id": "{escape_json(workspace_id)}", "email": "{escape_json(email)}", "role": "{escape_json(role)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
+
+#' Update a workspace
+#'
+#' @param workspace_id id of the workspace
+#' @param name new name for the workspace
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return id of the updated workspace (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' update_workspace("12345", name = "New Workspace Name")
+#' }
+update_workspace <- function(workspace_id,
+ name,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Updating workspace {workspace_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "workspace_update", "uuid": "{random_key()}", "args": {{"id": "{escape_json(workspace_id)}", "name": "{escape_json(name)}"}}}}]')
+ )
+
+ invisible(workspace_id)
+}
+
+#' Leave a workspace
+#'
+#' @param workspace_id id of the workspace
+#' @param verbose boolean that make the function verbose
+#' @param token todoist API token
+#'
+#' @return NULL (invisible)
+#' @export
+#' @importFrom glue glue
+#'
+#' @examples
+#' \dontrun{
+#' leave_workspace("12345")
+#' }
+leave_workspace <- function(workspace_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()) {
+ force(token)
+
+ if (verbose) {
+ message(glue::glue("Leaving workspace {workspace_id}"))
+ }
+
+ call_api(
+ token = token,
+ sync_token = "*",
+ commands = glue('[{{"type": "workspace_leave", "uuid": "{random_key()}", "args": {{"id": "{escape_json(workspace_id)}"}}}}]')
+ )
+
+ invisible(NULL)
+}
diff --git a/man/accept_invitation.Rd b/man/accept_invitation.Rd
new file mode 100644
index 0000000..b383c18
--- /dev/null
+++ b/man/accept_invitation.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/users.R
+\name{accept_invitation}
+\alias{accept_invitation}
+\title{Accept a project invitation}
+\usage{
+accept_invitation(
+ invitation_id,
+ invitation_secret,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{invitation_id}{id of the invitation}
+
+\item{invitation_secret}{secret of the invitation}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Accept a project invitation
+}
+\examples{
+\dontrun{
+accept_invitation("12345", "secret123")
+}
+}
diff --git a/man/add_comment.Rd b/man/add_comment.Rd
new file mode 100644
index 0000000..f1091c5
--- /dev/null
+++ b/man/add_comment.Rd
@@ -0,0 +1,37 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/comments.R
+\name{add_comment}
+\alias{add_comment}
+\title{Add a comment to a task or project}
+\usage{
+add_comment(
+ content,
+ task_id = NULL,
+ project_id = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{content}{content of the comment}
+
+\item{task_id}{id of the task (either task_id or project_id required)}
+
+\item{project_id}{id of the project (either task_id or project_id required)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the new comment
+}
+\description{
+Add a comment to a task or project
+}
+\examples{
+\dontrun{
+add_comment(content = "This is a comment", task_id = "12345")
+add_comment(content = "Project comment", project_id = "67890")
+}
+}
diff --git a/man/add_filter.Rd b/man/add_filter.Rd
new file mode 100644
index 0000000..9080657
--- /dev/null
+++ b/man/add_filter.Rd
@@ -0,0 +1,40 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{add_filter}
+\alias{add_filter}
+\title{Add a new filter}
+\usage{
+add_filter(
+ name,
+ query,
+ color = NULL,
+ is_favorite = FALSE,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{name}{name of the filter}
+
+\item{query}{filter query string (e.g., "today | overdue", "p1 & #Work")}
+
+\item{color}{color of the filter}
+
+\item{is_favorite}{boolean to mark as favorite}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the new filter
+}
+\description{
+Add a new filter
+}
+\examples{
+\dontrun{
+add_filter("Urgent Today", query = "today & p1")
+add_filter("Work Tasks", query = "#Work", color = "blue")
+}
+}
diff --git a/man/add_label.Rd b/man/add_label.Rd
new file mode 100644
index 0000000..e373ec9
--- /dev/null
+++ b/man/add_label.Rd
@@ -0,0 +1,37 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{add_label}
+\alias{add_label}
+\title{Add a new label}
+\usage{
+add_label(
+ name,
+ color = NULL,
+ is_favorite = FALSE,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{name}{name of the label}
+
+\item{color}{color of the label}
+
+\item{is_favorite}{boolean to mark as favorite}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the new label
+}
+\description{
+Add a new label
+}
+\examples{
+\dontrun{
+add_label("urgent")
+add_label("work", color = "red")
+}
+}
diff --git a/man/add_reminder.Rd b/man/add_reminder.Rd
new file mode 100644
index 0000000..c3a66a4
--- /dev/null
+++ b/man/add_reminder.Rd
@@ -0,0 +1,43 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reminders.R
+\name{add_reminder}
+\alias{add_reminder}
+\title{Add a reminder to a task}
+\usage{
+add_reminder(
+ task_id,
+ due_date = NULL,
+ due_datetime = NULL,
+ minute_offset = NULL,
+ type = "absolute",
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{task_id}{id of the task}
+
+\item{due_date}{due date for the reminder (format: YYYY-MM-DD)}
+
+\item{due_datetime}{due datetime for the reminder (format: YYYY-MM-DDTHH:MM:SS)}
+
+\item{minute_offset}{minutes before due time to remind (for relative reminders)}
+
+\item{type}{type of reminder: "absolute", "relative", or "location"}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the new reminder
+}
+\description{
+Add a reminder to a task
+}
+\examples{
+\dontrun{
+add_reminder(task_id = "12345", due_datetime = "2024-12-25T09:00:00")
+add_reminder(task_id = "12345", minute_offset = 30, type = "relative")
+}
+}
diff --git a/man/add_section.Rd b/man/add_section.Rd
index 99e3d48..8284680 100644
--- a/man/add_section.Rd
+++ b/man/add_section.Rd
@@ -2,7 +2,7 @@
% Please edit documentation in R/section.R
\name{add_section}
\alias{add_section}
-\title{add section}
+\title{Add section}
\usage{
add_section(
section_name,
@@ -23,6 +23,9 @@ add_section(
\item{token}{todoist API token}
}
+\value{
+section id (character)
+}
\description{
-add section
+Add section
}
diff --git a/man/archive_project.Rd b/man/archive_project.Rd
new file mode 100644
index 0000000..a6e5c38
--- /dev/null
+++ b/man/archive_project.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/projects.R
+\name{archive_project}
+\alias{archive_project}
+\title{Archive a project}
+\usage{
+archive_project(
+ project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{id of the project}
+
+\item{project_name}{name of the project (for lookup if project_id not provided)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the archived project (invisible)
+}
+\description{
+Archive a project
+}
+\examples{
+\dontrun{
+archive_project(project_name = "my_proj")
+}
+}
diff --git a/man/archive_section.Rd b/man/archive_section.Rd
new file mode 100644
index 0000000..b258418
--- /dev/null
+++ b/man/archive_section.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{archive_section}
+\alias{archive_section}
+\title{Archive a section}
+\usage{
+archive_section(section_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{section_id}{id of the section}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the archived section (invisible)
+}
+\description{
+Archive a section
+}
+\examples{
+\dontrun{
+archive_section("12345")
+}
+}
diff --git a/man/call_api.Rd b/man/call_api.Rd
index c079624..ed9a013 100644
--- a/man/call_api.Rd
+++ b/man/call_api.Rd
@@ -2,13 +2,9 @@
% Please edit documentation in R/utils.R
\name{call_api}
\alias{call_api}
-\title{Call the good version of API}
+\title{Call the Sync API}
\usage{
-call_api(
- ...,
- url = "https://api.todoist.com/api/v1/sync",
- token = get_todoist_api_token()
-)
+call_api(..., url = TODOIST_SYNC_URL, token = get_todoist_api_token())
}
\arguments{
\item{...}{any params of POST request}
@@ -21,5 +17,5 @@ call_api(
list
}
\description{
-Call the good version of API
+Call the Sync API
}
diff --git a/man/call_api_rest.Rd b/man/call_api_rest.Rd
new file mode 100644
index 0000000..3648b1b
--- /dev/null
+++ b/man/call_api_rest.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/utils.R
+\name{call_api_rest}
+\alias{call_api_rest}
+\title{Call the REST API}
+\usage{
+call_api_rest(endpoint, token = get_todoist_api_token(), ...)
+}
+\arguments{
+\item{endpoint}{API endpoint (e.g., "projects", "tasks")}
+
+\item{token}{todoist API token}
+
+\item{...}{query parameters}
+}
+\value{
+list
+}
+\description{
+Call the REST API
+}
diff --git a/man/close_task.Rd b/man/close_task.Rd
new file mode 100644
index 0000000..a4d9a66
--- /dev/null
+++ b/man/close_task.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{close_task}
+\alias{close_task}
+\title{Close (complete) a task}
+\usage{
+close_task(task_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{task_id}{id of the task to close}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the closed task (invisible)
+}
+\description{
+Close (complete) a task
+}
+\examples{
+\dontrun{
+close_task("12345")
+}
+}
diff --git a/man/delete_collaborator.Rd b/man/delete_collaborator.Rd
new file mode 100644
index 0000000..dd27edc
--- /dev/null
+++ b/man/delete_collaborator.Rd
@@ -0,0 +1,36 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/users.R
+\name{delete_collaborator}
+\alias{delete_collaborator}
+\title{Delete a collaborator from a project}
+\usage{
+delete_collaborator(
+ project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ email,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{id of the project}
+
+\item{project_name}{name of the project (for lookup if project_id not provided)}
+
+\item{email}{email of the collaborator to remove}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the project (invisible)
+}
+\description{
+Delete a collaborator from a project
+}
+\examples{
+\dontrun{
+delete_collaborator(project_name = "my_proj", email = "user@example.com")
+}
+}
diff --git a/man/delete_comment.Rd b/man/delete_comment.Rd
new file mode 100644
index 0000000..907a92b
--- /dev/null
+++ b/man/delete_comment.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/comments.R
+\name{delete_comment}
+\alias{delete_comment}
+\title{Delete a comment}
+\usage{
+delete_comment(comment_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{comment_id}{id of the comment}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a comment
+}
+\examples{
+\dontrun{
+delete_comment("12345")
+}
+}
diff --git a/man/delete_filter.Rd b/man/delete_filter.Rd
new file mode 100644
index 0000000..e762b68
--- /dev/null
+++ b/man/delete_filter.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{delete_filter}
+\alias{delete_filter}
+\title{Delete a filter}
+\usage{
+delete_filter(
+ filter_id = get_filter_id(filter_name = filter_name, token = token),
+ filter_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{filter_id}{id of the filter}
+
+\item{filter_name}{name of the filter (for lookup if filter_id not provided)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a filter
+}
+\examples{
+\dontrun{
+delete_filter(filter_name = "Urgent Today")
+}
+}
diff --git a/man/delete_invitation.Rd b/man/delete_invitation.Rd
new file mode 100644
index 0000000..fbd9759
--- /dev/null
+++ b/man/delete_invitation.Rd
@@ -0,0 +1,30 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/users.R
+\name{delete_invitation}
+\alias{delete_invitation}
+\title{Delete an invitation}
+\usage{
+delete_invitation(
+ invitation_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{invitation_id}{id of the invitation}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete an invitation
+}
+\examples{
+\dontrun{
+delete_invitation("12345")
+}
+}
diff --git a/man/delete_label.Rd b/man/delete_label.Rd
new file mode 100644
index 0000000..c53aa95
--- /dev/null
+++ b/man/delete_label.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{delete_label}
+\alias{delete_label}
+\title{Delete a label}
+\usage{
+delete_label(
+ label_id = get_label_id(label_name = label_name, token = token, create = FALSE),
+ label_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{label_id}{id of the label}
+
+\item{label_name}{name of the label (for lookup if label_id not provided)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a label
+}
+\examples{
+\dontrun{
+delete_label(label_name = "urgent")
+}
+}
diff --git a/man/delete_project.Rd b/man/delete_project.Rd
new file mode 100644
index 0000000..42f01ca
--- /dev/null
+++ b/man/delete_project.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/projects.R
+\name{delete_project}
+\alias{delete_project}
+\title{Delete a project}
+\usage{
+delete_project(
+ project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{id of the project}
+
+\item{project_name}{name of the project (for lookup if project_id not provided)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a project
+}
+\examples{
+\dontrun{
+delete_project(project_name = "my_proj")
+}
+}
diff --git a/man/delete_reminder.Rd b/man/delete_reminder.Rd
new file mode 100644
index 0000000..56300c7
--- /dev/null
+++ b/man/delete_reminder.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reminders.R
+\name{delete_reminder}
+\alias{delete_reminder}
+\title{Delete a reminder}
+\usage{
+delete_reminder(reminder_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{reminder_id}{id of the reminder}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a reminder
+}
+\examples{
+\dontrun{
+delete_reminder("12345")
+}
+}
diff --git a/man/delete_section.Rd b/man/delete_section.Rd
new file mode 100644
index 0000000..0e88bc9
--- /dev/null
+++ b/man/delete_section.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{delete_section}
+\alias{delete_section}
+\title{Delete a section}
+\usage{
+delete_section(section_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{section_id}{id of the section}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a section
+}
+\examples{
+\dontrun{
+delete_section("12345")
+}
+}
diff --git a/man/delete_task.Rd b/man/delete_task.Rd
new file mode 100644
index 0000000..f82ef94
--- /dev/null
+++ b/man/delete_task.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{delete_task}
+\alias{delete_task}
+\title{Delete a task}
+\usage{
+delete_task(task_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{task_id}{id of the task to delete}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete a task
+}
+\examples{
+\dontrun{
+delete_task("12345")
+}
+}
diff --git a/man/delete_upload.Rd b/man/delete_upload.Rd
new file mode 100644
index 0000000..d559293
--- /dev/null
+++ b/man/delete_upload.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/uploads.R
+\name{delete_upload}
+\alias{delete_upload}
+\title{Delete an uploaded file}
+\usage{
+delete_upload(file_url, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{file_url}{URL of the file to delete}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Delete an uploaded file
+}
+\examples{
+\dontrun{
+delete_upload("https://...")
+}
+}
diff --git a/man/download_backup.Rd b/man/download_backup.Rd
new file mode 100644
index 0000000..f5ace45
--- /dev/null
+++ b/man/download_backup.Rd
@@ -0,0 +1,34 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/backups.R
+\name{download_backup}
+\alias{download_backup}
+\title{Download a backup}
+\usage{
+download_backup(
+ version,
+ output_file,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{version}{version of the backup to download (from get_backups())}
+
+\item{output_file}{path where to save the backup file}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+path to the saved file (invisible)
+}
+\description{
+Download a backup
+}
+\examples{
+\dontrun{
+backups <- get_backups()
+download_backup(backups$version[1], "backup.zip")
+}
+}
diff --git a/man/export_template.Rd b/man/export_template.Rd
new file mode 100644
index 0000000..fc047eb
--- /dev/null
+++ b/man/export_template.Rd
@@ -0,0 +1,36 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/templates.R
+\name{export_template}
+\alias{export_template}
+\title{Export a project as a template}
+\usage{
+export_template(
+ project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ output_file,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{id of the project to export}
+
+\item{project_name}{name of the project (for lookup if project_id not provided)}
+
+\item{output_file}{path where to save the template file}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+path to the saved file (invisible)
+}
+\description{
+Export a project as a template
+}
+\examples{
+\dontrun{
+export_template(project_name = "my_proj", output_file = "template.csv")
+}
+}
diff --git a/man/get_activity_logs.Rd b/man/get_activity_logs.Rd
new file mode 100644
index 0000000..0133d05
--- /dev/null
+++ b/man/get_activity_logs.Rd
@@ -0,0 +1,55 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/activity.R
+\name{get_activity_logs}
+\alias{get_activity_logs}
+\title{Get activity logs}
+\usage{
+get_activity_logs(
+ object_type = NULL,
+ object_id = NULL,
+ event_type = NULL,
+ parent_project_id = NULL,
+ parent_item_id = NULL,
+ initiator_id = NULL,
+ since = NULL,
+ until = NULL,
+ limit = 30,
+ offset = 0,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{object_type}{filter by object type (e.g., "project", "item", "note")}
+
+\item{object_id}{filter by specific object id}
+
+\item{event_type}{filter by event type (e.g., "added", "updated", "completed")}
+
+\item{parent_project_id}{filter by parent project id}
+
+\item{parent_item_id}{filter by parent item id}
+
+\item{initiator_id}{filter by user who initiated the action}
+
+\item{since}{return events since this date (format: YYYY-MM-DDTHH:MM:SS)}
+
+\item{until}{return events until this date (format: YYYY-MM-DDTHH:MM:SS)}
+
+\item{limit}{maximum number of events to return (default 30, max 100)}
+
+\item{offset}{offset for pagination}
+
+\item{token}{todoist API token}
+}
+\value{
+tibble of activity events
+}
+\description{
+Get activity logs
+}
+\examples{
+\dontrun{
+get_activity_logs()
+get_activity_logs(object_type = "item", event_type = "completed")
+}
+}
diff --git a/man/get_all_filters.Rd b/man/get_all_filters.Rd
new file mode 100644
index 0000000..4c3b154
--- /dev/null
+++ b/man/get_all_filters.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{get_all_filters}
+\alias{get_all_filters}
+\title{Get all filters}
+\usage{
+get_all_filters(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of all filters
+}
+\description{
+Get all filters
+}
+\examples{
+\dontrun{
+get_all_filters()
+}
+}
diff --git a/man/get_all_labels.Rd b/man/get_all_labels.Rd
new file mode 100644
index 0000000..d5dc8dd
--- /dev/null
+++ b/man/get_all_labels.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{get_all_labels}
+\alias{get_all_labels}
+\title{Get all labels}
+\usage{
+get_all_labels(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of all labels
+}
+\description{
+Get all labels
+}
+\examples{
+\dontrun{
+get_all_labels()
+}
+}
diff --git a/man/get_all_reminders.Rd b/man/get_all_reminders.Rd
new file mode 100644
index 0000000..adf134c
--- /dev/null
+++ b/man/get_all_reminders.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reminders.R
+\name{get_all_reminders}
+\alias{get_all_reminders}
+\title{Get all reminders}
+\usage{
+get_all_reminders(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of all reminders
+}
+\description{
+Get all reminders
+}
+\examples{
+\dontrun{
+get_all_reminders()
+}
+}
diff --git a/man/get_all_sections.Rd b/man/get_all_sections.Rd
new file mode 100644
index 0000000..28eb2f6
--- /dev/null
+++ b/man/get_all_sections.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{get_all_sections}
+\alias{get_all_sections}
+\title{Get all sections}
+\usage{
+get_all_sections(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of all sections
+}
+\description{
+Get all sections
+}
+\examples{
+\dontrun{
+get_all_sections()
+}
+}
diff --git a/man/get_all_workspaces.Rd b/man/get_all_workspaces.Rd
new file mode 100644
index 0000000..d2925f1
--- /dev/null
+++ b/man/get_all_workspaces.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/workspaces.R
+\name{get_all_workspaces}
+\alias{get_all_workspaces}
+\title{Get all workspaces}
+\usage{
+get_all_workspaces(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of all workspaces
+}
+\description{
+Get all workspaces
+}
+\examples{
+\dontrun{
+get_all_workspaces()
+}
+}
diff --git a/man/get_archived_projects.Rd b/man/get_archived_projects.Rd
new file mode 100644
index 0000000..8c7f439
--- /dev/null
+++ b/man/get_archived_projects.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/projects.R
+\name{get_archived_projects}
+\alias{get_archived_projects}
+\title{Get archived projects}
+\usage{
+get_archived_projects(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of archived projects
+}
+\description{
+Get archived projects
+}
+\examples{
+\dontrun{
+get_archived_projects()
+}
+}
diff --git a/man/get_backups.Rd b/man/get_backups.Rd
new file mode 100644
index 0000000..6244cdc
--- /dev/null
+++ b/man/get_backups.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/backups.R
+\name{get_backups}
+\alias{get_backups}
+\title{Get list of backups}
+\usage{
+get_backups(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of available backups
+}
+\description{
+Get list of backups
+}
+\examples{
+\dontrun{
+get_backups()
+}
+}
diff --git a/man/get_comment.Rd b/man/get_comment.Rd
new file mode 100644
index 0000000..8ded5c1
--- /dev/null
+++ b/man/get_comment.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/comments.R
+\name{get_comment}
+\alias{get_comment}
+\title{Get a single comment by ID}
+\usage{
+get_comment(comment_id, token = get_todoist_api_token())
+}
+\arguments{
+\item{comment_id}{id of the comment}
+
+\item{token}{todoist API token}
+}
+\value{
+list with comment details
+}
+\description{
+Get a single comment by ID
+}
+\examples{
+\dontrun{
+get_comment("12345")
+}
+}
diff --git a/man/get_comments.Rd b/man/get_comments.Rd
new file mode 100644
index 0000000..3405ae4
--- /dev/null
+++ b/man/get_comments.Rd
@@ -0,0 +1,31 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/comments.R
+\name{get_comments}
+\alias{get_comments}
+\title{Get comments}
+\usage{
+get_comments(
+ task_id = NULL,
+ project_id = NULL,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{task_id}{id of the task (either task_id or project_id required)}
+
+\item{project_id}{id of the project (either task_id or project_id required)}
+
+\item{token}{todoist API token}
+}
+\value{
+tibble of comments
+}
+\description{
+Get comments
+}
+\examples{
+\dontrun{
+get_comments(task_id = "12345")
+get_comments(project_id = "67890")
+}
+}
diff --git a/man/get_completed_tasks.Rd b/man/get_completed_tasks.Rd
new file mode 100644
index 0000000..22fc7b6
--- /dev/null
+++ b/man/get_completed_tasks.Rd
@@ -0,0 +1,37 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{get_completed_tasks}
+\alias{get_completed_tasks}
+\title{Get completed tasks}
+\usage{
+get_completed_tasks(
+ project_id = NULL,
+ since = NULL,
+ until = NULL,
+ limit = 50,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{project id to filter by (optional)}
+
+\item{since}{return tasks completed since this date (optional, format: YYYY-MM-DDTHH:MM:SS)}
+
+\item{until}{return tasks completed until this date (optional, format: YYYY-MM-DDTHH:MM:SS)}
+
+\item{limit}{maximum number of tasks to return (default 50, max 200)}
+
+\item{token}{todoist API token}
+}
+\value{
+tibble of completed tasks
+}
+\description{
+Get completed tasks
+}
+\examples{
+\dontrun{
+get_completed_tasks()
+get_completed_tasks(project_id = "12345")
+}
+}
diff --git a/man/get_filter.Rd b/man/get_filter.Rd
new file mode 100644
index 0000000..bcf39b9
--- /dev/null
+++ b/man/get_filter.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{get_filter}
+\alias{get_filter}
+\title{Get a single filter by ID}
+\usage{
+get_filter(filter_id, token = get_todoist_api_token())
+}
+\arguments{
+\item{filter_id}{id of the filter}
+
+\item{token}{todoist API token}
+}
+\value{
+list with filter details
+}
+\description{
+Get a single filter by ID
+}
+\examples{
+\dontrun{
+get_filter("12345")
+}
+}
diff --git a/man/get_filter_id.Rd b/man/get_filter_id.Rd
new file mode 100644
index 0000000..5a7a466
--- /dev/null
+++ b/man/get_filter_id.Rd
@@ -0,0 +1,30 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{get_filter_id}
+\alias{get_filter_id}
+\title{Get filter id by name}
+\usage{
+get_filter_id(
+ filter_name,
+ all_filters = get_all_filters(token = token),
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{filter_name}{name of the filter}
+
+\item{all_filters}{result of get_all_filters (optional)}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the filter
+}
+\description{
+Get filter id by name
+}
+\examples{
+\dontrun{
+get_filter_id("Urgent Today")
+}
+}
diff --git a/man/get_label.Rd b/man/get_label.Rd
new file mode 100644
index 0000000..f4f2e04
--- /dev/null
+++ b/man/get_label.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{get_label}
+\alias{get_label}
+\title{Get a single label by ID}
+\usage{
+get_label(label_id, token = get_todoist_api_token())
+}
+\arguments{
+\item{label_id}{id of the label}
+
+\item{token}{todoist API token}
+}
+\value{
+list with label details
+}
+\description{
+Get a single label by ID
+}
+\examples{
+\dontrun{
+get_label("12345")
+}
+}
diff --git a/man/get_label_id.Rd b/man/get_label_id.Rd
new file mode 100644
index 0000000..569d47d
--- /dev/null
+++ b/man/get_label_id.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{get_label_id}
+\alias{get_label_id}
+\title{Get label id by name}
+\usage{
+get_label_id(
+ label_name,
+ all_labels = get_all_labels(token = token),
+ token = get_todoist_api_token(),
+ create = TRUE
+)
+}
+\arguments{
+\item{label_name}{name of the label}
+
+\item{all_labels}{result of get_all_labels (optional)}
+
+\item{token}{todoist API token}
+
+\item{create}{boolean create label if needed}
+}
+\value{
+id of the label
+}
+\description{
+Get label id by name
+}
+\examples{
+\dontrun{
+get_label_id("urgent")
+}
+}
diff --git a/man/get_productivity_stats.Rd b/man/get_productivity_stats.Rd
new file mode 100644
index 0000000..6d3182a
--- /dev/null
+++ b/man/get_productivity_stats.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/users.R
+\name{get_productivity_stats}
+\alias{get_productivity_stats}
+\title{Get productivity stats}
+\usage{
+get_productivity_stats(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+list with productivity statistics
+}
+\description{
+Get productivity stats
+}
+\examples{
+\dontrun{
+get_productivity_stats()
+}
+}
diff --git a/man/get_project.Rd b/man/get_project.Rd
new file mode 100644
index 0000000..5bb528a
--- /dev/null
+++ b/man/get_project.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/projects.R
+\name{get_project}
+\alias{get_project}
+\title{Get a single project by ID}
+\usage{
+get_project(project_id, token = get_todoist_api_token())
+}
+\arguments{
+\item{project_id}{id of the project}
+
+\item{token}{todoist API token}
+}
+\value{
+list with project details
+}
+\description{
+Get a single project by ID
+}
+\examples{
+\dontrun{
+get_project("12345")
+}
+}
diff --git a/man/get_section.Rd b/man/get_section.Rd
new file mode 100644
index 0000000..826cd48
--- /dev/null
+++ b/man/get_section.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{get_section}
+\alias{get_section}
+\title{Get a single section by ID}
+\usage{
+get_section(section_id, token = get_todoist_api_token())
+}
+\arguments{
+\item{section_id}{id of the section}
+
+\item{token}{todoist API token}
+}
+\value{
+list with section details
+}
+\description{
+Get a single section by ID
+}
+\examples{
+\dontrun{
+get_section("12345")
+}
+}
diff --git a/man/get_section_id.Rd b/man/get_section_id.Rd
index c48af83..17f8804 100644
--- a/man/get_section_id.Rd
+++ b/man/get_section_id.Rd
@@ -2,7 +2,7 @@
% Please edit documentation in R/section.R
\name{get_section_id}
\alias{get_section_id}
-\title{get id section}
+\title{Get section id}
\usage{
get_section_id(
project_id = get_project_id(project_name = project_name, token = token),
@@ -23,6 +23,9 @@ get_section_id(
\item{all_section}{all_section}
}
+\value{
+section id (character). Returns 0 if section not found.
+}
\description{
-get id section
+Get section id
}
diff --git a/man/get_shared_labels.Rd b/man/get_shared_labels.Rd
new file mode 100644
index 0000000..4d9c41d
--- /dev/null
+++ b/man/get_shared_labels.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{get_shared_labels}
+\alias{get_shared_labels}
+\title{Get shared labels}
+\usage{
+get_shared_labels(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of shared labels
+}
+\description{
+Get shared labels
+}
+\examples{
+\dontrun{
+get_shared_labels()
+}
+}
diff --git a/man/get_task.Rd b/man/get_task.Rd
new file mode 100644
index 0000000..e89a7ca
--- /dev/null
+++ b/man/get_task.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{get_task}
+\alias{get_task}
+\title{Get a single task by ID}
+\usage{
+get_task(task_id, token = get_todoist_api_token())
+}
+\arguments{
+\item{task_id}{id of the task}
+
+\item{token}{todoist API token}
+}
+\value{
+list with task details
+}
+\description{
+Get a single task by ID
+}
+\examples{
+\dontrun{
+get_task("12345")
+}
+}
diff --git a/man/get_tasks_by_filter.Rd b/man/get_tasks_by_filter.Rd
new file mode 100644
index 0000000..24d6d03
--- /dev/null
+++ b/man/get_tasks_by_filter.Rd
@@ -0,0 +1,25 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{get_tasks_by_filter}
+\alias{get_tasks_by_filter}
+\title{Get tasks by filter query}
+\usage{
+get_tasks_by_filter(query, token = get_todoist_api_token())
+}
+\arguments{
+\item{query}{filter query string (e.g., "today", "p1 & #Work")}
+
+\item{token}{todoist API token}
+}
+\value{
+tibble of tasks matching the filter
+}
+\description{
+Get tasks by filter query
+}
+\examples{
+\dontrun{
+get_tasks_by_filter("today")
+get_tasks_by_filter("p1 & #Work")
+}
+}
diff --git a/man/get_user_info.Rd b/man/get_user_info.Rd
new file mode 100644
index 0000000..e0047d8
--- /dev/null
+++ b/man/get_user_info.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/users.R
+\name{get_user_info}
+\alias{get_user_info}
+\title{Get current user info}
+\usage{
+get_user_info(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+list with user information
+}
+\description{
+Get current user info
+}
+\examples{
+\dontrun{
+get_user_info()
+}
+}
diff --git a/man/get_workspace_users.Rd b/man/get_workspace_users.Rd
new file mode 100644
index 0000000..b18f03c
--- /dev/null
+++ b/man/get_workspace_users.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/workspaces.R
+\name{get_workspace_users}
+\alias{get_workspace_users}
+\title{Get workspace users}
+\usage{
+get_workspace_users(token = get_todoist_api_token())
+}
+\arguments{
+\item{token}{todoist API token}
+}
+\value{
+tibble of workspace users
+}
+\description{
+Get workspace users
+}
+\examples{
+\dontrun{
+get_workspace_users()
+}
+}
diff --git a/man/import_template.Rd b/man/import_template.Rd
new file mode 100644
index 0000000..af9a157
--- /dev/null
+++ b/man/import_template.Rd
@@ -0,0 +1,36 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/templates.R
+\name{import_template}
+\alias{import_template}
+\title{Import a template into a project}
+\usage{
+import_template(
+ project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ file_path,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{id of the project to import into}
+
+\item{project_name}{name of the project (for lookup if project_id not provided)}
+
+\item{file_path}{path to the template file (CSV format)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Import a template into a project
+}
+\examples{
+\dontrun{
+import_template(project_name = "my_proj", file_path = "template.csv")
+}
+}
diff --git a/man/invite_to_workspace.Rd b/man/invite_to_workspace.Rd
new file mode 100644
index 0000000..8c5ad1f
--- /dev/null
+++ b/man/invite_to_workspace.Rd
@@ -0,0 +1,36 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/workspaces.R
+\name{invite_to_workspace}
+\alias{invite_to_workspace}
+\title{Invite user to workspace}
+\usage{
+invite_to_workspace(
+ workspace_id,
+ email,
+ role = "member",
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{workspace_id}{id of the workspace}
+
+\item{email}{email of the user to invite}
+
+\item{role}{role for the user (e.g., "member", "admin")}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Invite user to workspace
+}
+\examples{
+\dontrun{
+invite_to_workspace("12345", "user@example.com")
+}
+}
diff --git a/man/leave_workspace.Rd b/man/leave_workspace.Rd
new file mode 100644
index 0000000..34360df
--- /dev/null
+++ b/man/leave_workspace.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/workspaces.R
+\name{leave_workspace}
+\alias{leave_workspace}
+\title{Leave a workspace}
+\usage{
+leave_workspace(workspace_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{workspace_id}{id of the workspace}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Leave a workspace
+}
+\examples{
+\dontrun{
+leave_workspace("12345")
+}
+}
diff --git a/man/move_section.Rd b/man/move_section.Rd
new file mode 100644
index 0000000..01ff802
--- /dev/null
+++ b/man/move_section.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{move_section}
+\alias{move_section}
+\title{Move a section to another project}
+\usage{
+move_section(
+ section_id,
+ project_id,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{section_id}{id of the section}
+
+\item{project_id}{target project id}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the moved section (invisible)
+}
+\description{
+Move a section to another project
+}
+\examples{
+\dontrun{
+move_section("12345", project_id = "67890")
+}
+}
diff --git a/man/move_task.Rd b/man/move_task.Rd
new file mode 100644
index 0000000..bad0fc4
--- /dev/null
+++ b/man/move_task.Rd
@@ -0,0 +1,39 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{move_task}
+\alias{move_task}
+\title{Move a task to another project or section}
+\usage{
+move_task(
+ task_id,
+ project_id = NULL,
+ section_id = NULL,
+ parent_id = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{task_id}{id of the task to move}
+
+\item{project_id}{new project id (optional)}
+
+\item{section_id}{new section id (optional)}
+
+\item{parent_id}{new parent task id (optional)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the moved task (invisible)
+}
+\description{
+Move a task to another project or section
+}
+\examples{
+\dontrun{
+move_task("12345", project_id = "67890")
+}
+}
diff --git a/man/quick_add_task.Rd b/man/quick_add_task.Rd
new file mode 100644
index 0000000..236ad1b
--- /dev/null
+++ b/man/quick_add_task.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{quick_add_task}
+\alias{quick_add_task}
+\title{Quick add a task using natural language}
+\usage{
+quick_add_task(text, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{text}{natural language text for the task}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+task object
+}
+\description{
+Quick add a task using natural language
+}
+\examples{
+\dontrun{
+quick_add_task("Buy milk tomorrow at 5pm #Shopping")
+}
+}
diff --git a/man/reject_invitation.Rd b/man/reject_invitation.Rd
new file mode 100644
index 0000000..e72bc14
--- /dev/null
+++ b/man/reject_invitation.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/users.R
+\name{reject_invitation}
+\alias{reject_invitation}
+\title{Reject a project invitation}
+\usage{
+reject_invitation(
+ invitation_id,
+ invitation_secret,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{invitation_id}{id of the invitation}
+
+\item{invitation_secret}{secret of the invitation}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Reject a project invitation
+}
+\examples{
+\dontrun{
+reject_invitation("12345", "secret123")
+}
+}
diff --git a/man/remove_shared_label.Rd b/man/remove_shared_label.Rd
new file mode 100644
index 0000000..735a8a9
--- /dev/null
+++ b/man/remove_shared_label.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{remove_shared_label}
+\alias{remove_shared_label}
+\title{Remove a shared label}
+\usage{
+remove_shared_label(name, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{name}{name of the shared label to remove}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Remove a shared label
+}
+\examples{
+\dontrun{
+remove_shared_label("label_name")
+}
+}
diff --git a/man/rename_shared_label.Rd b/man/rename_shared_label.Rd
new file mode 100644
index 0000000..0b973a6
--- /dev/null
+++ b/man/rename_shared_label.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{rename_shared_label}
+\alias{rename_shared_label}
+\title{Rename a shared label}
+\usage{
+rename_shared_label(
+ old_name,
+ new_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{old_name}{current name of the shared label}
+
+\item{new_name}{new name for the shared label}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+NULL (invisible)
+}
+\description{
+Rename a shared label
+}
+\examples{
+\dontrun{
+rename_shared_label("old_name", "new_name")
+}
+}
diff --git a/man/reopen_task.Rd b/man/reopen_task.Rd
new file mode 100644
index 0000000..18f7c08
--- /dev/null
+++ b/man/reopen_task.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{reopen_task}
+\alias{reopen_task}
+\title{Reopen (uncomplete) a task}
+\usage{
+reopen_task(task_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{task_id}{id of the task to reopen}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the reopened task (invisible)
+}
+\description{
+Reopen (uncomplete) a task
+}
+\examples{
+\dontrun{
+reopen_task("12345")
+}
+}
diff --git a/man/rtodoist-package.Rd b/man/rtodoist-package.Rd
index ca79a2f..6417a6f 100644
--- a/man/rtodoist-package.Rd
+++ b/man/rtodoist-package.Rd
@@ -6,7 +6,7 @@
\alias{rtodoist-package}
\title{rtodoist: Create and Manage Todolist using 'Todoist.com' API}
\description{
-Allows you to interact with the API of the "Todoist" platform. 'Todoist' \url{https://todoist.com/} provides an online task manager service for teams.
+Allows you to interact with the API of the "Todoist" platform. 'Todoist' \url{https://www.todoist.com/} provides an online task manager service for teams.
}
\seealso{
Useful links:
@@ -21,7 +21,7 @@ Useful links:
Authors:
\itemize{
- \item Cervan Girard \email{cervan@thinkr.fr} (\href{https://orcid.org/0000-0002-4816-4624}{ORCID})
+ \item Cervan Girard \email{cervan@thinkr.fr} (\href{https://orcid.org/0000-0002-4816-4624}{ORCID}) (previous maintainer)
}
Other contributors:
diff --git a/man/unarchive_project.Rd b/man/unarchive_project.Rd
new file mode 100644
index 0000000..76ff146
--- /dev/null
+++ b/man/unarchive_project.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/projects.R
+\name{unarchive_project}
+\alias{unarchive_project}
+\title{Unarchive a project}
+\usage{
+unarchive_project(project_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{project_id}{id of the project}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the unarchived project (invisible)
+}
+\description{
+Unarchive a project
+}
+\examples{
+\dontrun{
+unarchive_project(project_id = "12345")
+}
+}
diff --git a/man/unarchive_section.Rd b/man/unarchive_section.Rd
new file mode 100644
index 0000000..75bd35a
--- /dev/null
+++ b/man/unarchive_section.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{unarchive_section}
+\alias{unarchive_section}
+\title{Unarchive a section}
+\usage{
+unarchive_section(section_id, verbose = TRUE, token = get_todoist_api_token())
+}
+\arguments{
+\item{section_id}{id of the section}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the unarchived section (invisible)
+}
+\description{
+Unarchive a section
+}
+\examples{
+\dontrun{
+unarchive_section("12345")
+}
+}
diff --git a/man/update_comment.Rd b/man/update_comment.Rd
new file mode 100644
index 0000000..85136a8
--- /dev/null
+++ b/man/update_comment.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/comments.R
+\name{update_comment}
+\alias{update_comment}
+\title{Update a comment}
+\usage{
+update_comment(
+ comment_id,
+ content,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{comment_id}{id of the comment}
+
+\item{content}{new content for the comment}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated comment (invisible)
+}
+\description{
+Update a comment
+}
+\examples{
+\dontrun{
+update_comment("12345", content = "Updated comment")
+}
+}
diff --git a/man/update_filter.Rd b/man/update_filter.Rd
new file mode 100644
index 0000000..b03d876
--- /dev/null
+++ b/man/update_filter.Rd
@@ -0,0 +1,45 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/filters.R
+\name{update_filter}
+\alias{update_filter}
+\title{Update a filter}
+\usage{
+update_filter(
+ filter_id = get_filter_id(filter_name = filter_name, token = token),
+ filter_name,
+ new_name = NULL,
+ query = NULL,
+ color = NULL,
+ is_favorite = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{filter_id}{id of the filter}
+
+\item{filter_name}{name of the filter (for lookup if filter_id not provided)}
+
+\item{new_name}{new name for the filter}
+
+\item{query}{new query string}
+
+\item{color}{new color}
+
+\item{is_favorite}{boolean to mark as favorite}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated filter (invisible)
+}
+\description{
+Update a filter
+}
+\examples{
+\dontrun{
+update_filter(filter_name = "Urgent Today", query = "today & p1 & #Work")
+}
+}
diff --git a/man/update_label.Rd b/man/update_label.Rd
new file mode 100644
index 0000000..49d7581
--- /dev/null
+++ b/man/update_label.Rd
@@ -0,0 +1,42 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/labels.R
+\name{update_label}
+\alias{update_label}
+\title{Update a label}
+\usage{
+update_label(
+ label_id = get_label_id(label_name = label_name, token = token, create = FALSE),
+ label_name,
+ new_name = NULL,
+ color = NULL,
+ is_favorite = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{label_id}{id of the label}
+
+\item{label_name}{name of the label (for lookup if label_id not provided)}
+
+\item{new_name}{new name for the label}
+
+\item{color}{new color for the label}
+
+\item{is_favorite}{boolean to mark as favorite}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated label (invisible)
+}
+\description{
+Update a label
+}
+\examples{
+\dontrun{
+update_label(label_name = "urgent", new_name = "very_urgent")
+}
+}
diff --git a/man/update_project.Rd b/man/update_project.Rd
new file mode 100644
index 0000000..f0e5219
--- /dev/null
+++ b/man/update_project.Rd
@@ -0,0 +1,45 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/projects.R
+\name{update_project}
+\alias{update_project}
+\title{Update a project}
+\usage{
+update_project(
+ project_id = get_project_id(project_name = project_name, token = token, create = FALSE),
+ project_name,
+ new_name = NULL,
+ color = NULL,
+ is_favorite = NULL,
+ view_style = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{project_id}{id of the project}
+
+\item{project_name}{name of the project (for lookup if project_id not provided)}
+
+\item{new_name}{new name for the project}
+
+\item{color}{new color for the project}
+
+\item{is_favorite}{boolean to mark as favorite}
+
+\item{view_style}{view style ("list" or "board")}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated project (invisible)
+}
+\description{
+Update a project
+}
+\examples{
+\dontrun{
+update_project(project_name = "my_proj", new_name = "new_name")
+}
+}
diff --git a/man/update_reminder.Rd b/man/update_reminder.Rd
new file mode 100644
index 0000000..0d259d8
--- /dev/null
+++ b/man/update_reminder.Rd
@@ -0,0 +1,42 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reminders.R
+\name{update_reminder}
+\alias{update_reminder}
+\title{Update a reminder}
+\usage{
+update_reminder(
+ reminder_id,
+ due_date = NULL,
+ due_datetime = NULL,
+ minute_offset = NULL,
+ type = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{reminder_id}{id of the reminder}
+
+\item{due_date}{new due date for the reminder (format: YYYY-MM-DD)}
+
+\item{due_datetime}{new due datetime for the reminder (format: YYYY-MM-DDTHH:MM:SS)}
+
+\item{minute_offset}{new minutes before due time to remind}
+
+\item{type}{new type of reminder}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated reminder (invisible)
+}
+\description{
+Update a reminder
+}
+\examples{
+\dontrun{
+update_reminder("12345", due_datetime = "2024-12-26T10:00:00")
+}
+}
diff --git a/man/update_section.Rd b/man/update_section.Rd
new file mode 100644
index 0000000..8679eab
--- /dev/null
+++ b/man/update_section.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/section.R
+\name{update_section}
+\alias{update_section}
+\title{Update a section}
+\usage{
+update_section(
+ section_id,
+ new_name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{section_id}{id of the section}
+
+\item{new_name}{new name for the section}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated section (invisible)
+}
+\description{
+Update a section
+}
+\examples{
+\dontrun{
+update_section("12345", new_name = "New Section Name")
+}
+}
diff --git a/man/update_task.Rd b/man/update_task.Rd
new file mode 100644
index 0000000..a5764db
--- /dev/null
+++ b/man/update_task.Rd
@@ -0,0 +1,48 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/tasks.R
+\name{update_task}
+\alias{update_task}
+\title{Update a task}
+\usage{
+update_task(
+ task_id,
+ content = NULL,
+ description = NULL,
+ priority = NULL,
+ due_string = NULL,
+ due_date = NULL,
+ labels = NULL,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{task_id}{id of the task to update}
+
+\item{content}{new content/title for the task}
+
+\item{description}{new description for the task}
+
+\item{priority}{priority (1-4, 4 being highest)}
+
+\item{due_string}{due date as string (e.g., "tomorrow", "every monday")}
+
+\item{due_date}{due date as date (format: YYYY-MM-DD)}
+
+\item{labels}{vector of label names}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated task (invisible)
+}
+\description{
+Update a task
+}
+\examples{
+\dontrun{
+update_task("12345", content = "Updated task name")
+}
+}
diff --git a/man/update_workspace.Rd b/man/update_workspace.Rd
new file mode 100644
index 0000000..6ac64c3
--- /dev/null
+++ b/man/update_workspace.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/workspaces.R
+\name{update_workspace}
+\alias{update_workspace}
+\title{Update a workspace}
+\usage{
+update_workspace(
+ workspace_id,
+ name,
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{workspace_id}{id of the workspace}
+
+\item{name}{new name for the workspace}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+id of the updated workspace (invisible)
+}
+\description{
+Update a workspace
+}
+\examples{
+\dontrun{
+update_workspace("12345", name = "New Workspace Name")
+}
+}
diff --git a/man/upload_file.Rd b/man/upload_file.Rd
new file mode 100644
index 0000000..baf6b5a
--- /dev/null
+++ b/man/upload_file.Rd
@@ -0,0 +1,34 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/uploads.R
+\name{upload_file}
+\alias{upload_file}
+\title{Upload a file}
+\usage{
+upload_file(
+ file_path,
+ file_name = basename(file_path),
+ verbose = TRUE,
+ token = get_todoist_api_token()
+)
+}
+\arguments{
+\item{file_path}{path to the file to upload}
+
+\item{file_name}{name for the uploaded file (defaults to basename of file_path)}
+
+\item{verbose}{boolean that make the function verbose}
+
+\item{token}{todoist API token}
+}
+\value{
+upload object with file_url
+}
+\description{
+Upload a file
+}
+\examples{
+\dontrun{
+upload <- upload_file("document.pdf")
+# Use upload$file_url in a comment
+}
+}
diff --git a/tests/testthat/fixtures/activity_logs_response.json b/tests/testthat/fixtures/activity_logs_response.json
new file mode 100644
index 0000000..632b0d1
--- /dev/null
+++ b/tests/testthat/fixtures/activity_logs_response.json
@@ -0,0 +1,34 @@
+{
+ "events": [
+ {
+ "id": "evt_001",
+ "object_type": "item",
+ "object_id": "task_123",
+ "event_type": "added",
+ "event_date": "2024-01-15T10:30:00Z",
+ "initiator_id": "user_001",
+ "parent_project_id": "proj_456",
+ "parent_item_id": null
+ },
+ {
+ "id": "evt_002",
+ "object_type": "item",
+ "object_id": "task_123",
+ "event_type": "completed",
+ "event_date": "2024-01-15T14:00:00Z",
+ "initiator_id": "user_001",
+ "parent_project_id": "proj_456",
+ "parent_item_id": null
+ },
+ {
+ "id": "evt_003",
+ "object_type": "project",
+ "object_id": "proj_456",
+ "event_type": "updated",
+ "event_date": "2024-01-16T09:00:00Z",
+ "initiator_id": "user_002",
+ "parent_project_id": null,
+ "parent_item_id": null
+ }
+ ]
+}
diff --git a/tests/testthat/fixtures/backups_response.json b/tests/testthat/fixtures/backups_response.json
new file mode 100644
index 0000000..e70a868
--- /dev/null
+++ b/tests/testthat/fixtures/backups_response.json
@@ -0,0 +1,16 @@
+{
+ "results": [
+ {
+ "version": "2024-01-15_10-30",
+ "url": "https://todoist.com/backups/backup1.zip"
+ },
+ {
+ "version": "2024-01-14_10-30",
+ "url": "https://todoist.com/backups/backup2.zip"
+ },
+ {
+ "version": "2024-01-13_10-30",
+ "url": "https://todoist.com/backups/backup3.zip"
+ }
+ ]
+}
diff --git a/tests/testthat/fixtures/comments_response.json b/tests/testthat/fixtures/comments_response.json
new file mode 100644
index 0000000..0e7f722
--- /dev/null
+++ b/tests/testthat/fixtures/comments_response.json
@@ -0,0 +1,19 @@
+{
+ "results": [
+ {
+ "id": "2992679862",
+ "content": "This is a test comment",
+ "task_id": "6001106591",
+ "project_id": null,
+ "posted_at": "2024-01-15T10:30:00Z"
+ },
+ {
+ "id": "2992679863",
+ "content": "Another comment here",
+ "task_id": "6001106591",
+ "project_id": null,
+ "posted_at": "2024-01-15T11:00:00Z"
+ }
+ ],
+ "next_cursor": null
+}
diff --git a/tests/testthat/fixtures/completed_tasks_response.json b/tests/testthat/fixtures/completed_tasks_response.json
new file mode 100644
index 0000000..40448f9
--- /dev/null
+++ b/tests/testthat/fixtures/completed_tasks_response.json
@@ -0,0 +1,17 @@
+{
+ "items": [
+ {
+ "id": "7001106591",
+ "content": "Completed Task 1",
+ "project_id": "2244736524",
+ "completed_at": "2024-01-15T10:00:00Z"
+ },
+ {
+ "id": "7001106592",
+ "content": "Completed Task 2",
+ "project_id": "2244736524",
+ "completed_at": "2024-01-14T15:30:00Z"
+ }
+ ],
+ "next_cursor": null
+}
diff --git a/tests/testthat/fixtures/filters_sync_response.json b/tests/testthat/fixtures/filters_sync_response.json
new file mode 100644
index 0000000..a063594
--- /dev/null
+++ b/tests/testthat/fixtures/filters_sync_response.json
@@ -0,0 +1,22 @@
+{
+ "full_sync": true,
+ "sync_token": "xyz123",
+ "filters": [
+ {
+ "id": "1234567",
+ "name": "Urgent Today",
+ "query": "today & p1",
+ "color": "red",
+ "order": 1,
+ "is_favorite": true
+ },
+ {
+ "id": "1234568",
+ "name": "Work Tasks",
+ "query": "#Work",
+ "color": "blue",
+ "order": 2,
+ "is_favorite": false
+ }
+ ]
+}
diff --git a/tests/testthat/fixtures/labels_response.json b/tests/testthat/fixtures/labels_response.json
new file mode 100644
index 0000000..d0517ad
--- /dev/null
+++ b/tests/testthat/fixtures/labels_response.json
@@ -0,0 +1,26 @@
+{
+ "results": [
+ {
+ "id": "2156154810",
+ "name": "urgent",
+ "color": "red",
+ "order": 1,
+ "is_favorite": true
+ },
+ {
+ "id": "2156154811",
+ "name": "work",
+ "color": "blue",
+ "order": 2,
+ "is_favorite": false
+ },
+ {
+ "id": "2156154812",
+ "name": "personal",
+ "color": "green",
+ "order": 3,
+ "is_favorite": false
+ }
+ ],
+ "next_cursor": null
+}
diff --git a/tests/testthat/fixtures/reminders_sync_response.json b/tests/testthat/fixtures/reminders_sync_response.json
new file mode 100644
index 0000000..a9ffc88
--- /dev/null
+++ b/tests/testthat/fixtures/reminders_sync_response.json
@@ -0,0 +1,23 @@
+{
+ "full_sync": true,
+ "sync_token": "abc789",
+ "reminders": [
+ {
+ "id": "9876543",
+ "item_id": "6001106591",
+ "type": "absolute",
+ "due": {
+ "date": "2024-12-25T09:00:00",
+ "is_recurring": false
+ },
+ "minute_offset": null
+ },
+ {
+ "id": "9876544",
+ "item_id": "6001106592",
+ "type": "relative",
+ "due": null,
+ "minute_offset": 30
+ }
+ ]
+}
diff --git a/tests/testthat/fixtures/user_sync_response.json b/tests/testthat/fixtures/user_sync_response.json
new file mode 100644
index 0000000..fef2e10
--- /dev/null
+++ b/tests/testthat/fixtures/user_sync_response.json
@@ -0,0 +1,21 @@
+{
+ "full_sync": true,
+ "sync_token": "user123",
+ "user": {
+ "id": "12345678",
+ "email": "user@example.com",
+ "full_name": "Test User",
+ "inbox_project_id": "2203306141",
+ "team_inbox_id": null,
+ "karma": 1500,
+ "karma_trend": "up",
+ "completed_today": 5,
+ "days_items": [
+ {"date": "2024-01-15", "count": 10},
+ {"date": "2024-01-14", "count": 8}
+ ],
+ "week_items": [
+ {"date": "2024-01-15", "count": 45}
+ ]
+ }
+}
diff --git a/tests/testthat/fixtures/workspace_users_response.json b/tests/testthat/fixtures/workspace_users_response.json
new file mode 100644
index 0000000..9f1c8b5
--- /dev/null
+++ b/tests/testthat/fixtures/workspace_users_response.json
@@ -0,0 +1,19 @@
+{
+ "workspace_users": [
+ {
+ "user_id": "user_001",
+ "workspace_id": "ws_123456",
+ "name": "John Doe",
+ "email": "john@example.com",
+ "role": "admin"
+ },
+ {
+ "user_id": "user_002",
+ "workspace_id": "ws_123456",
+ "name": "Jane Smith",
+ "email": "jane@example.com",
+ "role": "member"
+ }
+ ],
+ "sync_token": "def456"
+}
diff --git a/tests/testthat/fixtures/workspaces_response.json b/tests/testthat/fixtures/workspaces_response.json
new file mode 100644
index 0000000..46a075c
--- /dev/null
+++ b/tests/testthat/fixtures/workspaces_response.json
@@ -0,0 +1,15 @@
+{
+ "workspaces": [
+ {
+ "id": "ws_123456",
+ "name": "My Workspace",
+ "is_default": true
+ },
+ {
+ "id": "ws_789012",
+ "name": "Team Workspace",
+ "is_default": false
+ }
+ ],
+ "sync_token": "abc123"
+}
diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R
index 26a5112..e7b7efa 100644
--- a/tests/testthat/helper.R
+++ b/tests/testthat/helper.R
@@ -1,5 +1,8 @@
# Helper functions for rtodoist tests
+# Define null coalescing operator for tests
+`%||%` <- function(a, b) if (is.null(a)) b else a
+
# Skip tests if no API token is available
skip_if_no_token <- function() {
token <- tryCatch(
@@ -23,10 +26,41 @@ TEST_PROJECT_NAME <- "depuisclaude"
# Test collaborators
TEST_COLLABORATORS <- c("vincent@thinkr.fr", "murielle@thinkr.fr")
-# Clean up function to delete test project (to be used in teardown)
-cleanup_test_project <- function(project_name = TEST_PROJECT_NAME) {
- # Note: rtodoist doesn't have a delete_project function
+# Skip if test project doesn't exist
+skip_if_test_project_missing <- function(project_name = TEST_PROJECT_NAME) {
+ token <- tryCatch(
+ get_todoist_api_token(ask = FALSE),
+ error = function(e) NULL
+ )
+ if (is.null(token)) {
+ skip("No Todoist API token available")
+ }
- # Manual cleanup may be needed
- message("Remember to manually delete project: ", project_name)
+ project_exists <- tryCatch({
+ get_project_id(project_name, token = token, create = FALSE)
+ TRUE
+ }, error = function(e) FALSE)
+
+ if (!project_exists) {
+ skip(paste0("Test project '", project_name, "' does not exist"))
+ }
+}
+
+# Clean up function to delete test project (to be used in teardown)
+cleanup_test_project <- function(project_name = TEST_PROJECT_NAME, token = NULL) {
+ if (is.null(token)) {
+ token <- tryCatch(
+ get_todoist_api_token(ask = FALSE),
+ error = function(e) NULL
+ )
+ }
+ if (!is.null(token)) {
+ tryCatch({
+ project_id <- get_project_id(project_name, token = token, create = FALSE)
+ delete_project(project_id, token = token, verbose = FALSE)
+ message("Deleted test project: ", project_name)
+ }, error = function(e) {
+ message("Could not delete project: ", e$message)
+ })
+ }
}
diff --git a/tests/testthat/test-activity.R b/tests/testthat/test-activity.R
new file mode 100644
index 0000000..88c4d80
--- /dev/null
+++ b/tests/testthat/test-activity.R
@@ -0,0 +1,119 @@
+# Tests for activity logs functions
+
+library(rtodoist)
+
+# Load fixtures
+activity_json <- jsonlite::read_json(
+ test_path("fixtures", "activity_logs_response.json")
+)
+
+test_that("activity logs dataframe has correct structure from fixture", {
+ events <- activity_json$events
+
+ events_df <- purrr::map_dfr(events, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ object_type = x$object_type %||% NA_character_,
+ object_id = x$object_id %||% NA_character_,
+ event_type = x$event_type %||% NA_character_,
+ event_date = x$event_date %||% NA_character_,
+ initiator_id = x$initiator_id %||% NA_character_,
+ parent_project_id = x$parent_project_id %||% NA_character_,
+ parent_item_id = x$parent_item_id %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(events_df))
+ expect_true("object_type" %in% names(events_df))
+ expect_true("object_id" %in% names(events_df))
+ expect_true("event_type" %in% names(events_df))
+ expect_true("event_date" %in% names(events_df))
+ expect_true("initiator_id" %in% names(events_df))
+ expect_equal(nrow(events_df), 3)
+})
+
+test_that("activity logs contain expected event types", {
+ events <- activity_json$events
+
+ events_df <- purrr::map_dfr(events, function(x) {
+ data.frame(
+ id = x$id,
+ event_type = x$event_type,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("added" %in% events_df$event_type)
+ expect_true("completed" %in% events_df$event_type)
+ expect_true("updated" %in% events_df$event_type)
+})
+
+test_that("activity logs contain expected object types", {
+ events <- activity_json$events
+
+ object_types <- sapply(events, function(x) x$object_type)
+
+ expect_true("item" %in% object_types)
+ expect_true("project" %in% object_types)
+})
+
+test_that("empty activity logs returns empty dataframe", {
+ empty_response <- list(events = list())
+
+ result <- data.frame(
+ id = character(),
+ object_type = character(),
+ object_id = character(),
+ event_type = character(),
+ event_date = character(),
+ initiator_id = character(),
+ parent_project_id = character(),
+ parent_item_id = character(),
+ stringsAsFactors = FALSE
+ )
+
+ expect_equal(nrow(result), 0)
+ expect_true("id" %in% names(result))
+ expect_true("event_type" %in% names(result))
+ expect_true("initiator_id" %in% names(result))
+ expect_true("parent_project_id" %in% names(result))
+ expect_true("parent_item_id" %in% names(result))
+})
+
+# Integration tests (require API token and premium account)
+# Note: activity_logs endpoint may require premium/business account
+test_that("get_activity_logs returns dataframe", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+ skip("Activity logs endpoint requires premium account")
+
+ logs <- get_activity_logs(limit = 10)
+
+ expect_s3_class(logs, "data.frame")
+ expect_true("id" %in% names(logs) || nrow(logs) == 0)
+})
+
+test_that("get_activity_logs respects limit parameter", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+ skip("Activity logs endpoint requires premium account")
+
+ logs <- get_activity_logs(limit = 5)
+
+ expect_s3_class(logs, "data.frame")
+ expect_lte(nrow(logs), 5)
+})
+
+test_that("get_activity_logs filters by object_type", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+ skip("Activity logs endpoint requires premium account")
+
+ logs <- get_activity_logs(object_type = "item", limit = 10)
+
+ expect_s3_class(logs, "data.frame")
+ if (nrow(logs) > 0) {
+ expect_true(all(logs$object_type == "item"))
+ }
+})
diff --git a/tests/testthat/test-backups.R b/tests/testthat/test-backups.R
new file mode 100644
index 0000000..65c0908
--- /dev/null
+++ b/tests/testthat/test-backups.R
@@ -0,0 +1,80 @@
+# Tests for backups functions
+
+library(rtodoist)
+
+# Load fixtures
+backups_json <- jsonlite::read_json(
+ test_path("fixtures", "backups_response.json")
+)
+
+test_that("backups dataframe has correct structure from fixture", {
+ backups <- backups_json$results
+
+ backups_df <- purrr::map_dfr(backups, function(x) {
+ data.frame(
+ version = x$version %||% NA_character_,
+ url = x$url %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("version" %in% names(backups_df))
+ expect_true("url" %in% names(backups_df))
+ expect_equal(nrow(backups_df), 3)
+})
+
+test_that("backups contain valid version strings", {
+ backups <- backups_json$results
+
+ versions <- sapply(backups, function(x) x$version)
+
+ expect_true(all(grepl("^\\d{4}-\\d{2}-\\d{2}", versions)))
+})
+
+test_that("backups contain valid URLs", {
+ backups <- backups_json$results
+
+ urls <- sapply(backups, function(x) x$url)
+
+ expect_true(all(grepl("^https://", urls)))
+})
+
+test_that("empty backups returns empty dataframe", {
+ empty_response <- list(results = list())
+
+ if (length(empty_response$results) == 0) {
+ result <- data.frame(
+ version = character(),
+ url = character(),
+ stringsAsFactors = FALSE
+ )
+ }
+
+ expect_equal(nrow(result), 0)
+ expect_true("version" %in% names(result))
+ expect_true("url" %in% names(result))
+})
+
+test_that("download_backup validates version exists", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+
+ # This should error because the version doesn't exist
+
+ expect_error(
+ download_backup("nonexistent_version", tempfile(), verbose = FALSE),
+ "Backup version not found"
+ )
+})
+
+# Integration tests (require API token)
+test_that("get_backups returns dataframe", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+
+ backups <- get_backups()
+
+ expect_s3_class(backups, "data.frame")
+ expect_true("version" %in% names(backups) || nrow(backups) == 0)
+ expect_true("url" %in% names(backups) || nrow(backups) == 0)
+})
diff --git a/tests/testthat/test-comments.R b/tests/testthat/test-comments.R
new file mode 100644
index 0000000..2a9e102
--- /dev/null
+++ b/tests/testthat/test-comments.R
@@ -0,0 +1,50 @@
+# Tests for comments functions
+
+library(rtodoist)
+
+# Load fixtures
+comments_json <- jsonlite::read_json(
+ test_path("fixtures", "comments_response.json")
+)
+
+test_that("comments dataframe has correct structure", {
+ comments_df <- purrr::map_dfr(comments_json$results, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ content = x$content %||% NA_character_,
+ task_id = x$task_id %||% NA_character_,
+ project_id = x$project_id %||% NA_character_,
+ posted_at = x$posted_at %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(comments_df))
+ expect_true("content" %in% names(comments_df))
+ expect_true("task_id" %in% names(comments_df))
+ expect_true("posted_at" %in% names(comments_df))
+ expect_equal(nrow(comments_df), 2)
+})
+
+test_that("add_comment requires task_id or project_id", {
+ expect_error(
+ add_comment(
+ content = "Test comment",
+ task_id = NULL,
+ project_id = NULL,
+ token = "fake_token"
+ ),
+ "Either task_id or project_id must be provided"
+ )
+})
+
+test_that("get_comments requires task_id or project_id", {
+ expect_error(
+ get_comments(
+ task_id = NULL,
+ project_id = NULL,
+ token = "fake_token"
+ ),
+ "Either task_id or project_id must be provided"
+ )
+})
diff --git a/tests/testthat/test-filters.R b/tests/testthat/test-filters.R
new file mode 100644
index 0000000..dd6f99d
--- /dev/null
+++ b/tests/testthat/test-filters.R
@@ -0,0 +1,70 @@
+# Tests for filters functions
+
+library(rtodoist)
+
+# Load fixtures
+filters_json <- jsonlite::read_json(
+ test_path("fixtures", "filters_sync_response.json")
+)
+
+test_that("filters dataframe has correct structure", {
+ filters_df <- purrr::map_dfr(filters_json$filters, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ query = x$query %||% NA_character_,
+ color = x$color %||% NA_character_,
+ is_favorite = x$is_favorite %||% FALSE,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(filters_df))
+ expect_true("name" %in% names(filters_df))
+ expect_true("query" %in% names(filters_df))
+ expect_true("color" %in% names(filters_df))
+ expect_true("is_favorite" %in% names(filters_df))
+ expect_equal(nrow(filters_df), 2)
+})
+
+test_that("get_filter_id extracts filter id from list", {
+ filters_df <- purrr::map_dfr(filters_json$filters, function(x) {
+ data.frame(
+ id = x$id,
+ name = x$name,
+ query = x$query,
+ color = x$color,
+ is_favorite = x$is_favorite,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ result <- get_filter_id(
+ filter_name = "Urgent Today",
+ all_filters = filters_df,
+ token = "fake_token"
+ )
+ expect_equal(result, "1234567")
+})
+
+test_that("get_filter_id returns error for non-existent filter", {
+ filters_df <- purrr::map_dfr(filters_json$filters, function(x) {
+ data.frame(
+ id = x$id,
+ name = x$name,
+ query = x$query,
+ color = x$color,
+ is_favorite = x$is_favorite,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_error(
+ get_filter_id(
+ filter_name = "NonExistent",
+ all_filters = filters_df,
+ token = "fake_token"
+ ),
+ "Filter not found"
+ )
+})
diff --git a/tests/testthat/test-labels.R b/tests/testthat/test-labels.R
new file mode 100644
index 0000000..7e83283
--- /dev/null
+++ b/tests/testthat/test-labels.R
@@ -0,0 +1,69 @@
+# Tests for labels functions
+
+library(rtodoist)
+
+# Load fixtures
+labels_json <- jsonlite::read_json(
+ test_path("fixtures", "labels_response.json")
+)
+
+test_that("get_label_id extracts label id from list", {
+ # Create a mock labels dataframe
+ labels_df <- purrr::map_dfr(labels_json$results, function(x) {
+ data.frame(
+ id = x$id,
+ name = x$name,
+ color = x$color,
+ is_favorite = x$is_favorite,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ result <- get_label_id(
+ label_name = "urgent",
+ all_labels = labels_df,
+ token = "fake_token",
+ create = FALSE
+ )
+ expect_equal(result, "2156154810")
+})
+
+test_that("get_label_id returns error for non-existent label when create=FALSE", {
+ labels_df <- purrr::map_dfr(labels_json$results, function(x) {
+ data.frame(
+ id = x$id,
+ name = x$name,
+ color = x$color,
+ is_favorite = x$is_favorite,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_error(
+ get_label_id(
+ label_name = "NonExistent",
+ all_labels = labels_df,
+ token = "fake_token",
+ create = FALSE
+ ),
+ "Label not found"
+ )
+})
+
+test_that("labels dataframe has correct structure", {
+ labels_df <- purrr::map_dfr(labels_json$results, function(x) {
+ data.frame(
+ id = x$id,
+ name = x$name,
+ color = x$color,
+ is_favorite = x$is_favorite,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(labels_df))
+ expect_true("name" %in% names(labels_df))
+ expect_true("color" %in% names(labels_df))
+ expect_true("is_favorite" %in% names(labels_df))
+ expect_equal(nrow(labels_df), 3)
+})
diff --git a/tests/testthat/test-projects-extended.R b/tests/testthat/test-projects-extended.R
new file mode 100644
index 0000000..fb3e162
--- /dev/null
+++ b/tests/testthat/test-projects-extended.R
@@ -0,0 +1,49 @@
+# Tests for extended project functions
+
+library(rtodoist)
+
+# Load fixtures
+projects_json <- jsonlite::read_json(
+ test_path("fixtures", "projects_response.json")
+)
+
+test_that("projects dataframe has correct structure", {
+ projects_df <- purrr::map_dfr(projects_json$projects, function(x) {
+ data.frame(
+ id = x$id,
+ name = x$name,
+ color = x$color,
+ is_favorite = x$is_favorite,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(projects_df))
+ expect_true("name" %in% names(projects_df))
+ expect_true("color" %in% names(projects_df))
+ expect_true("is_favorite" %in% names(projects_df))
+ expect_equal(nrow(projects_df), 2)
+})
+
+test_that("get_project_id handles multiple projects", {
+ result <- get_project_id(
+ project_name = "TestProject",
+ all_projects = projects_json,
+ token = "fake_token",
+ create = FALSE
+ )
+ expect_equal(result, "2244736524")
+})
+
+test_that("get_archived_projects returns empty dataframe when no results", {
+ empty_response <- list(results = list(), next_cursor = NULL)
+
+ # Simulate empty response parsing
+ if (length(empty_response$results) == 0) {
+ result <- data.frame(id = character(), name = character(), stringsAsFactors = FALSE)
+ }
+
+ expect_equal(nrow(result), 0)
+ expect_true("id" %in% names(result))
+ expect_true("name" %in% names(result))
+})
diff --git a/tests/testthat/test-projects.R b/tests/testthat/test-projects.R
index e5e382f..049a13b 100644
--- a/tests/testthat/test-projects.R
+++ b/tests/testthat/test-projects.R
@@ -14,6 +14,7 @@ test_that("get_all_projects returns a list with projects", {
test_that("get_project_id returns project ID for existing project", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
# Use the test project
proj_id <- get_project_id(
@@ -29,6 +30,7 @@ test_that("get_project_id returns project ID for existing project", {
test_that("get_project_id with create=TRUE returns same ID for existing project", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id1 <- get_project_id(
project_name = TEST_PROJECT_NAME,
diff --git a/tests/testthat/test-reminders.R b/tests/testthat/test-reminders.R
new file mode 100644
index 0000000..5c60ed0
--- /dev/null
+++ b/tests/testthat/test-reminders.R
@@ -0,0 +1,42 @@
+# Tests for reminders functions
+
+library(rtodoist)
+
+# Load fixtures
+reminders_json <- jsonlite::read_json(
+ test_path("fixtures", "reminders_sync_response.json")
+)
+
+test_that("reminders dataframe has correct structure", {
+ reminders_df <- purrr::map_dfr(reminders_json$reminders, function(x) {
+ due_date <- if (!is.null(x$due)) x$due$date else NA_character_
+ data.frame(
+ id = x$id %||% NA_character_,
+ item_id = x$item_id %||% NA_character_,
+ type = x$type %||% NA_character_,
+ due_date = due_date,
+ minute_offset = x$minute_offset %||% NA_integer_,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(reminders_df))
+ expect_true("item_id" %in% names(reminders_df))
+ expect_true("type" %in% names(reminders_df))
+ expect_true("due_date" %in% names(reminders_df))
+ expect_true("minute_offset" %in% names(reminders_df))
+ expect_equal(nrow(reminders_df), 2)
+})
+
+test_that("reminders have correct types", {
+ reminders_df <- purrr::map_dfr(reminders_json$reminders, function(x) {
+ data.frame(
+ id = x$id,
+ type = x$type,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("absolute" %in% reminders_df$type)
+ expect_true("relative" %in% reminders_df$type)
+})
diff --git a/tests/testthat/test-sections.R b/tests/testthat/test-sections.R
index b7c67e2..2241626 100644
--- a/tests/testthat/test-sections.R
+++ b/tests/testthat/test-sections.R
@@ -3,6 +3,7 @@
test_that("add_section creates a section and returns ID", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_section <- paste0("Test_Section_", format(Sys.time(), "%H%M%S"))
@@ -19,6 +20,7 @@ test_that("add_section creates a section and returns ID", {
test_that("add_section with force=FALSE returns existing section ID", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
section_name <- "Section A - Preparation"
@@ -41,6 +43,7 @@ test_that("add_section with force=FALSE returns existing section ID", {
test_that("get_section_id returns correct ID for existing section", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
@@ -64,6 +67,7 @@ test_that("get_section_id returns correct ID for existing section", {
test_that("get_section_id returns vector for multiple sections", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
@@ -79,6 +83,7 @@ test_that("get_section_id returns vector for multiple sections", {
test_that("get_section_id returns 0 for non-existent section", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
diff --git a/tests/testthat/test-tasks-extended.R b/tests/testthat/test-tasks-extended.R
new file mode 100644
index 0000000..d39b9fa
--- /dev/null
+++ b/tests/testthat/test-tasks-extended.R
@@ -0,0 +1,56 @@
+# Tests for extended task functions
+
+library(rtodoist)
+
+# Load fixtures
+completed_json <- jsonlite::read_json(
+ test_path("fixtures", "completed_tasks_response.json")
+)
+
+test_that("completed tasks dataframe has correct structure", {
+ completed_df <- purrr::map_dfr(completed_json$items, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ content = x$content %||% NA_character_,
+ project_id = x$project_id %||% NA_character_,
+ completed_at = x$completed_at %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(completed_df))
+ expect_true("content" %in% names(completed_df))
+ expect_true("project_id" %in% names(completed_df))
+ expect_true("completed_at" %in% names(completed_df))
+ expect_equal(nrow(completed_df), 2)
+})
+
+test_that("get_completed_tasks returns empty dataframe when no results", {
+ empty_response <- list(items = list(), next_cursor = NULL)
+
+ # Simulate empty response parsing
+ if (length(empty_response$items) == 0) {
+ result <- data.frame(
+ id = character(),
+ content = character(),
+ project_id = character(),
+ completed_at = character(),
+ stringsAsFactors = FALSE
+ )
+ }
+
+ expect_equal(nrow(result), 0)
+ expect_true("id" %in% names(result))
+})
+
+test_that("escape_json handles special characters", {
+ # Test escape function
+ result <- rtodoist:::escape_json('Test "quoted" string')
+ expect_true(grepl('\\\\"', result))
+
+ result <- rtodoist:::escape_json("Test\nnewline")
+ expect_true(grepl("\\\\n", result))
+
+ result <- rtodoist:::escape_json("Test\ttab")
+ expect_true(grepl("\\\\t", result))
+})
diff --git a/tests/testthat/test-tasks.R b/tests/testthat/test-tasks.R
index 1958cef..8ec15b9 100644
--- a/tests/testthat/test-tasks.R
+++ b/tests/testthat/test-tasks.R
@@ -12,6 +12,7 @@ test_that("get_tasks returns a list", {
test_that("add_tasks_in_project adds simple tasks", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_task <- paste0("Test_Task_", format(Sys.time(), "%H%M%S"))
@@ -33,6 +34,7 @@ test_that("add_tasks_in_project adds simple tasks", {
test_that("add_tasks_in_project adds tasks with responsible and assigns correctly", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_task <- paste0("Task_With_Responsible_", format(Sys.time(), "%H%M%S"))
@@ -72,6 +74,7 @@ test_that("add_tasks_in_project adds tasks with responsible and assigns correctl
test_that("add_tasks_in_project adds tasks with due date", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_task <- paste0("Task_With_Due_", format(Sys.time(), "%H%M%S"))
@@ -90,6 +93,7 @@ test_that("add_tasks_in_project adds tasks with due date", {
test_that("add_tasks_in_project adds tasks with section", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_task <- paste0("Task_With_Section_", format(Sys.time(), "%H%M%S"))
@@ -107,6 +111,7 @@ test_that("add_tasks_in_project adds tasks with section", {
test_that("add_tasks_in_project check_only returns dataframe without creating tasks", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_task <- paste0("Check_Only_Task_", format(Sys.time(), "%H%M%S"))
@@ -134,6 +139,7 @@ test_that("add_tasks_in_project check_only returns dataframe without creating ta
test_that("add_tasks_in_project_from_df adds tasks from dataframe", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
timestamp <- format(Sys.time(), "%H%M%S")
@@ -164,6 +170,7 @@ test_that("add_tasks_in_project_from_df adds tasks from dataframe", {
test_that("get_tasks_of_project returns tasks for specific project", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
@@ -184,6 +191,7 @@ test_that("get_tasks_of_project returns tasks for specific project", {
test_that("add_responsible_to_task assigns user to existing task", {
skip_on_cran()
skip_if_no_token()
+ skip_if_test_project_missing()
proj_id <- get_project_id(TEST_PROJECT_NAME, create = FALSE)
unique_task <- paste0("Task_For_Assignment_", format(Sys.time(), "%H%M%S"))
diff --git a/tests/testthat/test-templates.R b/tests/testthat/test-templates.R
new file mode 100644
index 0000000..9053a40
--- /dev/null
+++ b/tests/testthat/test-templates.R
@@ -0,0 +1,99 @@
+# Tests for templates functions
+
+library(rtodoist)
+
+test_that("import_template validates file existence", {
+ expect_error(
+ import_template(
+ project_id = "12345",
+ file_path = "nonexistent_file.csv",
+ verbose = FALSE,
+ token = "fake_token"
+ ),
+ "Template file not found"
+ )
+})
+
+test_that("import_template accepts valid file path parameter", {
+ # Create a temporary CSV file
+ temp_file <- tempfile(fileext = ".csv")
+ writeLines("TYPE,CONTENT,PRIORITY", temp_file)
+
+ # The function should not error on file validation
+ # It will error on API call since we don't have a real token
+ expect_error(
+ import_template(
+ project_id = "12345",
+ file_path = temp_file,
+ verbose = FALSE,
+ token = "fake_token"
+ )
+ )
+
+ unlink(temp_file)
+})
+
+test_that("export_template requires project_id or project_name", {
+ # Without a valid token/project, this will error
+ expect_error(
+ export_template(
+ project_id = "nonexistent",
+ output_file = tempfile(),
+ verbose = FALSE,
+ token = "fake_token"
+ )
+ )
+})
+
+# Integration tests (require API token)
+# Note: Templates API may not be available for all account types
+test_that("export_template creates file for valid project", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+ skip("Templates API endpoint may not be available")
+
+ # Get an existing project
+ projects <- get_all_projects()
+ skip_if(nrow(projects) == 0, "No projects available for testing")
+
+ output_file <- tempfile(fileext = ".csv")
+
+ result <- export_template(
+ project_id = projects$id[1],
+ output_file = output_file,
+ verbose = FALSE
+ )
+
+ expect_true(file.exists(output_file))
+ expect_equal(result, output_file)
+
+ unlink(output_file)
+})
+
+test_that("import and export template round-trip works", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+ skip("Templates API endpoint may not be available")
+
+ # Create a test project
+ test_project_name <- paste0("rtodoist_test_template_", random_key())
+ project_id <- add_project(test_project_name, verbose = FALSE)
+
+ # Add a task to export
+ add_tasks_in_project(
+ project_id = project_id,
+ tasks = "Test task for template",
+ verbose = FALSE
+ )
+
+ # Export the project as template
+ export_file <- tempfile(fileext = ".csv")
+ export_template(project_id = project_id, output_file = export_file, verbose = FALSE)
+
+ expect_true(file.exists(export_file))
+ expect_gt(file.size(export_file), 0)
+
+ # Clean up
+ delete_project(project_id, verbose = FALSE)
+ unlink(export_file)
+})
diff --git a/tests/testthat/test-api-mocked.R b/tests/testthat/test-unit-logic.R
similarity index 92%
rename from tests/testthat/test-api-mocked.R
rename to tests/testthat/test-unit-logic.R
index df6f27a..4b4f66a 100644
--- a/tests/testthat/test-api-mocked.R
+++ b/tests/testthat/test-unit-logic.R
@@ -1,17 +1,8 @@
-# Tests with mocked API responses
+# Unit tests for pure logic functions using fixture data
+# These tests verify function behavior without making HTTP calls
library(rtodoist)
-# Helper to create mock responses
-mock_response <- function(body, status_code = 200L) {
- httptest2::httptest2_response(
- url = "https://api.todoist.com/",
- status_code = status_code,
- headers = list("Content-Type" = "application/json"),
- body = body
- )
-}
-
# Load fixtures
projects_json <- jsonlite::read_json(
test_path("fixtures", "projects_response.json")
diff --git a/tests/testthat/test-uploads.R b/tests/testthat/test-uploads.R
new file mode 100644
index 0000000..4f8b9e8
--- /dev/null
+++ b/tests/testthat/test-uploads.R
@@ -0,0 +1,114 @@
+# Tests for uploads functions
+
+library(rtodoist)
+
+test_that("upload_file validates file existence", {
+ expect_error(
+ upload_file(
+ file_path = "nonexistent_file.pdf",
+ verbose = FALSE,
+ token = "fake_token"
+ ),
+ "File not found"
+ )
+})
+
+test_that("upload_file uses basename as default file_name", {
+ # Create a temporary file
+
+ temp_file <- tempfile(pattern = "test_upload", fileext = ".txt")
+ writeLines("Test content", temp_file)
+
+ # The function should accept the file but will error on API call
+ expect_error(
+ upload_file(
+ file_path = temp_file,
+ verbose = FALSE,
+ token = "fake_token"
+ )
+ )
+
+ unlink(temp_file)
+})
+
+test_that("upload_file accepts custom file_name", {
+ # Create a temporary file
+ temp_file <- tempfile(pattern = "original_name", fileext = ".txt")
+ writeLines("Test content", temp_file)
+
+ # The function should accept custom file name but will error on API call
+ expect_error(
+ upload_file(
+ file_path = temp_file,
+ file_name = "custom_name.txt",
+ verbose = FALSE,
+ token = "fake_token"
+ )
+ )
+
+ unlink(temp_file)
+})
+
+test_that("delete_upload accepts file_url parameter", {
+ # Will error on API call since we don't have a real token
+ expect_error(
+ delete_upload(
+ file_url = "https://example.com/fake_file.pdf",
+ verbose = FALSE,
+ token = "fake_token"
+ )
+ )
+})
+
+# Integration tests (require API token)
+test_that("upload_file uploads and returns file info", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+
+ # Create a temporary file to upload
+ temp_file <- tempfile(pattern = "rtodoist_test_", fileext = ".txt")
+ writeLines("This is a test file for rtodoist upload testing.", temp_file)
+
+ result <- upload_file(
+ file_path = temp_file,
+ verbose = FALSE
+ )
+
+ expect_type(result, "list")
+ expect_true("file_url" %in% names(result) || "url" %in% names(result))
+
+ # Clean up - delete the uploaded file
+ file_url <- result$file_url %||% result$url
+ if (!is.null(file_url)) {
+ try(delete_upload(file_url, verbose = FALSE), silent = TRUE)
+ }
+
+ unlink(temp_file)
+})
+
+test_that("upload and delete cycle works", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+
+ # Create a temporary file
+ temp_file <- tempfile(pattern = "rtodoist_delete_test_", fileext = ".txt")
+ writeLines("Test file for delete testing.", temp_file)
+
+ # Upload
+ upload_result <- upload_file(
+ file_path = temp_file,
+ verbose = FALSE
+ )
+
+ file_url <- upload_result$file_url %||% upload_result$url
+ skip_if(is.null(file_url), "Upload did not return file URL")
+
+
+ # Delete - should not error
+
+ expect_no_error(
+ delete_upload(file_url, verbose = FALSE)
+ )
+
+ unlink(temp_file)
+})
diff --git a/tests/testthat/test-users-extended.R b/tests/testthat/test-users-extended.R
new file mode 100644
index 0000000..25036a5
--- /dev/null
+++ b/tests/testthat/test-users-extended.R
@@ -0,0 +1,44 @@
+# Tests for extended user functions
+
+library(rtodoist)
+
+# Load fixtures
+user_json <- jsonlite::read_json(
+ test_path("fixtures", "user_sync_response.json")
+)
+
+test_that("user info has correct structure", {
+ user <- user_json$user
+
+ expect_true(!is.null(user$id))
+ expect_true(!is.null(user$email))
+ expect_true(!is.null(user$full_name))
+ expect_true(!is.null(user$karma))
+ expect_true(!is.null(user$karma_trend))
+})
+
+test_that("productivity stats can be extracted from user data", {
+ user <- user_json$user
+
+ stats <- list(
+ karma = user$karma,
+ karma_trend = user$karma_trend,
+ completed_today = user$completed_today,
+ days_items = user$days_items,
+ week_items = user$week_items
+ )
+
+ expect_true(!is.null(stats$karma))
+ expect_equal(stats$karma, 1500)
+ expect_equal(stats$karma_trend, "up")
+ expect_equal(stats$completed_today, 5)
+})
+
+test_that("null coalescing operator works correctly", {
+ # Test the %||% operator
+ `%||%` <- rtodoist:::`%||%`
+
+ expect_equal(NULL %||% "default", "default")
+ expect_equal("value" %||% "default", "value")
+ expect_equal(NA %||% "default", NA)
+})
diff --git a/tests/testthat/test-workspaces.R b/tests/testthat/test-workspaces.R
new file mode 100644
index 0000000..4dba6e4
--- /dev/null
+++ b/tests/testthat/test-workspaces.R
@@ -0,0 +1,94 @@
+# Tests for workspaces functions
+
+library(rtodoist)
+
+# Load fixtures
+workspaces_json <- jsonlite::read_json(
+ test_path("fixtures", "workspaces_response.json")
+)
+
+workspace_users_json <- jsonlite::read_json(
+ test_path("fixtures", "workspace_users_response.json")
+)
+
+test_that("workspaces dataframe has correct structure from fixture", {
+ workspaces <- workspaces_json$workspaces
+
+ workspaces_df <- purrr::map_dfr(workspaces, function(x) {
+ data.frame(
+ id = x$id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ is_default = x$is_default %||% FALSE,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("id" %in% names(workspaces_df))
+ expect_true("name" %in% names(workspaces_df))
+ expect_true("is_default" %in% names(workspaces_df))
+ expect_equal(nrow(workspaces_df), 2)
+ expect_equal(workspaces_df$id[1], "ws_123456")
+ expect_equal(workspaces_df$name[1], "My Workspace")
+ expect_true(workspaces_df$is_default[1])
+})
+
+test_that("workspace users dataframe has correct structure from fixture", {
+ users <- workspace_users_json$workspace_users
+
+ users_df <- purrr::map_dfr(users, function(x) {
+ data.frame(
+ user_id = x$user_id %||% NA_character_,
+ workspace_id = x$workspace_id %||% NA_character_,
+ name = x$name %||% NA_character_,
+ email = x$email %||% NA_character_,
+ role = x$role %||% NA_character_,
+ stringsAsFactors = FALSE
+ )
+ })
+
+ expect_true("user_id" %in% names(users_df))
+ expect_true("workspace_id" %in% names(users_df))
+ expect_true("name" %in% names(users_df))
+ expect_true("email" %in% names(users_df))
+ expect_true("role" %in% names(users_df))
+ expect_equal(nrow(users_df), 2)
+ expect_equal(users_df$role[1], "admin")
+ expect_equal(users_df$role[2], "member")
+})
+
+test_that("empty workspaces returns empty dataframe", {
+ empty_response <- list(workspaces = list())
+
+ result <- data.frame(
+ id = character(),
+ name = character(),
+ is_default = logical(),
+ stringsAsFactors = FALSE
+ )
+
+ expect_equal(nrow(result), 0)
+ expect_true("id" %in% names(result))
+ expect_true("name" %in% names(result))
+ expect_true("is_default" %in% names(result))
+})
+
+# Integration tests (require API token)
+test_that("get_all_workspaces returns dataframe", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+
+ workspaces <- get_all_workspaces()
+
+ expect_s3_class(workspaces, "data.frame")
+ expect_true("id" %in% names(workspaces))
+ expect_true("name" %in% names(workspaces))
+})
+
+test_that("get_workspace_users returns dataframe", {
+ skip_if_no_token()
+ skip_on_ci_or_cran()
+
+ users <- get_workspace_users()
+
+ expect_s3_class(users, "data.frame")
+})
diff --git a/vignettes/complete_guide.Rmd b/vignettes/complete_guide.Rmd
new file mode 100644
index 0000000..b6699c1
--- /dev/null
+++ b/vignettes/complete_guide.Rmd
@@ -0,0 +1,622 @@
+---
+title: "Complete Guide to rtodoist"
+author: "ThinkR"
+date: "`r Sys.Date()`"
+output: rmarkdown::html_vignette
+vignette: >
+ %\VignetteIndexEntry{Complete Guide to rtodoist}
+ %\VignetteEngine{knitr::rmarkdown}
+ %\VignetteEncoding{UTF-8}
+---
+
+```{r setup, include = FALSE}
+knitr::opts_chunk$set(
+ eval = FALSE,
+ collapse = TRUE,
+ comment = "#>"
+)
+```
+
+# Introduction
+
+The `rtodoist` package provides a comprehensive R interface to the Todoist API v1. This vignette covers all available features including projects, tasks, sections, labels, comments, filters, reminders, and more.
+
+# Getting Started
+
+## Setting Up Your API Token
+
+First, you need to obtain your Todoist API token:
+
+1. Go to Todoist Settings > Integrations > Developer
+2. Copy your API token
+3. Store it securely in R using keyring
+
+```{r}
+library(rtodoist)
+
+# Open the website to find your token
+open_todoist_website_profile()
+
+# Set your token (only needs to be done once per computer)
+set_todoist_api_token("YOUR_API_TOKEN")
+
+# Retrieve your stored token
+token <- get_todoist_api_token()
+
+# Update your token if needed
+update_todoist_api_token()
+
+# Delete your stored token
+delete_todoist_api_token()
+```
+
+# Projects
+
+## Creating and Managing Projects
+
+```{r}
+# Create a new project
+project_id <- add_project("My New Project")
+
+# Get all projects
+projects <- get_all_projects()
+
+# Get project ID by name
+project_id <- get_project_id("My New Project")
+
+# Get a single project by ID
+project <- get_project(project_id)
+
+# Update a project
+update_project(
+ project_id = project_id,
+ new_name = "Renamed Project",
+ color = "blue",
+ is_favorite = TRUE,
+ view_style = "board"
+)
+
+# Archive a project
+archive_project(project_id = project_id)
+
+# Unarchive a project
+unarchive_project(project_id = project_id)
+
+# Get archived projects
+archived <- get_archived_projects()
+
+# Delete a project (use with caution!)
+delete_project(project_id = project_id)
+```
+
+# Tasks
+
+## Creating Tasks
+
+```{r}
+# Add a single task to a project
+project_id %>%
+ add_tasks_in_project("My first task")
+
+# Add multiple tasks
+project_id %>%
+ add_tasks_in_project(
+ tasks = c("Task 1", "Task 2", "Task 3")
+ )
+
+# Add tasks with due dates and assignees
+project_id %>%
+ add_tasks_in_project(
+ tasks = c("Task 1", "Task 2"),
+ responsible = c("user1@email.com", "user2@email.com"),
+ due = c("2024-12-25", "2024-12-31"),
+ section_name = c("Section A", "Section B")
+ )
+
+# Add tasks from a data frame
+tasks_df <- data.frame(
+ tasks = c("Task A", "Task B", "Task C"),
+ responsible = c("user@email.com", "user@email.com", "user@email.com"),
+ due = c("2024-12-01", "2024-12-02", "2024-12-03"),
+ section_name = c("Section 1", "Section 1", "Section 2")
+)
+
+add_tasks_in_project_from_df(
+ project_id = project_id,
+ tasks_as_df = tasks_df
+)
+
+# Quick add a task using natural language
+quick_add_task("Buy groceries tomorrow at 5pm #Shopping")
+```
+
+## Managing Tasks
+
+```{r}
+# Get all tasks
+tasks <- get_tasks()
+
+# Get tasks of a specific project
+project_tasks <- get_tasks_of_project(project_id = project_id)
+
+# Get a single task by ID
+task <- get_task("task_id")
+
+# Update a task
+update_task(
+ task_id = "task_id",
+ content = "Updated task content",
+ description = "Task description",
+ priority = 4, # 4 = highest priority
+ due_string = "tomorrow",
+ labels = c("urgent", "work")
+)
+
+# Add a responsible person to a task
+add_responsible_to_task(
+ project_id = project_id,
+ task = "Task 1",
+ responsible = "user@email.com"
+)
+
+# Close (complete) a task
+close_task("task_id")
+
+# Reopen a task
+reopen_task("task_id")
+
+# Move a task to another project or section
+move_task(
+ task_id = "task_id",
+ project_id = "new_project_id",
+ section_id = "new_section_id"
+)
+
+# Delete a task
+delete_task("task_id")
+
+# Get completed tasks
+completed <- get_completed_tasks(
+ project_id = project_id,
+ since = "2024-01-01T00:00:00",
+ limit = 100
+)
+```
+
+## Filtering Tasks
+
+```{r}
+# Get tasks by filter query
+today_tasks <- get_tasks_by_filter("today")
+urgent_tasks <- get_tasks_by_filter("p1")
+work_tasks <- get_tasks_by_filter("#Work")
+overdue_tasks <- get_tasks_by_filter("overdue")
+combined_filter <- get_tasks_by_filter("today & p1 & #Work")
+```
+
+# Sections
+
+## Managing Sections
+
+```{r}
+# Create a section
+section_id <- add_section(
+ section_name = "New Section",
+ project_id = project_id
+)
+
+# Get all sections
+all_sections <- get_all_sections()
+
+# Get sections from a specific project
+project_sections <- get_section_from_project(project_id = project_id)
+
+# Get section ID by name
+section_id <- get_section_id(
+ project_id = project_id,
+ section_name = "New Section"
+)
+
+# Get a single section by ID
+section <- get_section("section_id")
+
+# Update a section
+update_section(
+ section_id = section_id,
+ new_name = "Renamed Section"
+)
+
+# Move a section to another project
+move_section(
+ section_id = section_id,
+ project_id = "target_project_id"
+)
+
+# Archive a section
+archive_section(section_id = section_id)
+
+# Unarchive a section
+unarchive_section(section_id = section_id)
+
+# Delete a section
+delete_section(section_id = section_id)
+```
+
+# Labels
+
+## Managing Labels
+
+```{r}
+# Create a label
+label_id <- add_label(
+ name = "urgent",
+ color = "red",
+ is_favorite = TRUE
+)
+
+# Get all labels
+labels <- get_all_labels()
+
+# Get a single label by ID
+label <- get_label("label_id")
+
+# Get label ID by name
+label_id <- get_label_id("urgent")
+
+# Update a label
+update_label(
+ label_id = label_id,
+ new_name = "very_urgent",
+ color = "orange"
+)
+
+# Delete a label
+delete_label(label_id = label_id)
+
+# Get shared labels (in workspaces)
+shared_labels <- get_shared_labels()
+
+# Rename a shared label
+rename_shared_label("old_name", "new_name")
+
+# Remove a shared label
+remove_shared_label("label_name")
+```
+
+# Comments
+
+## Managing Comments
+
+```{r}
+# Add a comment to a task
+comment_id <- add_comment(
+ content = "This is a comment",
+ task_id = "task_id"
+)
+
+# Add a comment to a project
+comment_id <- add_comment(
+ content = "Project-level comment",
+ project_id = project_id
+)
+
+# Get comments on a task
+task_comments <- get_comments(task_id = "task_id")
+
+# Get comments on a project
+project_comments <- get_comments(project_id = project_id)
+
+# Get a single comment by ID
+comment <- get_comment("comment_id")
+
+# Update a comment
+update_comment(
+ comment_id = "comment_id",
+ content = "Updated comment content"
+)
+
+# Delete a comment
+delete_comment("comment_id")
+```
+
+# Filters
+
+## Managing Custom Filters
+
+```{r}
+# Create a filter
+filter_id <- add_filter(
+ name = "Urgent Today",
+ query = "today & p1",
+ color = "red",
+ is_favorite = TRUE
+)
+
+# Get all filters
+filters <- get_all_filters()
+
+# Get a single filter by ID
+filter <- get_filter("filter_id")
+
+# Get filter ID by name
+filter_id <- get_filter_id("Urgent Today")
+
+# Update a filter
+update_filter(
+ filter_id = filter_id,
+ query = "today & (p1 | p2)",
+ new_name = "Urgent & Important Today"
+)
+
+# Delete a filter
+delete_filter(filter_id = filter_id)
+```
+
+# Reminders
+
+## Managing Reminders
+
+```{r}
+# Add an absolute reminder
+reminder_id <- add_reminder(
+ task_id = "task_id",
+ due_datetime = "2024-12-25T09:00:00",
+ type = "absolute"
+)
+
+# Add a relative reminder (30 minutes before)
+reminder_id <- add_reminder(
+ task_id = "task_id",
+ minute_offset = 30,
+ type = "relative"
+)
+
+# Get all reminders
+reminders <- get_all_reminders()
+
+# Update a reminder
+update_reminder(
+ reminder_id = reminder_id,
+ due_datetime = "2024-12-25T10:00:00"
+)
+
+# Delete a reminder
+delete_reminder(reminder_id)
+```
+
+# Collaboration
+
+## Managing Users and Collaborators
+
+```{r}
+# Get all collaborators
+users <- get_all_users()
+
+# Get user IDs by email
+user_ids <- get_users_id(mails = c("user1@email.com", "user2@email.com"))
+
+# Add a single user to a project
+add_user_in_project(
+ project_id = project_id,
+ mail = "user@email.com"
+)
+
+# Add multiple users to a project
+add_users_in_project(
+ project_id = project_id,
+ users_email = c("user1@email.com", "user2@email.com")
+)
+
+# Get users in a project
+project_users <- get_users_in_project(project_id = project_id)
+
+# Remove a collaborator from a project
+delete_collaborator(
+ project_id = project_id,
+ email = "user@email.com"
+)
+```
+
+## User Information
+
+```{r}
+# Get current user info
+user_info <- get_user_info()
+
+# Get productivity stats
+stats <- get_productivity_stats()
+```
+
+## Invitations
+
+```{r}
+# Accept an invitation
+accept_invitation(
+ invitation_id = "invitation_id",
+ invitation_secret = "secret"
+)
+
+# Reject an invitation
+reject_invitation(
+ invitation_id = "invitation_id",
+ invitation_secret = "secret"
+)
+
+# Delete an invitation
+delete_invitation("invitation_id")
+```
+
+# Workspaces
+
+## Managing Workspaces
+
+```{r}
+# Get all workspaces
+workspaces <- get_all_workspaces()
+
+# Get workspace users
+workspace_users <- get_workspace_users()
+
+# Invite user to workspace
+invite_to_workspace(
+ workspace_id = "workspace_id",
+ email = "user@email.com",
+ role = "member"
+)
+
+# Update workspace
+update_workspace(
+ workspace_id = "workspace_id",
+ name = "New Workspace Name"
+)
+
+# Leave a workspace
+leave_workspace("workspace_id")
+```
+
+# Activity Logs
+
+```{r}
+# Get activity logs
+activity <- get_activity_logs(
+ object_type = "item",
+ event_type = "completed",
+ limit = 50
+)
+```
+
+# Templates
+
+## Import and Export Templates
+
+```{r}
+# Export a project as a template
+export_template(
+ project_id = project_id,
+ output_file = "my_template.csv"
+)
+
+# Import a template into a project
+import_template(
+ project_id = project_id,
+ file_path = "my_template.csv"
+)
+```
+
+# Backups
+
+## Managing Backups
+
+```{r}
+# Get list of available backups
+backups <- get_backups()
+
+# Download a backup
+download_backup(
+ version = backups$version[1],
+ output_file = "todoist_backup.zip"
+)
+```
+
+# File Uploads
+
+## Managing File Attachments
+
+```{r}
+# Upload a file
+upload_result <- upload_file("document.pdf")
+
+# The upload result contains a file_url that can be used in comments
+add_comment(
+ content = paste("Attached file:", upload_result$file_url),
+ task_id = "task_id"
+)
+
+# Delete an uploaded file
+delete_upload(file_url = upload_result$file_url)
+```
+
+# Complete Workflow Example
+
+Here's a complete example showing a typical workflow:
+
+```{r}
+library(rtodoist)
+
+# Create a new project
+project_id <- add_project("Q1 2024 Goals")
+
+# Add sections
+add_section("Personal", project_id = project_id)
+add_section("Work", project_id = project_id)
+add_section("Health", project_id = project_id)
+
+# Create labels
+add_label("high-priority", color = "red")
+add_label("quick-win", color = "green")
+
+# Add tasks with sections and due dates
+project_id %>%
+ add_tasks_in_project(
+ tasks = c(
+ "Complete project proposal",
+ "Review team performance",
+ "Plan Q2 roadmap"
+ ),
+ due = c(
+ as.character(Sys.Date() + 7),
+ as.character(Sys.Date() + 14),
+ as.character(Sys.Date() + 30)
+ ),
+ section_name = c("Work", "Work", "Work")
+ )
+
+project_id %>%
+ add_tasks_in_project(
+ tasks = c(
+ "Run 5K",
+ "Meal prep Sunday",
+ "Schedule annual checkup"
+ ),
+ section_name = c("Health", "Health", "Health")
+ )
+
+# Create a filter for important work tasks
+add_filter(
+ name = "Important Work",
+ query = "#Work & (p1 | p2)",
+ is_favorite = TRUE
+)
+
+# Add a reminder for the first task
+tasks <- get_tasks_of_project(project_id = project_id)
+first_task_id <- tasks[[1]]$id
+
+add_reminder(
+ task_id = first_task_id,
+ due_datetime = paste0(Sys.Date() + 6, "T09:00:00"),
+ type = "absolute"
+)
+
+# Export as template for future use
+export_template(
+ project_id = project_id,
+ output_file = "quarterly_goals_template.csv"
+)
+
+# View today's tasks
+today_tasks <- get_tasks_by_filter("today")
+print(today_tasks)
+```
+
+# API Reference
+
+For more details on the Todoist API, see:
+
+- [Todoist API Documentation](https://developer.todoist.com/api/v1/)
+
+# Session Info
+
+```{r, eval=TRUE}
+sessionInfo()
+```
diff --git a/vignettes/how_it_works.Rmd b/vignettes/how_it_works.Rmd
index 0ef7ca1..6923c62 100644
--- a/vignettes/how_it_works.Rmd
+++ b/vignettes/how_it_works.Rmd
@@ -27,7 +27,6 @@ Open todoist website to find it :
library(rtodoist)
rtodoist::open_todoist_website_profile()
token <- "YOURTOKEN" # copied and pasted from website
-library(lubridate)
```
@@ -96,23 +95,22 @@ if you use vectors, the order is important, and the matching will be done term b
**multiple** tasks for **one** responsible with **one** due dates
```{r}
-id_proj %>%
- add_tasks_in_project(tasks = c("t1","t2"),responsible = c("user1@mail.com"),due = today())
+id_proj %>%
+ add_tasks_in_project(tasks = c("t1","t2"),responsible = c("user1@mail.com"),due = Sys.Date())
```
**multiple** tasks for **one** responsible with **multiple** due dates
```{r}
-
-id_proj %>%
- add_tasks_in_project(tasks = c("t1","t2"),responsible = c("user1@mail.com"),due = today() + days(1:2))
+id_proj %>%
+ add_tasks_in_project(tasks = c("t1","t2"),responsible = c("user1@mail.com"),due = Sys.Date() + 1:2)
```
**multiple** tasks for **multiple** responsible with **on** due dates on many section
```{r}
-id_proj %>%
- add_tasks_in_project(tasks = c("t1","t2"),responsible = c("user1@mail.com","user2@mail.com"),due = lubridate::today(),section_name = c("S1","S2"))
+id_proj %>%
+ add_tasks_in_project(tasks = c("t1","t2"),responsible = c("user1@mail.com","user2@mail.com"),due = Sys.Date(),section_name = c("S1","S2"))
```
@@ -123,9 +121,8 @@ you can pass a data.frame using `add_tasks_in_project_from_df`
tasks_df <- data.frame(
"tasks" = LETTERS[1:5],
"responsible" = glue::glue("user{1:5}@mail.com"),
- "due" = today()+ days(1:5),
+ "due" = Sys.Date() + 1:5,
"section_name" = c("S1","S1","S3","S3","S3")
-
)
add_tasks_in_project_from_df(project_id = id_proj,tasks_as_df = tasks_df)