Skip to content

Commit 6e80ca7

Browse files
authored
Move and copy what the web can, across projects (#973)
Lands the CLI moves stack as one change: - #973 cards move --to-project/--to-card-table, cards copy, wormhole moves answer the card's new id - #975 one shared move and copy flow for every recording type - #976 todos move --to-project/--to-list (bulk), todos copy, todolists and todolistgroups move/copy - #977 messages, Docs & Files (bulk, --to-folder), card table columns, schedule entries, check-in questions, email forwards, whole tools; basecamp filings show; a guard refusing container flags a move or copy would ignore - #974 projects logo set/remove Pins basecamp-sdk v0.31.0.
1 parent d12e9f3 commit 6e80ca7

89 files changed

Lines changed: 10439 additions & 570 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.surface‎

Lines changed: 1114 additions & 11 deletions
Large diffs are not rendered by default.

‎.surface-breaking‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ ARG basecamp message 01 [body]
1919
ARG basecamp react 00 <content>
2020
ARG basecamp reopen 00 <id|url>...
2121
ARG basecamp todo 00 <content>
22+
ARG basecamp todos move 00 <id|url>
23+
ARG basecamp todos position 00 <id|url>
24+
ARG basecamp todos reorder 00 <id|url>
2225
ARG basecamp unassign 00 <id>
2326
ARG basecamp unassign 00 <todo_id>
2427
ARG basecamp upload archive 00 <id|url>
@@ -1502,3 +1505,11 @@ FLAG basecamp checkin question notify --digest-include-unanswered type=bool
15021505
FLAG basecamp checkin question notify --no-digest-include-unanswered type=bool
15031506
FLAG basecamp checkins question notify --digest-include-unanswered type=bool
15041507
FLAG basecamp checkins question notify --no-digest-include-unanswered type=bool
1508+
ARG basecamp docs move 00 <id|url>
1509+
ARG basecamp documents move 00 <id|url>
1510+
ARG basecamp file move 00 <id|url>
1511+
ARG basecamp files move 00 <id|url>
1512+
ARG basecamp folders move 00 <id|url>
1513+
ARG basecamp vault move 00 <id|url>
1514+
ARG basecamp vaults move 00 <id|url>
1515+
ARG basecamp tools move 00 <id>

‎API-COVERAGE.md‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github.
66

77
| Status | Sections | Endpoints |
88
|--------|----------|-----------|
9-
| ✅ Implemented | 55 | 213 |
9+
| ✅ Implemented | 57 | 218 |
1010
| ⚠️ Blocked | 0 | 0 |
1111
| ⏭️ Out of scope | 4 | 12 |
12-
| **Total tracked** | **59** | **225** |
13-
14-
**213 of 213 tracked in-scope endpoints.** Uploading and removing a project's
12+
| **Total tracked** | **61** | **230** |
13+
14+
**218 of 218 tracked in-scope endpoints.** Uploading and removing a project's
15+
logo (`PUT`/`DELETE /projects/:id/logo.json`, added by
16+
[bc3#14174](https://github.com/basecamp/bc3/pull/14174) and shipped in
17+
[SDK v0.30.0](https://github.com/basecamp/basecamp-sdk/releases/tag/v0.30.0))
18+
land as `projects logo`. Moving and copying a recording
19+
into another project (`POST /recordings/:id/moves.json`,
20+
`POST /recordings/:id/copies.json`) and reading the background filing they
21+
start (`GET /filings/:id.json`, also read on its own by `filings show`), modelled by
22+
[basecamp/basecamp-sdk#1053](https://github.com/basecamp/basecamp-sdk/pull/1053),
23+
land for cards as `cards move --to-project`/`--to-card-table` and `cards copy`, and for
24+
to-dos, to-do lists and groups as `todos move|copy`, `todolists move|copy` and
25+
`todolistgroups move|copy` with `--to-project`/`--to-list`/`--to-todoset`. Uploading and removing a project's
1526
wallpaper (`PUT`/`DELETE /projects/:id/wallpaper.json`, added by
1627
[bc3#14062](https://github.com/basecamp/bc3/pull/14062) and shipped in
1728
[SDK v0.28.0](https://github.com/basecamp/basecamp-sdk/releases/tag/v0.28.0))
@@ -205,18 +216,19 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
205216
|---------|-----------|-------------|--------|-------|----------|-------|
206217
| **Core** |
207218
| projects | 9 | `projects` | ✅ | BC4 | - | list, show, create, update, delete |
219+
| projects (logo) | 2 | `projects logo` | ✅ | BC5 | - | set (`PUT /projects/:id/logo.json`, multipart `logo`), remove (`DELETE` on the same route; 204 with or without a logo). PNG, JPEG, GIF, WebP, AVIF, HEIF or HEIC up to 5 MB, checked before the upload using a local MIME hint; the server applies its shared image validations. People and agents who can edit the project may change it; others get 403. Project JSON carries `logo {url}` when one is attached, so `projects show` shows it. `GET /projects/:id/logo.json` only redirects to the image `logo.url` already names, so the CLI does not wrap it. Project-scoped via `--in`/`--project` |
208220
| projects (wallpaper) | 2 | `projects wallpaper` | ✅ | BC5 | - | set (`PUT /projects/:id/wallpaper.json`, multipart `wallpaper` with an optional `mode`), remove (`DELETE` on the same route; 204 with or without a wallpaper). `mode` sends `wallpaper_mode` through the existing `PUT /projects/:id.json`, with the current name the SDK requires. PNG, JPEG, GIF, WebP, AVIF, HEIF or HEIC up to 10 MB, checked before the upload using a local MIME hint; the server applies its shared image validations. People and personal assistants follow project-edit permissions; shared agents are refused with 403. Project JSON carries `wallpaper {url, mode}` when one is attached, so `projects show` shows it. Project-scoped via `--in`/`--project` |
209-
| todos | 12 | `todos`, `todo`, `done`, `reopen` | ✅ | BC4 | - | list, show, create, update, complete, uncomplete, position (BC5: `steps` shown on `todos show`, plus `subtasks_count`/`subtasks_completed_count`/`subtasks_url`; edit via `subtasks`). `todos create --loose` creates on the to-do set, outside any list |
221+
| todos | 12 | `todos`, `todo`, `done`, `reopen` | ✅ | BC4 | - | list, show, create, update, complete, uncomplete, position, copy (BC5: `steps` shown on `todos show`, plus `subtasks_count`/`subtasks_completed_count`/`subtasks_url`; edit via `subtasks`). `todos create --loose` creates on the to-do set, outside any list |
210222
| subtasks | 8 | `subtasks` | ✅ | BC5 | - | list (`GET /recordings/:id/subtasks.json`, paginated), show, create (`POST` on the same list route), update (partial; `--no-due`/`--no-assignees` clear), complete/uncomplete (`POST`/`DELETE /subtasks/:id/completion.json`), move (`PUT /subtasks/:id/position.json`, 1-based), delete. Account-scoped flat routes (bc3#12659), so no `--in`. Parents are to-dos and cards only. Same `Kanban::Step` records as `card_table_steps` |
211-
| todolists | 9 | `todolists` | ✅ | BC4 | - | list, show, create, update, position |
223+
| todolists | 9 | `todolists` | ✅ | BC4 | - | list, show, create, update, position, copy |
212224
| todosets | 3 | `todosets` | ✅ | BC4 | - | Container for todolists, accessed via project dock (BC5: `todos_count`, `completed_loose_todos_count`, `todos_url`, `app_todos_url`) |
213-
| todolist_groups | 8 | `todolistgroups` | ✅ | BC4 | - | list, show, create, update, position |
214-
| dock_tools | 7 | `tools` | ✅ | BC4 | - | Dock tool management: show, update, trash, enable, disable, reposition. `create` is BC5-only (create-by-type: `POST /buckets/{id}/dock/tools.json`), replacing the removed clone call; create-time `visible_to_clients` behind `tools create --visible-to-clients` (chat/kanban only) |
225+
| todolist_groups | 8 | `todolistgroups` | ✅ | BC4 | - | list, show, create, update, position, copy |
226+
| dock_tools | 7 | `tools` | ✅ | BC4 | - | Dock tool management: show, update, trash, enable, disable, reposition (also `tools move --position`; `tools move --to-project` moves a tool to another project through recordings (moves, copies)). `create` is BC5-only (create-by-type: `POST /buckets/{id}/dock/tools.json`), replacing the removed clone call; create-time `visible_to_clients` behind `tools create --visible-to-clients` (chat/kanban only) |
215227
| **Hill Charts** |
216228
| hill_charts | 2 | `hillcharts` | ✅ | BC4 | - | show, track/untrack todolists |
217229
| gauges | 7 | `gauges` | ✅ | BC4 | - | list, needles, needle, create, update, delete, enable/disable |
218230
| **Communication** |
219-
| messages | 10 | `messages`, `message` | ✅ | BC4 | - | list, show, create, update, publish, pin, unpin. Create supports `--subscribe`/`--no-subscribe` and `--draft`. Publish promotes drafts to active |
231+
| messages | 10 | `messages`, `message` | ✅ | BC4 | - | list, show, create, update, publish, pin, unpin. Create supports `--subscribe`/`--no-subscribe` and `--draft`. Publish promotes drafts to active. `move`/`copy` file a message onto a message board in another project (see recordings (moves, copies)) |
220232
| message_boards | 3 | `messageboards` | ✅ | BC4 | - | Container, accessed via project dock |
221233
| message_types | 5 | `messagetypes` | ✅ | BC4 | - | list, show, create, update, delete. Bucket-scoped (`/buckets/{id}/categories…`); commands are project-scoped via `--in`/`--project` |
222234
| campfires | 14 | `chat` | ✅ | BC4 | - | list, messages, post, line show/update/delete. @mentions in content |
@@ -231,7 +243,8 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
231243
| card_table_cards | 9 | `cards` | ✅ | BC4 | - | list, show, create, update, move |
232244
| card_table_columns | 11 | `cards columns` | ✅ | BC4 | - | list columns. SDK v0.12.0 added `Subscribe`/`Unsubscribe`; `cards column watch\|unwatch` already performs the same action through the generic recording-subscription endpoint and returns the resulting subscription details the specific endpoint does not, so the CLI keeps one spelling |
233245
| card_table_steps | 4 | `cards steps` | ✅ | BC4 | - | Workflow steps on cards. The card-scoped aliases of `subtasks`, which bc3 keeps serving; `cards step move --position` is 1-based (the SDK refuses 0 since basecamp/basecamp-sdk#883) |
234-
| card_table_wormholes | 3 | `cards wormholes` | ✅ | BC5 | - | list (via `wormholes[]` on card table), create, update, delete; `cards move --to-wormhole` teleports a card across projects (async, new id) |
246+
| card_table_wormholes | 3 | `cards wormholes` | ✅ | BC5 | - | list (via `wormholes[]` on card table), create, update, delete; `cards move --to-wormhole` teleports a card across projects and reports the new card once it lands |
247+
| recordings (moves, copies) | 3 | `cards move --to-project`, `cards copy`, `todos move --to-project`/`--to-list`, `todos copy`, `todolists move --to-project`, `todolists copy`, `todolistgroups move --to-list`, `todolistgroups copy`, `messages move`, `messages copy`, `files move --to-project`, `files copy`, `cards column move --to-project`, `cards column copy`, `schedule move`, `schedule copy`, `checkins question move`, `checkins question copy`, `forwards move`, `forwards copy`, `tools move --to-project`, `filings show` | ✅ | BC5 | - | `POST /recordings/:id/moves.json` and `POST /recordings/:id/copies.json` file a card into a card table in this or another project, landing in Triage (Done if completed); `GET /filings/:id.json` reports the background filing, which the commands wait on before showing the new card, and which `filings show <id>` reads when a command's wait ends first (status, type, source, and once completed the recording it made). `--to-project` picks the project's only card table on its dock, `--to-card-table` one by ID, URL or name. `cards copy --include-comments` sends `include_comments`; a copy is never retried. To-dos file into a to-do list in any project (several IDs file one by one, and any failure fails the command naming what moved); a to-do list files into a project's To-dos (`--to-todoset` when it has several); a group files into a to-do list and stays a group. `--to-list`/`--to-todoset` take an ID, URL or a name looked up in `--to-project`; Basecamp refuses a group or an archived list as the destination. `messages move`/`copy` file a message onto a message board (`--to-project`, `--to-board`); a copy with no destination goes onto the message's own board. `files move --to-project`/`--to-folder` and `files copy` (also on `uploads`, `docs`, `vaults` and their aliases) file documents, uploads, cloud files, Google documents and folders into a project's Docs & Files, a folder inside it as `parent_id`; several IDs are filed one by one, as the web's bulk Move/Copy does, and `files show` reads the cloud files and Google documents they make. `cards column move --to-project`/`--to-card-table` and `cards column copy` file a column with its cards onto a card table; Triage, Not now, Done and On hold are refused before the request. `schedule move`/`copy` file an event onto a schedule (`--to-project`, `--to-schedule`); a recurring event, whose read answers with an occurrence, files as its whole series. `checkins question move`/`copy` file a question, with its answers on a move, into a project's Automatic Check-ins (`--to-project`, `--to-questionnaire`). `forwards move`/`copy` file an email forward into a project's Email Forwards (`--to-project`, `--to-inbox`), which must be turned on there; a turned-off one is named with the `tools enable` command. `tools move --to-project` moves a whole dock tool to another project with `destination_project_id`; tools are not copied, as on the web, and doors are refused |
235248
| **Personal (My)** |
236249
| my_bookmarks | 4 | `bookmarks` | ✅ | BC5 | - | list, check, add, remove. Private to the authenticated user; `add`/`remove` are idempotent, and `check` returns a bool reported in the payload rather than through the exit code. Bounded like the account-wide listings |
237250
| drafts | 1 | `drafts` | ✅ | BC5 | - | list unpublished drafts across projects (server caps at 250). Bounded like the account-wide listings; publishing happens through the command for the draft's type |
@@ -245,7 +258,7 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
245258
| **Files & Documents** |
246259
| uploads | 8 | `files`, `uploads` | ✅ | BC4 | - | list, show, create, update, download, versions (`files versions <id>`), replace (`files replace <id> <file>`); trash/archive/restore go through `recordings`. Create supports `--visible-to-clients` (root vault only) |
247260
| vaults | 8 | `files`, `vaults` | ✅ | BC4 | - | list, show, create |
248-
| vaults (move) | 1 | `files move` | ✅ | BC5 | - | `POST /recordings/:id/filing.json`: moves a document, upload or folder into another folder in the same project, in place (same id and comments). `--to` takes a folder ID or URL; `--position` is 1-indexed. Flat route, so no `--in` |
261+
| vaults (move) | 1 | `files move` | ✅ | BC5 | - | `POST /recordings/:id/filing.json`: moves a document, upload or folder into another folder in the same project, in place (same id and comments). `--to` takes a folder ID or URL; `--position` is 1-indexed. Flat route, so no `--in`. Moves and copies into another project go through recordings (moves, copies) with `--to-project`/`--to-folder` |
249262
| documents | 8 | `files`, `docs` | ✅ | BC4 | - | list, show, create, update. Create supports `--subscribe`/`--no-subscribe`, `--visible-to-clients` (root vault only) |
250263
| attachments | 1 | `uploads`, `attachments` | ✅ | BC4 | - | Upload via `attach`; list embedded attachments via `attachments list` (parses `<bc-attachment>` from content) |
251264
| embeds | 2 | `embeds` | ✅ | BC5 | - | create, show. Rich-text commands also embed a URL that stands alone on its line in Markdown content, through the same create |

‎e2e/cards_columns_steps.bats‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,6 @@ load test_helper
6767
assert_output_contains "ID required"
6868
}
6969

70-
@test "cards column show without project shows error" {
71-
create_credentials
72-
create_global_config '{"account_id": 99999}'
73-
74-
run basecamp cards column show 456
75-
assert_failure
76-
assert_output_contains "project"
77-
}
78-
7970

8071
# Column create errors
8172

‎e2e/errors.bats‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ load test_helper
335335

336336
run basecamp todos position --to 1
337337
assert_failure
338-
assert_json_value '.error' '<id|url> required'
338+
assert_json_value '.error' '<id|url>... required'
339339
assert_json_value '.code' 'usage'
340340
}
341341

‎e2e/projects.bats‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,57 @@ load test_helper
9191
run basecamp projects wallpaper remove skyline.png
9292
assert_failure
9393
}
94+
95+
96+
@test "projects logo without subcommand shows help" {
97+
run basecamp projects logo
98+
assert_success
99+
assert_output_contains "COMMANDS"
100+
assert_output_contains "set"
101+
assert_output_contains "remove"
102+
}
103+
104+
@test "projects logo set help names the formats and the size cap" {
105+
run basecamp projects logo set --help
106+
assert_success
107+
assert_output_contains "PNG, JPEG, GIF, WebP, AVIF, HEIF, or HEIC files up to 5 MB"
108+
}
109+
110+
@test "projects logo set without a file shows error" {
111+
create_credentials
112+
create_global_config '{"account_id": 99999, "project_id": 123}'
113+
114+
run basecamp projects logo set
115+
assert_failure
116+
assert_json_value '.error' '<file> required'
117+
assert_json_value '.code' 'usage'
118+
}
119+
120+
@test "projects logo set refuses an SVG" {
121+
create_credentials
122+
create_global_config '{"account_id": 99999, "project_id": 123}'
123+
printf '<svg></svg>' > brand.svg
124+
125+
run basecamp projects logo set brand.svg
126+
assert_failure
127+
assert_json_value '.error' 'brand.svg: unsupported image type image/svg+xml (use PNG, JPEG, GIF, WebP, AVIF, HEIF, or HEIC)'
128+
assert_json_value '.code' 'usage'
129+
}
130+
131+
@test "projects logo set refuses a file over 5 MB" {
132+
create_credentials
133+
create_global_config '{"account_id": 99999, "project_id": 123}'
134+
dd if=/dev/zero of=big.png bs=1 count=1 seek=5242880 2>/dev/null
135+
136+
run basecamp projects logo set big.png
137+
assert_failure
138+
assert_json_value '.error' 'big.png exceeds maximum logo size of 5 MB'
139+
}
140+
141+
@test "projects logo remove takes no arguments" {
142+
create_credentials
143+
create_global_config '{"account_id": 99999, "project_id": 123}'
144+
145+
run basecamp projects logo remove brand.png
146+
assert_failure
147+
}

‎e2e/smoke/smoke_aliases.bats‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,6 @@ load smoke_helper
265265
mark_out_of_scope "Alias for tools trash — tested via canonical form"
266266
}
267267

268-
@test "tools move is out of scope" {
269-
mark_out_of_scope "Alias for tools reposition — tested via canonical form"
270-
}
271-
272268
@test "tools rename is out of scope" {
273269
mark_out_of_scope "Alias for tools update — tested via canonical form"
274270
}

‎e2e/smoke/smoke_cards_column_write.bats‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ setup_file() {
6969
assert_json_value '.ok' 'true'
7070
}
7171

72+
@test "cards column copy copies a column onto its own card table" {
73+
local id_file="$BATS_FILE_TMPDIR/column_id"
74+
[[ -f "$id_file" ]] || mark_unverifiable "No column created in prior test"
75+
local col_id
76+
col_id=$(<"$id_file")
77+
78+
run_smoke basecamp cards column copy "$col_id" --json
79+
assert_success
80+
assert_json_value '.ok' 'true'
81+
assert_json_value '.data.source_id' "$col_id"
82+
assert_json_not_null '.data.id'
83+
}
84+
85+
@test "cards column move refuses the card table the column is on" {
86+
local id_file="$BATS_FILE_TMPDIR/column_id"
87+
[[ -f "$id_file" ]] || mark_unverifiable "No column created in prior test"
88+
local col_id
89+
col_id=$(<"$id_file")
90+
91+
run_smoke basecamp cards column move "$col_id" --to-card-table "$QA_CARDTABLE" --json
92+
assert_failure
93+
assert_json_value '.ok' 'false'
94+
}
95+
7296
@test "cards column watch watches a column" {
7397
local id_file="$BATS_FILE_TMPDIR/column_id"
7498
[[ -f "$id_file" ]] || mark_unverifiable "No column created in prior test"

‎e2e/smoke/smoke_cards_write.bats‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ setup_file() {
4545
assert_json_not_null '.data.id'
4646
}
4747

48+
@test "cards copy copies a card onto its own card table" {
49+
local id_file="$BATS_FILE_TMPDIR/card_id"
50+
[[ -f "$id_file" ]] || mark_unverifiable "No card created in prior test"
51+
local card_id
52+
card_id=$(<"$id_file")
53+
54+
run_smoke basecamp cards copy "$card_id" --json
55+
assert_success
56+
assert_json_value '.ok' 'true'
57+
assert_json_value '.data.source_id' "$card_id"
58+
assert_json_not_null '.data.id'
59+
}
60+
4861
@test "cards columns lists columns" {
4962
run_smoke basecamp cards columns --card-table "$QA_CARDTABLE" -p "$QA_PROJECT" --json
5063
assert_success

‎e2e/smoke/smoke_checkins_write.bats‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,29 @@ setup_file() {
5757
assert_success
5858
assert_json_value '.ok' 'true'
5959
}
60+
61+
@test "checkins question copy copies a question into its own check-ins" {
62+
local id_file="$BATS_FILE_TMPDIR/question_id"
63+
[[ -f "$id_file" ]] || mark_unverifiable "No question created in prior test"
64+
local qid
65+
qid=$(<"$id_file")
66+
67+
run_smoke basecamp checkins question copy "$qid" --json
68+
assert_success
69+
assert_json_value '.ok' 'true'
70+
assert_json_value '.data.source_id' "$qid"
71+
assert_json_not_null '.data.id'
72+
}
73+
74+
@test "checkins question move refuses the check-ins the question is in" {
75+
local id_file="$BATS_FILE_TMPDIR/question_id"
76+
[[ -f "$id_file" ]] || mark_unverifiable "No question created in prior test"
77+
local qid
78+
qid=$(<"$id_file")
79+
80+
# A cross-project move would take the QA question away; the move itself
81+
# is covered against local bc3.
82+
run_smoke basecamp checkins question move "$qid" --to-project "$QA_PROJECT" --json
83+
assert_failure
84+
assert_json_value '.ok' 'false'
85+
}

0 commit comments

Comments
 (0)