basecamp-sdk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/basecamp/client.rb +5 -0
- data/lib/basecamp/generated/metadata.json +237 -4
- data/lib/basecamp/generated/services/card_steps_service.rb +3 -1
- data/lib/basecamp/generated/services/checkins_service.rb +5 -4
- data/lib/basecamp/generated/services/my_notifications_service.rb +2 -1
- data/lib/basecamp/generated/services/recordings_service.rb +16 -2
- data/lib/basecamp/generated/services/subtasks_service.rb +97 -0
- data/lib/basecamp/generated/services/templates_service.rb +68 -9
- data/lib/basecamp/generated/services/timesheets_service.rb +1 -1
- data/lib/basecamp/generated/types.rb +175 -81
- data/lib/basecamp/version.rb +2 -2
- data/scripts/generate-services.rb +8 -3
- data/scripts/generate-types.rb +73 -9
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92bdbb7967efe2d02dd1d5e007c1d52c22d241be8b067e4ec633bfe3209fb496
|
|
4
|
+
data.tar.gz: b1de8569bed78718e03a54aa34df54143d81ddc6c7e73d77f63e97d64e21adbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9de61285d797588afab159d64e44cbb38f058bddf1a08734d68eeb24c5baa9703e89e88eb12a48a06618b766f8c010ee9206e13ae5ea11941a6087ce368000a6
|
|
7
|
+
data.tar.gz: fccb795b1aa3901a705550a5f4c5054e33b0a2c9e316acd01ade37e95e343cf2a33784f23368c4c41fa88a2618b480f9aa38c23e4adc1e27dc3945d68c43cae4
|
data/README.md
CHANGED
|
@@ -347,6 +347,7 @@ The SDK provides the full account-scoped service set documented in SPEC §5. The
|
|
|
347
347
|
| `card_tables` | Card tables (kanban) |
|
|
348
348
|
| `card_columns` | Card table columns |
|
|
349
349
|
| `card_steps` | Card workflow steps |
|
|
350
|
+
| `subtasks` | Subtasks (checklist items) on to-dos and cards |
|
|
350
351
|
| `wormholes` | Card table wormholes (cross-project moves) |
|
|
351
352
|
| `lineup` | Card lineup view |
|
|
352
353
|
| `tools` | Project dock tools |
|
data/lib/basecamp/client.rb
CHANGED
|
@@ -607,6 +607,11 @@ module Basecamp
|
|
|
607
607
|
service(:boosts) { Services::BoostsService.new(self) }
|
|
608
608
|
end
|
|
609
609
|
|
|
610
|
+
# @return [Services::SubtasksService]
|
|
611
|
+
def subtasks
|
|
612
|
+
service(:subtasks) { Services::SubtasksService.new(self) }
|
|
613
|
+
end
|
|
614
|
+
|
|
610
615
|
# @return [Services::AccountService]
|
|
611
616
|
def account
|
|
612
617
|
service(:account) { Services::AccountService.new(self) }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://basecamp.com/schemas/sdk-metadata.json",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"generated": "2026-
|
|
4
|
+
"generated": "2026-10-03T00:10:53Z",
|
|
5
5
|
"operations": {
|
|
6
6
|
"GetAccount": {
|
|
7
7
|
"retry": {
|
|
@@ -375,6 +375,28 @@
|
|
|
375
375
|
]
|
|
376
376
|
}
|
|
377
377
|
},
|
|
378
|
+
"CreateTemplatification": {
|
|
379
|
+
"retry": {
|
|
380
|
+
"maxAttempts": 2,
|
|
381
|
+
"baseDelayMs": 1000,
|
|
382
|
+
"backoff": "exponential",
|
|
383
|
+
"retryOn": [
|
|
384
|
+
429,
|
|
385
|
+
503
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
"GetTemplatification": {
|
|
390
|
+
"retry": {
|
|
391
|
+
"maxAttempts": 3,
|
|
392
|
+
"baseDelayMs": 1000,
|
|
393
|
+
"backoff": "exponential",
|
|
394
|
+
"retryOn": [
|
|
395
|
+
429,
|
|
396
|
+
503
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
},
|
|
378
400
|
"CreateTodosetTodo": {
|
|
379
401
|
"retry": {
|
|
380
402
|
"maxAttempts": 3,
|
|
@@ -2366,6 +2388,17 @@
|
|
|
2366
2388
|
]
|
|
2367
2389
|
}
|
|
2368
2390
|
},
|
|
2391
|
+
"MoveRecordingToVault": {
|
|
2392
|
+
"retry": {
|
|
2393
|
+
"maxAttempts": 2,
|
|
2394
|
+
"baseDelayMs": 1000,
|
|
2395
|
+
"backoff": "exponential",
|
|
2396
|
+
"retryOn": [
|
|
2397
|
+
429,
|
|
2398
|
+
503
|
|
2399
|
+
]
|
|
2400
|
+
}
|
|
2401
|
+
},
|
|
2369
2402
|
"SpotlightRecording": {
|
|
2370
2403
|
"retry": {
|
|
2371
2404
|
"maxAttempts": 3,
|
|
@@ -2489,6 +2522,33 @@
|
|
|
2489
2522
|
"natural": true
|
|
2490
2523
|
}
|
|
2491
2524
|
},
|
|
2525
|
+
"ListSubtasks": {
|
|
2526
|
+
"retry": {
|
|
2527
|
+
"maxAttempts": 3,
|
|
2528
|
+
"baseDelayMs": 1000,
|
|
2529
|
+
"backoff": "exponential",
|
|
2530
|
+
"retryOn": [
|
|
2531
|
+
429,
|
|
2532
|
+
503
|
|
2533
|
+
]
|
|
2534
|
+
},
|
|
2535
|
+
"pagination": {
|
|
2536
|
+
"style": "link",
|
|
2537
|
+
"totalCountHeader": "X-Total-Count",
|
|
2538
|
+
"maxPageSize": 50
|
|
2539
|
+
}
|
|
2540
|
+
},
|
|
2541
|
+
"CreateSubtask": {
|
|
2542
|
+
"retry": {
|
|
2543
|
+
"maxAttempts": 2,
|
|
2544
|
+
"baseDelayMs": 1000,
|
|
2545
|
+
"backoff": "exponential",
|
|
2546
|
+
"retryOn": [
|
|
2547
|
+
429,
|
|
2548
|
+
503
|
|
2549
|
+
]
|
|
2550
|
+
}
|
|
2551
|
+
},
|
|
2492
2552
|
"GetRecordingTimesheet": {
|
|
2493
2553
|
"retry": {
|
|
2494
2554
|
"maxAttempts": 3,
|
|
@@ -2834,7 +2894,46 @@
|
|
|
2834
2894
|
"natural": true
|
|
2835
2895
|
}
|
|
2836
2896
|
},
|
|
2837
|
-
"
|
|
2897
|
+
"GetSubtask": {
|
|
2898
|
+
"retry": {
|
|
2899
|
+
"maxAttempts": 3,
|
|
2900
|
+
"baseDelayMs": 1000,
|
|
2901
|
+
"backoff": "exponential",
|
|
2902
|
+
"retryOn": [
|
|
2903
|
+
429,
|
|
2904
|
+
503
|
|
2905
|
+
]
|
|
2906
|
+
}
|
|
2907
|
+
},
|
|
2908
|
+
"UpdateSubtask": {
|
|
2909
|
+
"retry": {
|
|
2910
|
+
"maxAttempts": 3,
|
|
2911
|
+
"baseDelayMs": 1000,
|
|
2912
|
+
"backoff": "exponential",
|
|
2913
|
+
"retryOn": [
|
|
2914
|
+
429,
|
|
2915
|
+
503
|
|
2916
|
+
]
|
|
2917
|
+
},
|
|
2918
|
+
"idempotent": {
|
|
2919
|
+
"natural": true
|
|
2920
|
+
}
|
|
2921
|
+
},
|
|
2922
|
+
"DeleteSubtask": {
|
|
2923
|
+
"retry": {
|
|
2924
|
+
"maxAttempts": 3,
|
|
2925
|
+
"baseDelayMs": 1000,
|
|
2926
|
+
"backoff": "exponential",
|
|
2927
|
+
"retryOn": [
|
|
2928
|
+
429,
|
|
2929
|
+
503
|
|
2930
|
+
]
|
|
2931
|
+
},
|
|
2932
|
+
"idempotent": {
|
|
2933
|
+
"natural": true
|
|
2934
|
+
}
|
|
2935
|
+
},
|
|
2936
|
+
"CompleteSubtask": {
|
|
2838
2937
|
"retry": {
|
|
2839
2938
|
"maxAttempts": 3,
|
|
2840
2939
|
"baseDelayMs": 1000,
|
|
@@ -2843,6 +2942,59 @@
|
|
|
2843
2942
|
429,
|
|
2844
2943
|
503
|
|
2845
2944
|
]
|
|
2945
|
+
},
|
|
2946
|
+
"idempotent": {
|
|
2947
|
+
"natural": true
|
|
2948
|
+
}
|
|
2949
|
+
},
|
|
2950
|
+
"UncompleteSubtask": {
|
|
2951
|
+
"retry": {
|
|
2952
|
+
"maxAttempts": 3,
|
|
2953
|
+
"baseDelayMs": 1000,
|
|
2954
|
+
"backoff": "exponential",
|
|
2955
|
+
"retryOn": [
|
|
2956
|
+
429,
|
|
2957
|
+
503
|
|
2958
|
+
]
|
|
2959
|
+
},
|
|
2960
|
+
"idempotent": {
|
|
2961
|
+
"natural": true
|
|
2962
|
+
}
|
|
2963
|
+
},
|
|
2964
|
+
"RepositionSubtask": {
|
|
2965
|
+
"retry": {
|
|
2966
|
+
"maxAttempts": 3,
|
|
2967
|
+
"baseDelayMs": 1000,
|
|
2968
|
+
"backoff": "exponential",
|
|
2969
|
+
"retryOn": [
|
|
2970
|
+
429,
|
|
2971
|
+
503
|
|
2972
|
+
]
|
|
2973
|
+
},
|
|
2974
|
+
"idempotent": {
|
|
2975
|
+
"natural": true
|
|
2976
|
+
}
|
|
2977
|
+
},
|
|
2978
|
+
"GetTemplateLibraryCardTables": {
|
|
2979
|
+
"retry": {
|
|
2980
|
+
"maxAttempts": 3,
|
|
2981
|
+
"baseDelayMs": 1000,
|
|
2982
|
+
"backoff": "exponential",
|
|
2983
|
+
"retryOn": [
|
|
2984
|
+
429,
|
|
2985
|
+
503
|
|
2986
|
+
]
|
|
2987
|
+
}
|
|
2988
|
+
},
|
|
2989
|
+
"CreateTemplateLibraryCardTable": {
|
|
2990
|
+
"retry": {
|
|
2991
|
+
"maxAttempts": 2,
|
|
2992
|
+
"baseDelayMs": 1000,
|
|
2993
|
+
"backoff": "exponential",
|
|
2994
|
+
"retryOn": [
|
|
2995
|
+
429,
|
|
2996
|
+
503
|
|
2997
|
+
]
|
|
2846
2998
|
}
|
|
2847
2999
|
},
|
|
2848
3000
|
"CreateTemplateLibraryCopy": {
|
|
@@ -2867,6 +3019,28 @@
|
|
|
2867
3019
|
]
|
|
2868
3020
|
}
|
|
2869
3021
|
},
|
|
3022
|
+
"GetTemplateLibraryTodolists": {
|
|
3023
|
+
"retry": {
|
|
3024
|
+
"maxAttempts": 3,
|
|
3025
|
+
"baseDelayMs": 1000,
|
|
3026
|
+
"backoff": "exponential",
|
|
3027
|
+
"retryOn": [
|
|
3028
|
+
429,
|
|
3029
|
+
503
|
|
3030
|
+
]
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
"CreateTemplateLibraryTodolist": {
|
|
3034
|
+
"retry": {
|
|
3035
|
+
"maxAttempts": 2,
|
|
3036
|
+
"baseDelayMs": 1000,
|
|
3037
|
+
"backoff": "exponential",
|
|
3038
|
+
"retryOn": [
|
|
3039
|
+
429,
|
|
3040
|
+
503
|
|
3041
|
+
]
|
|
3042
|
+
}
|
|
3043
|
+
},
|
|
2870
3044
|
"ListTemplates": {
|
|
2871
3045
|
"retry": {
|
|
2872
3046
|
"maxAttempts": 3,
|
|
@@ -3566,7 +3740,35 @@
|
|
|
3566
3740
|
"creator",
|
|
3567
3741
|
"participants.[]"
|
|
3568
3742
|
],
|
|
3743
|
+
"CreateSubtask": [
|
|
3744
|
+
"assignees.[]",
|
|
3745
|
+
"completer",
|
|
3746
|
+
"creator"
|
|
3747
|
+
],
|
|
3748
|
+
"CreateTemplateLibraryCardTable": [
|
|
3749
|
+
"creator",
|
|
3750
|
+
"lists.[].creator",
|
|
3751
|
+
"lists.[].subscribers.[]",
|
|
3752
|
+
"subscribers.[]",
|
|
3753
|
+
"wormholes.[].creator"
|
|
3754
|
+
],
|
|
3569
3755
|
"CreateTemplateLibraryCopy": [
|
|
3756
|
+
"destination_card_table.creator",
|
|
3757
|
+
"destination_card_table.lists.[].creator",
|
|
3758
|
+
"destination_card_table.lists.[].subscribers.[]",
|
|
3759
|
+
"destination_card_table.subscribers.[]",
|
|
3760
|
+
"destination_card_table.wormholes.[].creator",
|
|
3761
|
+
"destination_todolist.creator"
|
|
3762
|
+
],
|
|
3763
|
+
"CreateTemplateLibraryTodolist": [
|
|
3764
|
+
"creator"
|
|
3765
|
+
],
|
|
3766
|
+
"CreateTemplatification": [
|
|
3767
|
+
"destination_card_table.creator",
|
|
3768
|
+
"destination_card_table.lists.[].creator",
|
|
3769
|
+
"destination_card_table.lists.[].subscribers.[]",
|
|
3770
|
+
"destination_card_table.subscribers.[]",
|
|
3771
|
+
"destination_card_table.wormholes.[].creator",
|
|
3570
3772
|
"destination_todolist.creator"
|
|
3571
3773
|
],
|
|
3572
3774
|
"CreateTimesheetEntry": [
|
|
@@ -3914,10 +4116,31 @@
|
|
|
3914
4116
|
"GetSubscription": [
|
|
3915
4117
|
"subscribers.[]"
|
|
3916
4118
|
],
|
|
3917
|
-
"
|
|
3918
|
-
"
|
|
4119
|
+
"GetSubtask": [
|
|
4120
|
+
"assignees.[]",
|
|
4121
|
+
"completer",
|
|
4122
|
+
"creator"
|
|
4123
|
+
],
|
|
4124
|
+
"GetTemplateLibraryCardTables": [
|
|
4125
|
+
"card_tables.[].creator"
|
|
3919
4126
|
],
|
|
3920
4127
|
"GetTemplateLibraryCopy": [
|
|
4128
|
+
"destination_card_table.creator",
|
|
4129
|
+
"destination_card_table.lists.[].creator",
|
|
4130
|
+
"destination_card_table.lists.[].subscribers.[]",
|
|
4131
|
+
"destination_card_table.subscribers.[]",
|
|
4132
|
+
"destination_card_table.wormholes.[].creator",
|
|
4133
|
+
"destination_todolist.creator"
|
|
4134
|
+
],
|
|
4135
|
+
"GetTemplateLibraryTodolists": [
|
|
4136
|
+
"todolists.[].creator"
|
|
4137
|
+
],
|
|
4138
|
+
"GetTemplatification": [
|
|
4139
|
+
"destination_card_table.creator",
|
|
4140
|
+
"destination_card_table.lists.[].creator",
|
|
4141
|
+
"destination_card_table.lists.[].subscribers.[]",
|
|
4142
|
+
"destination_card_table.subscribers.[]",
|
|
4143
|
+
"destination_card_table.wormholes.[].creator",
|
|
3921
4144
|
"destination_todolist.creator"
|
|
3922
4145
|
],
|
|
3923
4146
|
"GetTimesheetEntry": [
|
|
@@ -4047,6 +4270,11 @@
|
|
|
4047
4270
|
"[].creator",
|
|
4048
4271
|
"[].participants.[]"
|
|
4049
4272
|
],
|
|
4273
|
+
"ListSubtasks": [
|
|
4274
|
+
"[].assignees.[]",
|
|
4275
|
+
"[].completer",
|
|
4276
|
+
"[].creator"
|
|
4277
|
+
],
|
|
4050
4278
|
"ListTodolistGroups": [
|
|
4051
4279
|
"[].creator"
|
|
4052
4280
|
],
|
|
@@ -4162,6 +4390,11 @@
|
|
|
4162
4390
|
"UpdateSubscription": [
|
|
4163
4391
|
"subscribers.[]"
|
|
4164
4392
|
],
|
|
4393
|
+
"UpdateSubtask": [
|
|
4394
|
+
"assignees.[]",
|
|
4395
|
+
"completer",
|
|
4396
|
+
"creator"
|
|
4397
|
+
],
|
|
4165
4398
|
"UpdateTimesheetEntry": [
|
|
4166
4399
|
"creator",
|
|
4167
4400
|
"person"
|
|
@@ -10,7 +10,9 @@ module Basecamp
|
|
|
10
10
|
# Reposition a step within a card
|
|
11
11
|
# @param card_id [Integer] card id ID
|
|
12
12
|
# @param source_id [Integer] source id
|
|
13
|
-
# @param position [Integer]
|
|
13
|
+
# @param position [Integer] The 1-based position to move it to (1 = top), the same `reposition_to`
|
|
14
|
+
# a to-do uses. bc3's doc said "Zero indexed" until BC3 #12659 corrected
|
|
15
|
+
# it; the server never was.
|
|
14
16
|
# @return [void]
|
|
15
17
|
def reposition(card_id:, source_id:, position:)
|
|
16
18
|
with_operation(service: "cardsteps", operation: "reposition", is_mutation: true, resource_id: card_id) do
|
|
@@ -141,12 +141,13 @@ module Basecamp
|
|
|
141
141
|
|
|
142
142
|
# Update notification settings for a check-in question
|
|
143
143
|
# @param question_id [Integer] question id ID
|
|
144
|
-
# @param
|
|
145
|
-
#
|
|
144
|
+
# @param responding [Boolean, nil] Whether the authenticated user is asked this question. Omit to leave it unchanged.
|
|
145
|
+
# Turning it on also subscribes the user unless `subscribed: false` is sent with it.
|
|
146
|
+
# @param subscribed [Boolean, nil] Whether the authenticated user is notified when someone answers. Omit to leave it unchanged.
|
|
146
147
|
# @return [Hash] response data
|
|
147
|
-
def update_notification_settings(question_id:,
|
|
148
|
+
def update_notification_settings(question_id:, responding: nil, subscribed: nil)
|
|
148
149
|
with_operation(service: "checkins", operation: "update_notification_settings", is_mutation: true, resource_id: question_id) do
|
|
149
|
-
http_put("/questions/#{question_id}/notification_settings.json", body: compact_params(
|
|
150
|
+
http_put("/questions/#{question_id}/notification_settings.json", body: compact_params(responding: responding, subscribed: subscribed)).json(operation: "UpdateQuestionNotificationSettings")
|
|
150
151
|
end
|
|
151
152
|
end
|
|
152
153
|
|
|
@@ -34,7 +34,8 @@ module Basecamp
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Mark specified items as read
|
|
37
|
-
# @param readables [Array] Array of readable_sgid values identifying the items to mark as read
|
|
37
|
+
# @param readables [Array] Array of readable_sgid values identifying the items to mark as read.
|
|
38
|
+
# At most 500 per request.
|
|
38
39
|
# @return [void]
|
|
39
40
|
def mark_as_read(readables:)
|
|
40
41
|
with_operation(service: "mynotifications", operation: "mark_as_read", is_mutation: true) do
|
|
@@ -23,6 +23,20 @@ module Basecamp
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Move a document, upload or vault into another vault in its project, or reorder it in its own vault.
|
|
27
|
+
# @param recording_id [Integer] recording id ID
|
|
28
|
+
# @param parent_id [Integer] The destination vault. The recording's current vault keeps it where it is
|
|
29
|
+
# and changes only its position.
|
|
30
|
+
# @param position [Integer, nil] 1-indexed position within the destination vault. Defaults to 1 (first); a
|
|
31
|
+
# position past the end places it last.
|
|
32
|
+
# @return [void]
|
|
33
|
+
def move_to_vault(recording_id:, parent_id:, position: nil)
|
|
34
|
+
with_operation(service: "recordings", operation: "move_to_vault", is_mutation: true, resource_id: recording_id) do
|
|
35
|
+
http_post("/recordings/#{recording_id}/filing.json", body: compact_params(parent_id: parent_id, position: position))
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
26
40
|
# Put a recording's card in the spotlight area on its project or template home page.
|
|
27
41
|
# @param recording_id [Integer] recording id ID
|
|
28
42
|
# @return [Hash] response data
|
|
@@ -52,7 +66,7 @@ module Basecamp
|
|
|
52
66
|
end
|
|
53
67
|
end
|
|
54
68
|
|
|
55
|
-
# Archive a recording
|
|
69
|
+
# Archive a recording; bc3 answers 403 for types it never lets be archived, timesheet entries among them
|
|
56
70
|
# @param recording_id [Integer] recording id ID
|
|
57
71
|
# @return [void]
|
|
58
72
|
def archive(recording_id:)
|
|
@@ -62,7 +76,7 @@ module Basecamp
|
|
|
62
76
|
end
|
|
63
77
|
end
|
|
64
78
|
|
|
65
|
-
# Trash a recording
|
|
79
|
+
# Trash a recording; bc3 answers 403 for types it never lets be trashed, timesheet entries among them (DestroyTimesheetEntry removes those, permanently)
|
|
66
80
|
# @param recording_id [Integer] recording id ID
|
|
67
81
|
# @return [void]
|
|
68
82
|
def trash(recording_id:)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Services
|
|
5
|
+
# Service for Subtasks operations
|
|
6
|
+
#
|
|
7
|
+
# @generated from OpenAPI spec
|
|
8
|
+
class SubtasksService < BaseService
|
|
9
|
+
|
|
10
|
+
# List a recording's subtasks, in position order
|
|
11
|
+
# @param recording_id [Integer] recording id ID
|
|
12
|
+
# @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
|
|
13
|
+
# @param max_items [Integer, nil] cap on items yielded across pages; nil or non-positive means no cap
|
|
14
|
+
# @return [ListEnumerator<Hash>] lazily paginated results (#meta carries pagination metadata)
|
|
15
|
+
def list(recording_id:, page: nil, max_items: nil)
|
|
16
|
+
wrap_paginated(service: "subtasks", operation: "list", is_mutation: false, resource_id: recording_id) do
|
|
17
|
+
params = compact_query_params(page: page)
|
|
18
|
+
paginate("/recordings/#{recording_id}/subtasks.json", params: params, operation: "ListSubtasks", max_items: max_items)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Create a subtask under a to-do or a card
|
|
23
|
+
# @param recording_id [Integer] recording id ID
|
|
24
|
+
# @param title [String] title
|
|
25
|
+
# @param due_on [String, nil] due on (YYYY-MM-DD)
|
|
26
|
+
# @param assignee_ids [Array, nil] assignee ids
|
|
27
|
+
# @return [Hash] response data
|
|
28
|
+
def create(recording_id:, title:, due_on: nil, assignee_ids: nil)
|
|
29
|
+
with_operation(service: "subtasks", operation: "create", is_mutation: true, resource_id: recording_id) do
|
|
30
|
+
http_post("/recordings/#{recording_id}/subtasks.json", body: compact_params(title: title, due_on: due_on, assignee_ids: assignee_ids)).json(operation: "CreateSubtask")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get a subtask by ID
|
|
35
|
+
# @param subtask_id [Integer] subtask id ID
|
|
36
|
+
# @return [Hash] response data
|
|
37
|
+
def get(subtask_id:)
|
|
38
|
+
with_operation(service: "subtasks", operation: "get", is_mutation: false, resource_id: subtask_id) do
|
|
39
|
+
http_get("/subtasks/#{subtask_id}", operation: "GetSubtask").json(operation: "GetSubtask")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Update a subtask
|
|
44
|
+
# @param subtask_id [Integer] subtask id ID
|
|
45
|
+
# @param title [String, nil] title
|
|
46
|
+
# @param due_on [String, nil] due on (YYYY-MM-DD)
|
|
47
|
+
# @param assignee_ids [Array, nil] assignee ids
|
|
48
|
+
# @return [Hash] response data
|
|
49
|
+
def update(subtask_id:, title: nil, due_on: nil, assignee_ids: nil)
|
|
50
|
+
with_operation(service: "subtasks", operation: "update", is_mutation: true, resource_id: subtask_id) do
|
|
51
|
+
http_put("/subtasks/#{subtask_id}", body: compact_params(title: title, due_on: due_on, assignee_ids: assignee_ids)).json(operation: "UpdateSubtask")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Delete a subtask
|
|
56
|
+
# @param subtask_id [Integer] subtask id ID
|
|
57
|
+
# @return [void]
|
|
58
|
+
def delete(subtask_id:)
|
|
59
|
+
with_operation(service: "subtasks", operation: "delete", is_mutation: true, resource_id: subtask_id) do
|
|
60
|
+
http_delete("/subtasks/#{subtask_id}")
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Mark a subtask as completed
|
|
66
|
+
# @param subtask_id [Integer] subtask id ID
|
|
67
|
+
# @return [void]
|
|
68
|
+
def complete(subtask_id:)
|
|
69
|
+
with_operation(service: "subtasks", operation: "complete", is_mutation: true, resource_id: subtask_id) do
|
|
70
|
+
http_post("/subtasks/#{subtask_id}/completion.json")
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Mark a subtask as not completed
|
|
76
|
+
# @param subtask_id [Integer] subtask id ID
|
|
77
|
+
# @return [void]
|
|
78
|
+
def uncomplete(subtask_id:)
|
|
79
|
+
with_operation(service: "subtasks", operation: "uncomplete", is_mutation: true, resource_id: subtask_id) do
|
|
80
|
+
http_delete("/subtasks/#{subtask_id}/completion.json")
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Move a subtask to a new position among its siblings
|
|
86
|
+
# @param subtask_id [Integer] subtask id ID
|
|
87
|
+
# @param position [Integer] The 1-based position to move it to
|
|
88
|
+
# @return [void]
|
|
89
|
+
def reposition(subtask_id:, position:)
|
|
90
|
+
with_operation(service: "subtasks", operation: "reposition", is_mutation: true, resource_id: subtask_id) do
|
|
91
|
+
http_put("/subtasks/#{subtask_id}/position.json", body: compact_params(position: position))
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -7,22 +7,63 @@ module Basecamp
|
|
|
7
7
|
# @generated from OpenAPI spec
|
|
8
8
|
class TemplatesService < BaseService
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# Templatify a to-do list or card table
|
|
11
|
+
# @param bucket_id [Integer] bucket id ID
|
|
12
|
+
# @param recording_id [Integer] The to-do list or card table to templatify. Anything else is a 403.
|
|
13
|
+
# @param template_name [String, nil] What to call the template. Defaults to the source recording's own title.
|
|
14
|
+
# @param copy_comments [Boolean, nil] Carry the comments across.
|
|
15
|
+
# @param copy_assignments [Boolean, nil] Carry assignees and the people involved across, adding them to the library
|
|
16
|
+
# if they are not already there.
|
|
17
|
+
# @param move_cards_to_triage [Boolean, nil] Gather the cards into Triage. Card tables only, and ignored otherwise.
|
|
11
18
|
# @return [Hash] response data
|
|
12
|
-
def
|
|
13
|
-
with_operation(service: "templates", operation: "
|
|
14
|
-
|
|
19
|
+
def create_templatification(bucket_id:, recording_id:, template_name: nil, copy_comments: nil, copy_assignments: nil, move_cards_to_triage: nil)
|
|
20
|
+
with_operation(service: "templates", operation: "create_templatification", is_mutation: true, project_id: bucket_id, resource_id: recording_id) do
|
|
21
|
+
http_post("/buckets/#{bucket_id}/recordings/#{recording_id}/templatifications.json", body: compact_params(template_name: template_name, copy_comments: copy_comments, copy_assignments: copy_assignments, move_cards_to_triage: move_cards_to_triage)).json(operation: "CreateTemplatification")
|
|
15
22
|
end
|
|
16
23
|
end
|
|
17
24
|
|
|
18
|
-
#
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
25
|
+
# Get a templatification
|
|
26
|
+
# @param bucket_id [Integer] bucket id ID
|
|
27
|
+
# @param recording_id [Integer] recording id ID
|
|
28
|
+
# @param templatification_id [Integer] templatification id ID
|
|
29
|
+
# @return [Hash] response data
|
|
30
|
+
def get_templatification(bucket_id:, recording_id:, templatification_id:)
|
|
31
|
+
with_operation(service: "templates", operation: "get_templatification", is_mutation: false, project_id: bucket_id, resource_id: templatification_id) do
|
|
32
|
+
http_get("/buckets/#{bucket_id}/recordings/#{recording_id}/templatifications/#{templatification_id}", operation: "GetTemplatification").json(operation: "GetTemplatification")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the account's card table templates
|
|
37
|
+
# @return [Hash] response data
|
|
38
|
+
def get_library_card_tables()
|
|
39
|
+
with_operation(service: "templates", operation: "get_library_card_tables", is_mutation: false) do
|
|
40
|
+
http_get("/template_library/card_tables.json", operation: "GetTemplateLibraryCardTables").json(operation: "GetTemplateLibraryCardTables")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Create a card table template with the default columns
|
|
45
|
+
# @param name [String] The template's name. Write-only: the response carries it as `title`.
|
|
46
|
+
# @return [Hash] response data
|
|
47
|
+
def create_library_card_table(name:)
|
|
48
|
+
with_operation(service: "templates", operation: "create_library_card_table", is_mutation: true) do
|
|
49
|
+
http_post("/template_library/card_tables.json", body: compact_params(name: name)).json(operation: "CreateTemplateLibraryCardTable")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Start copying a to-do list or card table template into a project
|
|
54
|
+
# @param template_recording_id [Integer] The to-do list or card table in the library to copy.
|
|
55
|
+
# @param destination_project_id [Integer, nil] The destination project. Basecamp resolves the container from the
|
|
56
|
+
# template's kind, so a caller naming a project needs to know nothing about
|
|
57
|
+
# docks or to-do sets. Supply this or destination_parent_id; if both are
|
|
58
|
+
# sent, destination_parent_id wins.
|
|
59
|
+
# @param destination_parent_id [Integer, nil] The container to copy into, for a caller that already holds one: the
|
|
60
|
+
# project's to-do set for a to-do list template, or its dock for a card
|
|
61
|
+
# table. A caller who may not edit it gets 404, not 403.
|
|
21
62
|
# @param adding_people_confirmed [Boolean, nil] Confirm granting destination-project access to people referenced by the template.
|
|
22
63
|
# @return [Hash] response data
|
|
23
|
-
def create_library_copy(template_recording_id:, destination_parent_id
|
|
64
|
+
def create_library_copy(template_recording_id:, destination_project_id: nil, destination_parent_id: nil, adding_people_confirmed: nil)
|
|
24
65
|
with_operation(service: "templates", operation: "create_library_copy", is_mutation: true) do
|
|
25
|
-
http_post("/template_library/copies.json", body: compact_params(template_recording_id: template_recording_id, destination_parent_id: destination_parent_id, adding_people_confirmed: adding_people_confirmed)).json(operation: "CreateTemplateLibraryCopy")
|
|
66
|
+
http_post("/template_library/copies.json", body: compact_params(template_recording_id: template_recording_id, destination_project_id: destination_project_id, destination_parent_id: destination_parent_id, adding_people_confirmed: adding_people_confirmed)).json(operation: "CreateTemplateLibraryCopy")
|
|
26
67
|
end
|
|
27
68
|
end
|
|
28
69
|
|
|
@@ -35,6 +76,24 @@ module Basecamp
|
|
|
35
76
|
end
|
|
36
77
|
end
|
|
37
78
|
|
|
79
|
+
# Get the account's to-do list templates
|
|
80
|
+
# @return [Hash] response data
|
|
81
|
+
def get_library_todolists()
|
|
82
|
+
with_operation(service: "templates", operation: "get_library_todolists", is_mutation: false) do
|
|
83
|
+
http_get("/template_library/todolists.json", operation: "GetTemplateLibraryTodolists").json(operation: "GetTemplateLibraryTodolists")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Create an empty to-do list template
|
|
88
|
+
# @param name [String] What to call the template.
|
|
89
|
+
# @param description [String, nil] Rich text describing the template.
|
|
90
|
+
# @return [Hash] response data
|
|
91
|
+
def create_library_todolist(name:, description: nil)
|
|
92
|
+
with_operation(service: "templates", operation: "create_library_todolist", is_mutation: true) do
|
|
93
|
+
http_post("/template_library/todolists.json", body: compact_params(name: name, description: description)).json(operation: "CreateTemplateLibraryTodolist")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
38
97
|
# List all templates visible to the current user
|
|
39
98
|
# @param status [String, nil] active|archived|trashed
|
|
40
99
|
# @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
|
|
@@ -83,7 +83,7 @@ module Basecamp
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
# Permanently delete a timesheet entry; answers 403 when the caller may not
|
|
86
|
+
# Permanently delete a timesheet entry, which never appears in the trash and cannot be restored; this is the only way to remove one, since bc3 answers 403 to TrashRecording and ArchiveRecording for every timesheet entry, and it answers 403 when the caller may not remove the entry.
|
|
87
87
|
# @param entry_id [Integer] entry id ID
|
|
88
88
|
# @return [void]
|
|
89
89
|
def destroy(entry_id:)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Auto-generated from OpenAPI spec. Do not edit manually.
|
|
4
|
-
# Generated: 2026-
|
|
4
|
+
# Generated: 2026-10-03T00:10:53Z
|
|
5
5
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "time"
|
|
@@ -594,7 +594,7 @@ module Basecamp
|
|
|
594
594
|
# Card
|
|
595
595
|
class Card
|
|
596
596
|
include TypeHelpers
|
|
597
|
-
attr_accessor :app_url, :bucket, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completed_at, :completer, :completion_subscribers, :completion_url, :content, :description, :due_on, :position, :steps, :subscription_url
|
|
597
|
+
attr_accessor :app_url, :bucket, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completed_at, :completer, :completion_subscribers, :completion_url, :content, :description, :due_on, :position, :steps, :subscription_url, :subtasks_completed_count, :subtasks_count, :subtasks_url
|
|
598
598
|
|
|
599
599
|
# @return [Array<Symbol>]
|
|
600
600
|
def self.required_fields
|
|
@@ -631,8 +631,11 @@ module Basecamp
|
|
|
631
631
|
@description = data["description"]
|
|
632
632
|
@due_on = data["due_on"]
|
|
633
633
|
@position = parse_integer(data["position"])
|
|
634
|
-
@steps = parse_array(data["steps"], "
|
|
634
|
+
@steps = parse_array(data["steps"], "Subtask")
|
|
635
635
|
@subscription_url = data["subscription_url"]
|
|
636
|
+
@subtasks_completed_count = parse_integer(data["subtasks_completed_count"])
|
|
637
|
+
@subtasks_count = parse_integer(data["subtasks_count"])
|
|
638
|
+
@subtasks_url = data["subtasks_url"]
|
|
636
639
|
end
|
|
637
640
|
|
|
638
641
|
def to_h
|
|
@@ -668,6 +671,9 @@ module Basecamp
|
|
|
668
671
|
"position" => @position,
|
|
669
672
|
"steps" => @steps,
|
|
670
673
|
"subscription_url" => @subscription_url,
|
|
674
|
+
"subtasks_completed_count" => @subtasks_completed_count,
|
|
675
|
+
"subtasks_count" => @subtasks_count,
|
|
676
|
+
"subtasks_url" => @subtasks_url,
|
|
671
677
|
}.compact
|
|
672
678
|
end
|
|
673
679
|
|
|
@@ -782,75 +788,10 @@ module Basecamp
|
|
|
782
788
|
end
|
|
783
789
|
end
|
|
784
790
|
|
|
785
|
-
# CardStep
|
|
786
|
-
class CardStep
|
|
787
|
-
include TypeHelpers
|
|
788
|
-
attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :completed, :completed_at, :completer, :completion_url, :due_on, :position
|
|
789
|
-
|
|
790
|
-
# @return [Array<Symbol>]
|
|
791
|
-
def self.required_fields
|
|
792
|
-
%i[app_url bucket created_at creator id inherits_status parent status title type updated_at url visible_to_clients].freeze
|
|
793
|
-
end
|
|
794
|
-
|
|
795
|
-
def initialize(data = {})
|
|
796
|
-
@app_url = data["app_url"]
|
|
797
|
-
@bucket = parse_type(data["bucket"], "TodoBucket")
|
|
798
|
-
@created_at = parse_datetime(data["created_at"])
|
|
799
|
-
@creator = parse_type(data["creator"], "Person")
|
|
800
|
-
@id = parse_integer(data["id"])
|
|
801
|
-
@inherits_status = parse_boolean(data["inherits_status"])
|
|
802
|
-
@parent = parse_type(data["parent"], "RecordingParent")
|
|
803
|
-
@status = data["status"]
|
|
804
|
-
@title = data["title"]
|
|
805
|
-
@type = data["type"]
|
|
806
|
-
@updated_at = parse_datetime(data["updated_at"])
|
|
807
|
-
@url = data["url"]
|
|
808
|
-
@visible_to_clients = parse_boolean(data["visible_to_clients"])
|
|
809
|
-
@assignees = parse_array(data["assignees"], "Person")
|
|
810
|
-
@bookmark_url = data["bookmark_url"]
|
|
811
|
-
@completed = parse_boolean(data["completed"])
|
|
812
|
-
@completed_at = parse_datetime(data["completed_at"])
|
|
813
|
-
@completer = parse_type(data["completer"], "Person")
|
|
814
|
-
@completion_url = data["completion_url"]
|
|
815
|
-
@due_on = data["due_on"]
|
|
816
|
-
@position = parse_integer(data["position"])
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
def to_h
|
|
820
|
-
{
|
|
821
|
-
"app_url" => @app_url,
|
|
822
|
-
"bucket" => @bucket,
|
|
823
|
-
"created_at" => @created_at,
|
|
824
|
-
"creator" => @creator,
|
|
825
|
-
"id" => @id,
|
|
826
|
-
"inherits_status" => @inherits_status,
|
|
827
|
-
"parent" => @parent,
|
|
828
|
-
"status" => @status,
|
|
829
|
-
"title" => @title,
|
|
830
|
-
"type" => @type,
|
|
831
|
-
"updated_at" => @updated_at,
|
|
832
|
-
"url" => @url,
|
|
833
|
-
"visible_to_clients" => @visible_to_clients,
|
|
834
|
-
"assignees" => @assignees,
|
|
835
|
-
"bookmark_url" => @bookmark_url,
|
|
836
|
-
"completed" => @completed,
|
|
837
|
-
"completed_at" => @completed_at,
|
|
838
|
-
"completer" => @completer,
|
|
839
|
-
"completion_url" => @completion_url,
|
|
840
|
-
"due_on" => @due_on,
|
|
841
|
-
"position" => @position,
|
|
842
|
-
}.compact
|
|
843
|
-
end
|
|
844
|
-
|
|
845
|
-
def to_json(*args)
|
|
846
|
-
to_h.to_json(*args)
|
|
847
|
-
end
|
|
848
|
-
end
|
|
849
|
-
|
|
850
791
|
# CardTable
|
|
851
792
|
class CardTable
|
|
852
793
|
include TypeHelpers
|
|
853
|
-
attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :lists, :subscribers, :subscription_url, :wormholes
|
|
794
|
+
attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :lists, :parent, :position, :public_link_url, :subscribers, :subscription_url, :wormholes
|
|
854
795
|
|
|
855
796
|
# @return [Array<Symbol>]
|
|
856
797
|
def self.required_fields
|
|
@@ -872,6 +813,9 @@ module Basecamp
|
|
|
872
813
|
@visible_to_clients = parse_boolean(data["visible_to_clients"])
|
|
873
814
|
@bookmark_url = data["bookmark_url"]
|
|
874
815
|
@lists = parse_array(data["lists"], "CardColumn")
|
|
816
|
+
@parent = parse_type(data["parent"], "RecordingParent")
|
|
817
|
+
@position = parse_integer(data["position"])
|
|
818
|
+
@public_link_url = data["public_link_url"]
|
|
875
819
|
@subscribers = parse_array(data["subscribers"], "Person")
|
|
876
820
|
@subscription_url = data["subscription_url"]
|
|
877
821
|
@wormholes = parse_array(data["wormholes"], "Wormhole")
|
|
@@ -893,6 +837,9 @@ module Basecamp
|
|
|
893
837
|
"visible_to_clients" => @visible_to_clients,
|
|
894
838
|
"bookmark_url" => @bookmark_url,
|
|
895
839
|
"lists" => @lists,
|
|
840
|
+
"parent" => @parent,
|
|
841
|
+
"position" => @position,
|
|
842
|
+
"public_link_url" => @public_link_url,
|
|
896
843
|
"subscribers" => @subscribers,
|
|
897
844
|
"subscription_url" => @subscription_url,
|
|
898
845
|
"wormholes" => @wormholes,
|
|
@@ -3819,7 +3766,7 @@ module Basecamp
|
|
|
3819
3766
|
# Recording
|
|
3820
3767
|
class Recording
|
|
3821
3768
|
include TypeHelpers
|
|
3822
|
-
attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :boosts_count, :boosts_url, :bubble_up_url, :category, :comments_count, :comments_url, :content, :content_attachments, :description, :description_attachments, :from, :group_on, :parent, :position, :replies_count, :replies_url, :service, :subject, :subscription_url
|
|
3769
|
+
attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :boosts_count, :boosts_url, :bubble_up_url, :category, :comments_count, :comments_url, :content, :content_attachments, :description, :description_attachments, :from, :group_on, :parent, :position, :replies_count, :replies_url, :service, :subject, :subscription_url, :subtasks_completed_count, :subtasks_count, :subtasks_url
|
|
3823
3770
|
|
|
3824
3771
|
# @return [Array<Symbol>]
|
|
3825
3772
|
def self.required_fields
|
|
@@ -3859,6 +3806,9 @@ module Basecamp
|
|
|
3859
3806
|
@service = parse_type(data["service"], "DoorService")
|
|
3860
3807
|
@subject = data["subject"]
|
|
3861
3808
|
@subscription_url = data["subscription_url"]
|
|
3809
|
+
@subtasks_completed_count = parse_integer(data["subtasks_completed_count"])
|
|
3810
|
+
@subtasks_count = parse_integer(data["subtasks_count"])
|
|
3811
|
+
@subtasks_url = data["subtasks_url"]
|
|
3862
3812
|
end
|
|
3863
3813
|
|
|
3864
3814
|
def to_h
|
|
@@ -3895,6 +3845,9 @@ module Basecamp
|
|
|
3895
3845
|
"service" => @service,
|
|
3896
3846
|
"subject" => @subject,
|
|
3897
3847
|
"subscription_url" => @subscription_url,
|
|
3848
|
+
"subtasks_completed_count" => @subtasks_completed_count,
|
|
3849
|
+
"subtasks_count" => @subtasks_count,
|
|
3850
|
+
"subtasks_url" => @subtasks_url,
|
|
3898
3851
|
}.compact
|
|
3899
3852
|
end
|
|
3900
3853
|
|
|
@@ -4462,6 +4415,71 @@ module Basecamp
|
|
|
4462
4415
|
end
|
|
4463
4416
|
end
|
|
4464
4417
|
|
|
4418
|
+
# Subtask
|
|
4419
|
+
class Subtask
|
|
4420
|
+
include TypeHelpers
|
|
4421
|
+
attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :completed, :completed_at, :completer, :completion_url, :due_on, :position
|
|
4422
|
+
|
|
4423
|
+
# @return [Array<Symbol>]
|
|
4424
|
+
def self.required_fields
|
|
4425
|
+
%i[app_url bucket created_at creator id inherits_status parent status title type updated_at url visible_to_clients].freeze
|
|
4426
|
+
end
|
|
4427
|
+
|
|
4428
|
+
def initialize(data = {})
|
|
4429
|
+
@app_url = data["app_url"]
|
|
4430
|
+
@bucket = parse_type(data["bucket"], "TodoBucket")
|
|
4431
|
+
@created_at = parse_datetime(data["created_at"])
|
|
4432
|
+
@creator = parse_type(data["creator"], "Person")
|
|
4433
|
+
@id = parse_integer(data["id"])
|
|
4434
|
+
@inherits_status = parse_boolean(data["inherits_status"])
|
|
4435
|
+
@parent = parse_type(data["parent"], "RecordingParent")
|
|
4436
|
+
@status = data["status"]
|
|
4437
|
+
@title = data["title"]
|
|
4438
|
+
@type = data["type"]
|
|
4439
|
+
@updated_at = parse_datetime(data["updated_at"])
|
|
4440
|
+
@url = data["url"]
|
|
4441
|
+
@visible_to_clients = parse_boolean(data["visible_to_clients"])
|
|
4442
|
+
@assignees = parse_array(data["assignees"], "Person")
|
|
4443
|
+
@bookmark_url = data["bookmark_url"]
|
|
4444
|
+
@completed = parse_boolean(data["completed"])
|
|
4445
|
+
@completed_at = parse_datetime(data["completed_at"])
|
|
4446
|
+
@completer = parse_type(data["completer"], "Person")
|
|
4447
|
+
@completion_url = data["completion_url"]
|
|
4448
|
+
@due_on = data["due_on"]
|
|
4449
|
+
@position = parse_integer(data["position"])
|
|
4450
|
+
end
|
|
4451
|
+
|
|
4452
|
+
def to_h
|
|
4453
|
+
{
|
|
4454
|
+
"app_url" => @app_url,
|
|
4455
|
+
"bucket" => @bucket,
|
|
4456
|
+
"created_at" => @created_at,
|
|
4457
|
+
"creator" => @creator,
|
|
4458
|
+
"id" => @id,
|
|
4459
|
+
"inherits_status" => @inherits_status,
|
|
4460
|
+
"parent" => @parent,
|
|
4461
|
+
"status" => @status,
|
|
4462
|
+
"title" => @title,
|
|
4463
|
+
"type" => @type,
|
|
4464
|
+
"updated_at" => @updated_at,
|
|
4465
|
+
"url" => @url,
|
|
4466
|
+
"visible_to_clients" => @visible_to_clients,
|
|
4467
|
+
"assignees" => @assignees,
|
|
4468
|
+
"bookmark_url" => @bookmark_url,
|
|
4469
|
+
"completed" => @completed,
|
|
4470
|
+
"completed_at" => @completed_at,
|
|
4471
|
+
"completer" => @completer,
|
|
4472
|
+
"completion_url" => @completion_url,
|
|
4473
|
+
"due_on" => @due_on,
|
|
4474
|
+
"position" => @position,
|
|
4475
|
+
}.compact
|
|
4476
|
+
end
|
|
4477
|
+
|
|
4478
|
+
def to_json(*args)
|
|
4479
|
+
to_h.to_json(*args)
|
|
4480
|
+
end
|
|
4481
|
+
end
|
|
4482
|
+
|
|
4465
4483
|
# Template
|
|
4466
4484
|
class Template
|
|
4467
4485
|
include TypeHelpers
|
|
@@ -4503,27 +4521,27 @@ module Basecamp
|
|
|
4503
4521
|
end
|
|
4504
4522
|
end
|
|
4505
4523
|
|
|
4506
|
-
#
|
|
4507
|
-
class
|
|
4524
|
+
# TemplateLibraryCardTables
|
|
4525
|
+
class TemplateLibraryCardTables
|
|
4508
4526
|
include TypeHelpers
|
|
4509
|
-
attr_accessor :bucket, :
|
|
4527
|
+
attr_accessor :bucket, :card_tables, :kanban_boardset
|
|
4510
4528
|
|
|
4511
4529
|
# @return [Array<Symbol>]
|
|
4512
4530
|
def self.required_fields
|
|
4513
|
-
%i[bucket
|
|
4531
|
+
%i[bucket card_tables kanban_boardset].freeze
|
|
4514
4532
|
end
|
|
4515
4533
|
|
|
4516
4534
|
def initialize(data = {})
|
|
4517
4535
|
@bucket = parse_type(data["bucket"], "RecordingBucket")
|
|
4518
|
-
@
|
|
4519
|
-
@
|
|
4536
|
+
@card_tables = parse_array(data["card_tables"], "Recording")
|
|
4537
|
+
@kanban_boardset = data["kanban_boardset"]
|
|
4520
4538
|
end
|
|
4521
4539
|
|
|
4522
4540
|
def to_h
|
|
4523
4541
|
{
|
|
4524
4542
|
"bucket" => @bucket,
|
|
4525
|
-
"
|
|
4526
|
-
"
|
|
4543
|
+
"card_tables" => @card_tables,
|
|
4544
|
+
"kanban_boardset" => @kanban_boardset,
|
|
4527
4545
|
}.compact
|
|
4528
4546
|
end
|
|
4529
4547
|
|
|
@@ -4564,7 +4582,7 @@ module Basecamp
|
|
|
4564
4582
|
# TemplateLibraryCopy
|
|
4565
4583
|
class TemplateLibraryCopy
|
|
4566
4584
|
include TypeHelpers
|
|
4567
|
-
attr_accessor :destination_parent_id, :id, :source_recording_id, :status, :url, :destination_todolist
|
|
4585
|
+
attr_accessor :destination_parent_id, :id, :source_recording_id, :status, :url, :destination_card_table, :destination_todolist
|
|
4568
4586
|
|
|
4569
4587
|
# @return [Array<Symbol>]
|
|
4570
4588
|
def self.required_fields
|
|
@@ -4577,6 +4595,7 @@ module Basecamp
|
|
|
4577
4595
|
@source_recording_id = parse_integer(data["source_recording_id"])
|
|
4578
4596
|
@status = data["status"]
|
|
4579
4597
|
@url = data["url"]
|
|
4598
|
+
@destination_card_table = parse_type(data["destination_card_table"], "CardTable")
|
|
4580
4599
|
@destination_todolist = parse_type(data["destination_todolist"], "Todolist")
|
|
4581
4600
|
end
|
|
4582
4601
|
|
|
@@ -4587,6 +4606,71 @@ module Basecamp
|
|
|
4587
4606
|
"source_recording_id" => @source_recording_id,
|
|
4588
4607
|
"status" => @status,
|
|
4589
4608
|
"url" => @url,
|
|
4609
|
+
"destination_card_table" => @destination_card_table,
|
|
4610
|
+
"destination_todolist" => @destination_todolist,
|
|
4611
|
+
}.compact
|
|
4612
|
+
end
|
|
4613
|
+
|
|
4614
|
+
def to_json(*args)
|
|
4615
|
+
to_h.to_json(*args)
|
|
4616
|
+
end
|
|
4617
|
+
end
|
|
4618
|
+
|
|
4619
|
+
# TemplateLibraryTodolists
|
|
4620
|
+
class TemplateLibraryTodolists
|
|
4621
|
+
include TypeHelpers
|
|
4622
|
+
attr_accessor :bucket, :todolists, :todoset
|
|
4623
|
+
|
|
4624
|
+
# @return [Array<Symbol>]
|
|
4625
|
+
def self.required_fields
|
|
4626
|
+
%i[bucket todolists todoset].freeze
|
|
4627
|
+
end
|
|
4628
|
+
|
|
4629
|
+
def initialize(data = {})
|
|
4630
|
+
@bucket = parse_type(data["bucket"], "RecordingBucket")
|
|
4631
|
+
@todolists = parse_array(data["todolists"], "Todolist")
|
|
4632
|
+
@todoset = parse_type(data["todoset"], "RecordingParent")
|
|
4633
|
+
end
|
|
4634
|
+
|
|
4635
|
+
def to_h
|
|
4636
|
+
{
|
|
4637
|
+
"bucket" => @bucket,
|
|
4638
|
+
"todolists" => @todolists,
|
|
4639
|
+
"todoset" => @todoset,
|
|
4640
|
+
}.compact
|
|
4641
|
+
end
|
|
4642
|
+
|
|
4643
|
+
def to_json(*args)
|
|
4644
|
+
to_h.to_json(*args)
|
|
4645
|
+
end
|
|
4646
|
+
end
|
|
4647
|
+
|
|
4648
|
+
# Templatification
|
|
4649
|
+
class Templatification
|
|
4650
|
+
include TypeHelpers
|
|
4651
|
+
attr_accessor :id, :source_recording_id, :status, :url, :destination_card_table, :destination_todolist
|
|
4652
|
+
|
|
4653
|
+
# @return [Array<Symbol>]
|
|
4654
|
+
def self.required_fields
|
|
4655
|
+
%i[id source_recording_id status url].freeze
|
|
4656
|
+
end
|
|
4657
|
+
|
|
4658
|
+
def initialize(data = {})
|
|
4659
|
+
@id = parse_integer(data["id"])
|
|
4660
|
+
@source_recording_id = parse_integer(data["source_recording_id"])
|
|
4661
|
+
@status = data["status"]
|
|
4662
|
+
@url = data["url"]
|
|
4663
|
+
@destination_card_table = parse_type(data["destination_card_table"], "CardTable")
|
|
4664
|
+
@destination_todolist = parse_type(data["destination_todolist"], "Todolist")
|
|
4665
|
+
end
|
|
4666
|
+
|
|
4667
|
+
def to_h
|
|
4668
|
+
{
|
|
4669
|
+
"id" => @id,
|
|
4670
|
+
"source_recording_id" => @source_recording_id,
|
|
4671
|
+
"status" => @status,
|
|
4672
|
+
"url" => @url,
|
|
4673
|
+
"destination_card_table" => @destination_card_table,
|
|
4590
4674
|
"destination_todolist" => @destination_todolist,
|
|
4591
4675
|
}.compact
|
|
4592
4676
|
end
|
|
@@ -4827,7 +4911,7 @@ module Basecamp
|
|
|
4827
4911
|
# Todo
|
|
4828
4912
|
class Todo
|
|
4829
4913
|
include TypeHelpers
|
|
4830
|
-
attr_accessor :app_url, :bucket, :content, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completion_subscribers, :completion_url, :description, :due_on, :position, :starts_on, :steps, :subscription_url
|
|
4914
|
+
attr_accessor :app_url, :bucket, :content, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completion_subscribers, :completion_url, :description, :due_on, :position, :starts_on, :steps, :subscription_url, :subtasks_completed_count, :subtasks_count, :subtasks_url
|
|
4831
4915
|
|
|
4832
4916
|
# @return [Array<Symbol>]
|
|
4833
4917
|
def self.required_fields
|
|
@@ -4863,8 +4947,11 @@ module Basecamp
|
|
|
4863
4947
|
@due_on = data["due_on"]
|
|
4864
4948
|
@position = parse_integer(data["position"])
|
|
4865
4949
|
@starts_on = data["starts_on"]
|
|
4866
|
-
@steps = parse_array(data["steps"], "
|
|
4950
|
+
@steps = parse_array(data["steps"], "Subtask")
|
|
4867
4951
|
@subscription_url = data["subscription_url"]
|
|
4952
|
+
@subtasks_completed_count = parse_integer(data["subtasks_completed_count"])
|
|
4953
|
+
@subtasks_count = parse_integer(data["subtasks_count"])
|
|
4954
|
+
@subtasks_url = data["subtasks_url"]
|
|
4868
4955
|
end
|
|
4869
4956
|
|
|
4870
4957
|
def to_h
|
|
@@ -4899,6 +4986,9 @@ module Basecamp
|
|
|
4899
4986
|
"starts_on" => @starts_on,
|
|
4900
4987
|
"steps" => @steps,
|
|
4901
4988
|
"subscription_url" => @subscription_url,
|
|
4989
|
+
"subtasks_completed_count" => @subtasks_completed_count,
|
|
4990
|
+
"subtasks_count" => @subtasks_count,
|
|
4991
|
+
"subtasks_url" => @subtasks_url,
|
|
4902
4992
|
}.compact
|
|
4903
4993
|
end
|
|
4904
4994
|
|
|
@@ -5875,5 +5965,9 @@ module Basecamp
|
|
|
5875
5965
|
to_h.to_json(*args)
|
|
5876
5966
|
end
|
|
5877
5967
|
end
|
|
5968
|
+
|
|
5969
|
+
# CardStep
|
|
5970
|
+
# @deprecated renamed to Subtask, the same Kanban::Step record.
|
|
5971
|
+
CardStep = Subtask
|
|
5878
5972
|
end
|
|
5879
5973
|
end
|
data/lib/basecamp/version.rb
CHANGED
|
@@ -82,6 +82,7 @@ class ServiceGenerator
|
|
|
82
82
|
'Projects' => 'Projects',
|
|
83
83
|
'ClientFeatures' => 'ClientFeatures',
|
|
84
84
|
'Boosts' => 'Boosts',
|
|
85
|
+
'Subtasks' => 'Subtasks',
|
|
85
86
|
'Untagged' => 'Miscellaneous'
|
|
86
87
|
}.freeze
|
|
87
88
|
|
|
@@ -178,6 +179,7 @@ class ServiceGenerator
|
|
|
178
179
|
METHOD_NAME_OVERRIDES = {
|
|
179
180
|
'SpotlightRecording' => 'spotlight',
|
|
180
181
|
'UnspotlightRecording' => 'unspotlight',
|
|
182
|
+
'MoveRecordingToVault' => 'move_to_vault',
|
|
181
183
|
'GetMyProfile' => 'my_profile',
|
|
182
184
|
'GetTodolistOrGroup' => 'get',
|
|
183
185
|
# The plain `update` name belongs to the merge-safe composite; the raw
|
|
@@ -214,7 +216,10 @@ class ServiceGenerator
|
|
|
214
216
|
'Search' => 'search',
|
|
215
217
|
'CreateProjectFromTemplate' => 'create_project',
|
|
216
218
|
'GetProjectConstruction' => 'get_construction',
|
|
217
|
-
'
|
|
219
|
+
'GetTemplateLibraryTodolists' => 'get_library_todolists',
|
|
220
|
+
'GetTemplateLibraryCardTables' => 'get_library_card_tables',
|
|
221
|
+
'CreateTemplateLibraryCardTable' => 'create_library_card_table',
|
|
222
|
+
'CreateTemplateLibraryTodolist' => 'create_library_todolist',
|
|
218
223
|
'CreateTemplateLibraryCopy' => 'create_library_copy',
|
|
219
224
|
'GetTemplateLibraryCopy' => 'get_library_copy',
|
|
220
225
|
'GetRecordingTimesheet' => 'for_recording',
|
|
@@ -358,7 +363,7 @@ class ServiceGenerator
|
|
|
358
363
|
lineupmarker clientapproval clientapprovals clientcorrespondence
|
|
359
364
|
clientcorrespondences clientreply clientreplies forwardreply
|
|
360
365
|
forwardreplies campfireline campfirelines todolistgroup todolistgroups
|
|
361
|
-
todolistorgroup uploadversions hillchart hillcharts
|
|
366
|
+
todolistorgroup uploadversions hillchart hillcharts subtask subtasks
|
|
362
367
|
wormhole wormholes
|
|
363
368
|
].freeze
|
|
364
369
|
|
|
@@ -791,7 +796,7 @@ class ServiceGenerator
|
|
|
791
796
|
op[:path_params].each do |p|
|
|
792
797
|
ruby_name = to_snake_case(p[:name])
|
|
793
798
|
type = p[:type] || 'Integer'
|
|
794
|
-
desc = p[:description] || "#{ruby_name.gsub('_', ' ')} ID"
|
|
799
|
+
desc = yard_param_description(p[:description] || "#{ruby_name.gsub('_', ' ')} ID")
|
|
795
800
|
lines << " # @param #{ruby_name} [#{type}] #{desc}"
|
|
796
801
|
end
|
|
797
802
|
|
data/scripts/generate-types.rb
CHANGED
|
@@ -132,6 +132,59 @@ def deprecation_doc_lines(reason, indent:, tag_prefix: '')
|
|
|
132
132
|
out
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
# Whether the main loop emits a class for this component. The alias pass checks
|
|
136
|
+
# its targets against exactly this predicate, so an alias can never name a
|
|
137
|
+
# class that was not emitted (a *RequestContent target used to produce
|
|
138
|
+
# `Alias = CreateCardStepRequestContent`, a NameError the moment types.rb loads).
|
|
139
|
+
def emits_class?(name, schema)
|
|
140
|
+
return false if SKIP_PATTERNS.any? { |p| name.match?(p) }
|
|
141
|
+
|
|
142
|
+
schema.is_a?(Hash) && schema['type'] == 'object' && !(schema['properties'] || {}).empty?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
DEPRECATED_ALIAS_KEYS = %w[$ref deprecated description x-deprecated-reason].freeze
|
|
146
|
+
|
|
147
|
+
# A component that is nothing but a deprecated $ref: a rename kept for
|
|
148
|
+
# compatibility, e.g. CardStep -> Subtask. `deprecated` must be the JSON boolean
|
|
149
|
+
# true, as in every other generator; 1 or "false" is not an alias.
|
|
150
|
+
def deprecated_alias?(schema)
|
|
151
|
+
schema.is_a?(Hash) && schema['deprecated'] == true && schema['$ref'].is_a?(String) &&
|
|
152
|
+
(schema.keys - DEPRECATED_ALIAS_KEYS).empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Constants Basecamp::Types already holds besides the generated classes.
|
|
156
|
+
RESERVED_TYPE_CONSTANTS = %w[TypeHelpers].freeze
|
|
157
|
+
|
|
158
|
+
# Every deprecated alias as [alias, target], sorted by alias. Aborts on one this
|
|
159
|
+
# generator cannot place, the same cases every SDK generator refuses: a missing
|
|
160
|
+
# target, an alias of an alias (rejected, not resolved), a target that is not an
|
|
161
|
+
# object model, a target this generator does not emit, and a name collision.
|
|
162
|
+
def deprecated_aliases(schemas)
|
|
163
|
+
schemas.keys.sort.filter_map do |name|
|
|
164
|
+
schema = schemas[name]
|
|
165
|
+
next unless deprecated_alias?(schema)
|
|
166
|
+
|
|
167
|
+
target = schema['$ref'].split('/').last
|
|
168
|
+
target_schema = schemas[target]
|
|
169
|
+
problem =
|
|
170
|
+
if !schemas.key?(target)
|
|
171
|
+
'target schema does not exist'
|
|
172
|
+
elsif deprecated_alias?(target_schema)
|
|
173
|
+
"target is itself a deprecated alias; point #{name} at the model directly"
|
|
174
|
+
elsif !(target_schema.is_a?(Hash) && target_schema['type'] == 'object' &&
|
|
175
|
+
!(target_schema['properties'] || {}).empty?)
|
|
176
|
+
'target is not an object model'
|
|
177
|
+
elsif !emits_class?(target, target_schema)
|
|
178
|
+
'target class was not emitted (SKIP_PATTERNS excludes it)'
|
|
179
|
+
elsif RESERVED_TYPE_CONSTANTS.include?(name)
|
|
180
|
+
"#{name} collides with an existing constant in Basecamp::Types"
|
|
181
|
+
end
|
|
182
|
+
abort "Error: deprecated alias #{name} -> #{target}: #{problem}" if problem
|
|
183
|
+
|
|
184
|
+
[ name, target ]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
135
188
|
# Main execution
|
|
136
189
|
if __FILE__ == $PROGRAM_NAME
|
|
137
190
|
openapi_path = ARGV[0] || File.expand_path('../../openapi.json', __dir__)
|
|
@@ -141,6 +194,13 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
141
194
|
exit 1
|
|
142
195
|
end
|
|
143
196
|
|
|
197
|
+
# UTF-8 regardless of process locale — see generate-metadata.rb
|
|
198
|
+
schemas = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))['components']['schemas'] || {}
|
|
199
|
+
sorted = schemas.keys.sort
|
|
200
|
+
# Validated before the first line is printed, so a refusal leaves no partial
|
|
201
|
+
# types.rb on stdout.
|
|
202
|
+
aliases = deprecated_aliases(schemas)
|
|
203
|
+
|
|
144
204
|
puts header
|
|
145
205
|
puts generate_helpers
|
|
146
206
|
puts ''
|
|
@@ -148,18 +208,11 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
148
208
|
puts ' module Types'
|
|
149
209
|
puts ' include TypeHelpers'
|
|
150
210
|
|
|
151
|
-
# UTF-8 regardless of process locale — see generate-metadata.rb
|
|
152
|
-
schemas = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))['components']['schemas'] || {}
|
|
153
|
-
sorted = schemas.keys.sort
|
|
154
|
-
|
|
155
211
|
sorted.each do |name|
|
|
156
|
-
next if SKIP_PATTERNS.any? { |p| name.match?(p) }
|
|
157
|
-
|
|
158
212
|
schema = schemas[name]
|
|
159
|
-
next unless schema
|
|
213
|
+
next unless emits_class?(name, schema)
|
|
160
214
|
|
|
161
|
-
properties = schema['properties']
|
|
162
|
-
next if properties.empty?
|
|
215
|
+
properties = schema['properties']
|
|
163
216
|
|
|
164
217
|
required_fields = schema['required'] || []
|
|
165
218
|
required_set = required_fields.to_set
|
|
@@ -273,6 +326,17 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
273
326
|
puts ' end'
|
|
274
327
|
end
|
|
275
328
|
|
|
329
|
+
# Deprecated former names (see deprecated_aliases), emitted after every class
|
|
330
|
+
# because the alias must reference an already-defined constant. The alias
|
|
331
|
+
# also keeps Basecamp::Types.const_get("CardStep") resolving.
|
|
332
|
+
aliases.each do |name, target|
|
|
333
|
+
schema = schemas[name]
|
|
334
|
+
puts ''
|
|
335
|
+
puts " # #{name}"
|
|
336
|
+
puts deprecation_doc_lines(schema['x-deprecated-reason'] || 'deprecated', indent: ' ')
|
|
337
|
+
puts " #{name} = #{target}"
|
|
338
|
+
end
|
|
339
|
+
|
|
276
340
|
puts ' end'
|
|
277
341
|
puts 'end'
|
|
278
342
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: basecamp-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Basecamp
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -224,6 +224,7 @@ files:
|
|
|
224
224
|
- lib/basecamp/generated/services/schedules_service.rb
|
|
225
225
|
- lib/basecamp/generated/services/search_service.rb
|
|
226
226
|
- lib/basecamp/generated/services/subscriptions_service.rb
|
|
227
|
+
- lib/basecamp/generated/services/subtasks_service.rb
|
|
227
228
|
- lib/basecamp/generated/services/templates_service.rb
|
|
228
229
|
- lib/basecamp/generated/services/timeline_service.rb
|
|
229
230
|
- lib/basecamp/generated/services/timesheets_service.rb
|