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)