activeproject 0.3.0 → 0.5.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 +201 -55
- data/lib/active_project/adapters/base.rb +154 -14
- data/lib/active_project/adapters/basecamp/comments.rb +34 -0
- data/lib/active_project/adapters/basecamp/connection.rb +6 -24
- data/lib/active_project/adapters/basecamp/issues.rb +6 -5
- data/lib/active_project/adapters/basecamp/webhooks.rb +7 -8
- data/lib/active_project/adapters/fizzy/columns.rb +116 -0
- data/lib/active_project/adapters/fizzy/comments.rb +129 -0
- data/lib/active_project/adapters/fizzy/connection.rb +41 -0
- data/lib/active_project/adapters/fizzy/issues.rb +221 -0
- data/lib/active_project/adapters/fizzy/projects.rb +105 -0
- data/lib/active_project/adapters/fizzy_adapter.rb +151 -0
- data/lib/active_project/adapters/github_project/comments.rb +91 -0
- data/lib/active_project/adapters/github_project/connection.rb +58 -0
- data/lib/active_project/adapters/github_project/helpers.rb +100 -0
- data/lib/active_project/adapters/github_project/issues.rb +287 -0
- data/lib/active_project/adapters/github_project/projects.rb +139 -0
- data/lib/active_project/adapters/github_project/webhooks.rb +168 -0
- data/lib/active_project/adapters/github_project.rb +8 -0
- data/lib/active_project/adapters/github_project_adapter.rb +65 -0
- data/lib/active_project/adapters/github_repo/connection.rb +62 -0
- data/lib/active_project/adapters/github_repo/issues.rb +242 -0
- data/lib/active_project/adapters/github_repo/projects.rb +116 -0
- data/lib/active_project/adapters/github_repo/webhooks.rb +354 -0
- data/lib/active_project/adapters/github_repo_adapter.rb +134 -0
- data/lib/active_project/adapters/jira/attribute_normalizer.rb +16 -0
- data/lib/active_project/adapters/jira/comments.rb +41 -0
- data/lib/active_project/adapters/jira/connection.rb +15 -15
- data/lib/active_project/adapters/jira/issues.rb +21 -7
- data/lib/active_project/adapters/jira/projects.rb +3 -1
- data/lib/active_project/adapters/jira/transitions.rb +2 -1
- data/lib/active_project/adapters/jira/webhooks.rb +5 -7
- data/lib/active_project/adapters/jira_adapter.rb +23 -3
- data/lib/active_project/adapters/trello/comments.rb +34 -0
- data/lib/active_project/adapters/trello/connection.rb +12 -9
- data/lib/active_project/adapters/trello/issues.rb +7 -5
- data/lib/active_project/adapters/trello/webhooks.rb +5 -7
- data/lib/active_project/adapters/trello_adapter.rb +5 -3
- data/lib/active_project/association_proxy.rb +3 -2
- data/lib/active_project/configuration.rb +6 -3
- data/lib/active_project/configurations/base_adapter_configuration.rb +102 -0
- data/lib/active_project/configurations/basecamp_configuration.rb +42 -0
- data/lib/active_project/configurations/fizzy_configuration.rb +47 -0
- data/lib/active_project/configurations/github_configuration.rb +57 -0
- data/lib/active_project/configurations/jira_configuration.rb +54 -0
- data/lib/active_project/configurations/trello_configuration.rb +24 -2
- data/lib/active_project/connections/base.rb +35 -0
- data/lib/active_project/connections/graph_ql.rb +83 -0
- data/lib/active_project/connections/http_client.rb +79 -0
- data/lib/active_project/connections/pagination.rb +44 -0
- data/lib/active_project/connections/rest.rb +33 -0
- data/lib/active_project/error_mapper.rb +38 -0
- data/lib/active_project/errors.rb +13 -0
- data/lib/active_project/railtie.rb +1 -3
- data/lib/active_project/resources/base_resource.rb +13 -14
- data/lib/active_project/resources/comment.rb +46 -2
- data/lib/active_project/resources/issue.rb +106 -18
- data/lib/active_project/resources/persistable_resource.rb +47 -0
- data/lib/active_project/resources/project.rb +1 -1
- data/lib/active_project/status_mapper.rb +145 -0
- data/lib/active_project/version.rb +1 -1
- data/lib/active_project/webhook_event.rb +34 -12
- data/lib/activeproject.rb +9 -6
- metadata +74 -16
- data/lib/active_project/adapters/http_client.rb +0 -71
- data/lib/active_project/adapters/pagination.rb +0 -68
|
@@ -25,10 +25,10 @@ module ActiveProject
|
|
|
25
25
|
loop do
|
|
26
26
|
response = @connection.get(path, query)
|
|
27
27
|
todos_data = begin
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
JSON.parse(response.body)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
32
|
break if todos_data.empty?
|
|
33
33
|
|
|
34
34
|
todos_data.each do |todo_data|
|
|
@@ -145,7 +145,8 @@ module ActiveProject
|
|
|
145
145
|
def delete_issue(todo_id, context = {})
|
|
146
146
|
project_id = context[:project_id]
|
|
147
147
|
unless project_id
|
|
148
|
-
raise ArgumentError,
|
|
148
|
+
raise ArgumentError,
|
|
149
|
+
"Missing required context: :project_id must be provided for BasecampAdapter#delete_issue"
|
|
149
150
|
end
|
|
150
151
|
|
|
151
152
|
path = "buckets/#{project_id}/todos/#{todo_id}.json"
|
|
@@ -38,7 +38,8 @@ module ActiveProject
|
|
|
38
38
|
when /todo_created$/
|
|
39
39
|
event_type = :issue_created
|
|
40
40
|
object_kind = :issue
|
|
41
|
-
when /todo_assignment_changed$/, /todo_completion_changed$/, /todo_content_updated$/,
|
|
41
|
+
when /todo_assignment_changed$/, /todo_completion_changed$/, /todo_content_updated$/,
|
|
42
|
+
/todo_description_changed$/, /todo_due_on_changed$/
|
|
42
43
|
event_type = :issue_updated
|
|
43
44
|
object_kind = :issue
|
|
44
45
|
when /comment_created$/
|
|
@@ -52,16 +53,14 @@ module ActiveProject
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
WebhookEvent.new(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
object_key: object_key,
|
|
56
|
+
type: event_type,
|
|
57
|
+
resource_type: object_kind,
|
|
58
|
+
resource_id: event_object_id,
|
|
59
59
|
project_id: project_id,
|
|
60
60
|
actor: map_user_data(creator),
|
|
61
61
|
timestamp: timestamp,
|
|
62
|
-
|
|
63
|
-
changes: changes,
|
|
64
|
-
object_data: object_data,
|
|
62
|
+
source: webhook_type,
|
|
63
|
+
data: (object_data || {}).merge(changes: changes, object_key: object_key),
|
|
65
64
|
raw_data: payload
|
|
66
65
|
)
|
|
67
66
|
rescue JSON::ParserError
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveProject
|
|
4
|
+
module Adapters
|
|
5
|
+
module Fizzy
|
|
6
|
+
module Columns
|
|
7
|
+
# Lists columns (workflow stages) on a board.
|
|
8
|
+
# @param board_id [String] The board ULID.
|
|
9
|
+
# @return [Array<Hash>] An array of column hashes.
|
|
10
|
+
def list_lists(board_id)
|
|
11
|
+
path = "boards/#{board_id}/columns.json"
|
|
12
|
+
response = @connection.get(path)
|
|
13
|
+
columns_data = parse_response(response)
|
|
14
|
+
|
|
15
|
+
columns_data.map do |column_data|
|
|
16
|
+
map_column_data(column_data, board_id)
|
|
17
|
+
end
|
|
18
|
+
rescue Faraday::Error => e
|
|
19
|
+
handle_faraday_error(e)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Creates a new column on a board.
|
|
23
|
+
# @param board_id [String] The board ULID.
|
|
24
|
+
# @param attributes [Hash] Column attributes.
|
|
25
|
+
# - :name [String] Required. The column name.
|
|
26
|
+
# - :color [String] Optional. CSS var color (e.g., "var(--color-card-4)").
|
|
27
|
+
# @return [Hash] The created column hash.
|
|
28
|
+
def create_list(board_id, attributes)
|
|
29
|
+
unless attributes[:name] && !attributes[:name].empty?
|
|
30
|
+
raise ArgumentError, "Missing required attribute for Fizzy column creation: :name"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
path = "boards/#{board_id}/columns.json"
|
|
34
|
+
payload = {
|
|
35
|
+
column: {
|
|
36
|
+
name: attributes[:name],
|
|
37
|
+
color: attributes[:color]
|
|
38
|
+
}.compact
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
response = @connection.post(path) do |req|
|
|
42
|
+
req.body = payload.to_json
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Extract column ID from Location header and fetch it
|
|
46
|
+
location = response.headers["Location"]
|
|
47
|
+
if location
|
|
48
|
+
column_id = location.match(%r{/columns/([^/.]+)})[1]
|
|
49
|
+
find_list(board_id, column_id)
|
|
50
|
+
else
|
|
51
|
+
# Fallback: parse response body if available
|
|
52
|
+
column_data = parse_response(response)
|
|
53
|
+
map_column_data(column_data, board_id)
|
|
54
|
+
end
|
|
55
|
+
rescue Faraday::Error => e
|
|
56
|
+
handle_faraday_error(e)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Finds a specific column.
|
|
60
|
+
# @param board_id [String] The board ULID.
|
|
61
|
+
# @param column_id [String] The column ULID.
|
|
62
|
+
# @return [Hash] The column hash.
|
|
63
|
+
def find_list(board_id, column_id)
|
|
64
|
+
path = "boards/#{board_id}/columns/#{column_id}.json"
|
|
65
|
+
column_data = make_request(:get, path)
|
|
66
|
+
return nil unless column_data
|
|
67
|
+
|
|
68
|
+
map_column_data(column_data, board_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Updates a column.
|
|
72
|
+
# @param board_id [String] The board ULID.
|
|
73
|
+
# @param column_id [String] The column ULID.
|
|
74
|
+
# @param attributes [Hash] Attributes to update.
|
|
75
|
+
# - :name [String] The column name.
|
|
76
|
+
# - :color [String] CSS var color.
|
|
77
|
+
# @return [Hash] The updated column hash.
|
|
78
|
+
def update_list(board_id, column_id, attributes)
|
|
79
|
+
path = "boards/#{board_id}/columns/#{column_id}.json"
|
|
80
|
+
payload = {
|
|
81
|
+
column: {
|
|
82
|
+
name: attributes[:name],
|
|
83
|
+
color: attributes[:color]
|
|
84
|
+
}.compact
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
make_request(:put, path, payload.to_json)
|
|
88
|
+
find_list(board_id, column_id)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Deletes a column.
|
|
92
|
+
# @param board_id [String] The board ULID.
|
|
93
|
+
# @param column_id [String] The column ULID.
|
|
94
|
+
# @return [Boolean] True if successfully deleted.
|
|
95
|
+
def delete_list(board_id, column_id)
|
|
96
|
+
path = "boards/#{board_id}/columns/#{column_id}.json"
|
|
97
|
+
make_request(:delete, path)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def map_column_data(column_data, board_id)
|
|
104
|
+
{
|
|
105
|
+
id: column_data["id"],
|
|
106
|
+
name: column_data["name"],
|
|
107
|
+
color: column_data["color"],
|
|
108
|
+
board_id: board_id,
|
|
109
|
+
created_at: column_data["created_at"] ? Time.parse(column_data["created_at"]) : nil,
|
|
110
|
+
raw_data: column_data
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveProject
|
|
4
|
+
module Adapters
|
|
5
|
+
module Fizzy
|
|
6
|
+
module Comments
|
|
7
|
+
# Lists comments on a card.
|
|
8
|
+
# @param card_number [Integer, String] The card number.
|
|
9
|
+
# @return [Array<ActiveProject::Resources::Comment>] An array of comment resources.
|
|
10
|
+
def list_comments(card_number)
|
|
11
|
+
all_comments = []
|
|
12
|
+
path = "cards/#{card_number}/comments.json"
|
|
13
|
+
|
|
14
|
+
loop do
|
|
15
|
+
response = @connection.get(path)
|
|
16
|
+
comments_data = parse_response(response)
|
|
17
|
+
break if comments_data.empty?
|
|
18
|
+
|
|
19
|
+
comments_data.each do |comment_data|
|
|
20
|
+
all_comments << map_comment_data(comment_data, card_number)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
next_url = parse_next_link(response.headers["Link"])
|
|
24
|
+
break unless next_url
|
|
25
|
+
|
|
26
|
+
path = extract_path_from_url(next_url)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
all_comments
|
|
30
|
+
rescue Faraday::Error => e
|
|
31
|
+
handle_faraday_error(e)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Adds a comment to a card.
|
|
35
|
+
# @param card_number [Integer, String] The card number.
|
|
36
|
+
# @param comment_body [String] The comment body (supports HTML rich text).
|
|
37
|
+
# @param context [Hash] Optional context (not required for Fizzy).
|
|
38
|
+
# @return [ActiveProject::Resources::Comment] The created comment resource.
|
|
39
|
+
def add_comment(card_number, comment_body, context = {})
|
|
40
|
+
path = "cards/#{card_number}/comments.json"
|
|
41
|
+
payload = {
|
|
42
|
+
comment: {
|
|
43
|
+
body: comment_body
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
response = @connection.post(path) do |req|
|
|
48
|
+
req.body = payload.to_json
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extract comment ID from Location header and fetch it
|
|
52
|
+
location = response.headers["Location"]
|
|
53
|
+
if location
|
|
54
|
+
comment_id = location.match(%r{/comments/([^/.]+)})[1]
|
|
55
|
+
find_comment(card_number, comment_id)
|
|
56
|
+
else
|
|
57
|
+
# Fallback: parse response body if available
|
|
58
|
+
comment_data = parse_response(response)
|
|
59
|
+
map_comment_data(comment_data, card_number)
|
|
60
|
+
end
|
|
61
|
+
rescue Faraday::Error => e
|
|
62
|
+
handle_faraday_error(e)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Finds a specific comment.
|
|
66
|
+
# @param card_number [Integer, String] The card number.
|
|
67
|
+
# @param comment_id [String] The comment ULID.
|
|
68
|
+
# @return [ActiveProject::Resources::Comment] The comment resource.
|
|
69
|
+
def find_comment(card_number, comment_id)
|
|
70
|
+
path = "cards/#{card_number}/comments/#{comment_id}.json"
|
|
71
|
+
comment_data = make_request(:get, path)
|
|
72
|
+
return nil unless comment_data
|
|
73
|
+
|
|
74
|
+
map_comment_data(comment_data, card_number)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Updates a comment.
|
|
78
|
+
# @param card_number [Integer, String] The card number.
|
|
79
|
+
# @param comment_id [String] The comment ULID.
|
|
80
|
+
# @param comment_body [String] The new comment body.
|
|
81
|
+
# @return [ActiveProject::Resources::Comment] The updated comment resource.
|
|
82
|
+
def update_comment(card_number, comment_id, comment_body)
|
|
83
|
+
path = "cards/#{card_number}/comments/#{comment_id}.json"
|
|
84
|
+
payload = {
|
|
85
|
+
comment: {
|
|
86
|
+
body: comment_body
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
make_request(:put, path, payload.to_json)
|
|
91
|
+
find_comment(card_number, comment_id)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Deletes a comment.
|
|
95
|
+
# @param card_number [Integer, String] The card number.
|
|
96
|
+
# @param comment_id [String] The comment ULID.
|
|
97
|
+
# @return [Boolean] True if successfully deleted.
|
|
98
|
+
def delete_comment(card_number, comment_id)
|
|
99
|
+
path = "cards/#{card_number}/comments/#{comment_id}.json"
|
|
100
|
+
make_request(:delete, path)
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def map_comment_data(comment_data, card_number)
|
|
107
|
+
# Fizzy returns body as { plain_text: "...", html: "..." }
|
|
108
|
+
body = if comment_data["body"].is_a?(Hash)
|
|
109
|
+
comment_data["body"]["plain_text"] || comment_data["body"]["html"]
|
|
110
|
+
else
|
|
111
|
+
comment_data["body"]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Resources::Comment.new(
|
|
115
|
+
self,
|
|
116
|
+
id: comment_data["id"],
|
|
117
|
+
body: body,
|
|
118
|
+
author: map_user_data(comment_data["creator"]),
|
|
119
|
+
created_at: comment_data["created_at"] ? Time.parse(comment_data["created_at"]) : nil,
|
|
120
|
+
updated_at: comment_data["updated_at"] ? Time.parse(comment_data["updated_at"]) : nil,
|
|
121
|
+
issue_id: card_number.to_s,
|
|
122
|
+
adapter_source: :fizzy,
|
|
123
|
+
raw_data: comment_data
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveProject
|
|
4
|
+
module Adapters
|
|
5
|
+
module Fizzy
|
|
6
|
+
module Connection
|
|
7
|
+
include Connections::Rest
|
|
8
|
+
|
|
9
|
+
DEFAULT_BASE_URL = "https://app.fizzy.do"
|
|
10
|
+
|
|
11
|
+
# Initializes the Fizzy Adapter.
|
|
12
|
+
# @param config [Configurations::FizzyConfiguration] The configuration object for Fizzy.
|
|
13
|
+
# @raise [ArgumentError] if required configuration options are missing.
|
|
14
|
+
def initialize(config:)
|
|
15
|
+
unless config.is_a?(ActiveProject::Configurations::FizzyConfiguration)
|
|
16
|
+
raise ArgumentError, "FizzyAdapter requires a FizzyConfiguration object"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
super(config: config)
|
|
20
|
+
|
|
21
|
+
account_slug = @config.options.fetch(:account_slug)
|
|
22
|
+
access_token = @config.options.fetch(:access_token)
|
|
23
|
+
base_url = @config.options[:base_url] || DEFAULT_BASE_URL
|
|
24
|
+
|
|
25
|
+
# Fizzy uses account_slug in URL path: /:account_slug/boards, etc.
|
|
26
|
+
@base_url = "#{base_url}/#{account_slug}/"
|
|
27
|
+
|
|
28
|
+
init_rest(
|
|
29
|
+
base_url: @base_url,
|
|
30
|
+
auth_middleware: ->(conn) { conn.request :authorization, "Bearer", access_token },
|
|
31
|
+
extra_headers: { "Accept" => "application/json", "Content-Type" => "application/json" }
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return if account_slug && !account_slug.to_s.empty? && access_token && !access_token.empty?
|
|
35
|
+
|
|
36
|
+
raise ArgumentError, "FizzyAdapter configuration requires :account_slug and :access_token"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveProject
|
|
4
|
+
module Adapters
|
|
5
|
+
module Fizzy
|
|
6
|
+
module Issues
|
|
7
|
+
# Lists cards within a specific board.
|
|
8
|
+
# @param board_id [String] The ULID of the Fizzy board.
|
|
9
|
+
# @param options [Hash] Optional options for filtering.
|
|
10
|
+
# - :tag_ids [Array<String>] Filter by tag IDs
|
|
11
|
+
# - :assignee_ids [Array<String>] Filter by assignee user IDs
|
|
12
|
+
# - :indexed_by [String] Filter: all, closed, not_now, stalled, postponing_soon, golden
|
|
13
|
+
# - :sorted_by [String] Sort: latest, newest, oldest
|
|
14
|
+
# @return [Array<ActiveProject::Resources::Issue>] An array of issue resources.
|
|
15
|
+
def list_issues(board_id, options = {})
|
|
16
|
+
all_cards = []
|
|
17
|
+
query = { "board_ids[]" => board_id }
|
|
18
|
+
|
|
19
|
+
# Add optional filters
|
|
20
|
+
options[:tag_ids]&.each { |id| query["tag_ids[]"] = id }
|
|
21
|
+
options[:assignee_ids]&.each { |id| query["assignee_ids[]"] = id }
|
|
22
|
+
query[:indexed_by] = options[:indexed_by] if options[:indexed_by]
|
|
23
|
+
query[:sorted_by] = options[:sorted_by] if options[:sorted_by]
|
|
24
|
+
|
|
25
|
+
path = "cards.json"
|
|
26
|
+
|
|
27
|
+
loop do
|
|
28
|
+
response = @connection.get(path, query)
|
|
29
|
+
cards_data = parse_response(response)
|
|
30
|
+
break if cards_data.empty?
|
|
31
|
+
|
|
32
|
+
cards_data.each do |card_data|
|
|
33
|
+
all_cards << map_card_data(card_data, board_id)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
next_url = parse_next_link(response.headers["Link"])
|
|
37
|
+
break unless next_url
|
|
38
|
+
|
|
39
|
+
path = extract_path_from_url(next_url)
|
|
40
|
+
query = {} # Query params are in the URL now
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
all_cards
|
|
44
|
+
rescue Faraday::Error => e
|
|
45
|
+
handle_faraday_error(e)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Finds a specific card by its number.
|
|
49
|
+
# @param card_number [Integer, String] The card number (sequential per account).
|
|
50
|
+
# @param context [Hash] Optional context (not required for Fizzy).
|
|
51
|
+
# @return [ActiveProject::Resources::Issue] The issue resource.
|
|
52
|
+
def find_issue(card_number, context = {})
|
|
53
|
+
path = "cards/#{card_number}.json"
|
|
54
|
+
card_data = make_request(:get, path)
|
|
55
|
+
return nil unless card_data
|
|
56
|
+
|
|
57
|
+
board_id = card_data.dig("board", "id")
|
|
58
|
+
map_card_data(card_data, board_id)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Creates a new card in a board.
|
|
62
|
+
# @param board_id [String] The ULID of the Fizzy board.
|
|
63
|
+
# @param attributes [Hash] Card attributes.
|
|
64
|
+
# - :title [String] Required. The card title.
|
|
65
|
+
# - :description [String] Optional. Rich text description (HTML).
|
|
66
|
+
# - :status [String] Optional. Initial status: published (default), drafted.
|
|
67
|
+
# - :tag_ids [Array<String>] Optional. Tag IDs to apply.
|
|
68
|
+
# @return [ActiveProject::Resources::Issue] The created issue resource.
|
|
69
|
+
def create_issue(board_id, attributes)
|
|
70
|
+
title = attributes[:title]
|
|
71
|
+
unless title && !title.empty?
|
|
72
|
+
raise ArgumentError, "Missing required attribute for Fizzy card creation: :title"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
path = "boards/#{board_id}/cards.json"
|
|
76
|
+
payload = {
|
|
77
|
+
card: {
|
|
78
|
+
title: title,
|
|
79
|
+
description: attributes[:description],
|
|
80
|
+
status: attributes[:status] || "published",
|
|
81
|
+
tag_ids: attributes[:tag_ids]
|
|
82
|
+
}.compact
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Fizzy returns 201 Created with Location header
|
|
86
|
+
response = @connection.post(path) do |req|
|
|
87
|
+
req.body = payload.to_json
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Extract card number from Location header and fetch it
|
|
91
|
+
location = response.headers["Location"]
|
|
92
|
+
if location
|
|
93
|
+
card_number = location.match(%r{/cards/(\d+)})[1]
|
|
94
|
+
find_issue(card_number)
|
|
95
|
+
else
|
|
96
|
+
# Fallback: parse response body if available
|
|
97
|
+
card_data = parse_response(response)
|
|
98
|
+
map_card_data(card_data, board_id)
|
|
99
|
+
end
|
|
100
|
+
rescue Faraday::Error => e
|
|
101
|
+
handle_faraday_error(e)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Updates an existing card.
|
|
105
|
+
# @param card_number [Integer, String] The card number.
|
|
106
|
+
# @param attributes [Hash] Attributes to update.
|
|
107
|
+
# - :title [String] The card title.
|
|
108
|
+
# - :description [String] Rich text description.
|
|
109
|
+
# - :status [Symbol] Status change: :open, :closed, :on_hold.
|
|
110
|
+
# - :tag_ids [Array<String>] Tag IDs to apply.
|
|
111
|
+
# @param context [Hash] Optional context (not required for Fizzy).
|
|
112
|
+
# @return [ActiveProject::Resources::Issue] The updated issue resource.
|
|
113
|
+
def update_issue(card_number, attributes, context = {})
|
|
114
|
+
put_payload = {}
|
|
115
|
+
put_payload[:title] = attributes[:title] if attributes.key?(:title)
|
|
116
|
+
put_payload[:description] = attributes[:description] if attributes.key?(:description)
|
|
117
|
+
put_payload[:tag_ids] = attributes[:tag_ids] if attributes.key?(:tag_ids)
|
|
118
|
+
|
|
119
|
+
status_change_required = attributes.key?(:status)
|
|
120
|
+
target_status = attributes[:status] if status_change_required
|
|
121
|
+
|
|
122
|
+
unless !put_payload.empty? || status_change_required
|
|
123
|
+
raise ArgumentError, "No attributes provided to update for FizzyAdapter#update_issue"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Update basic fields via PUT
|
|
127
|
+
unless put_payload.empty?
|
|
128
|
+
put_path = "cards/#{card_number}.json"
|
|
129
|
+
make_request(:put, put_path, { card: put_payload }.to_json)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Handle status changes via separate endpoints
|
|
133
|
+
if status_change_required
|
|
134
|
+
handle_status_change(card_number, target_status)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
find_issue(card_number, context)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Deletes a card.
|
|
141
|
+
# @param card_number [Integer, String] The card number to delete.
|
|
142
|
+
# @param context [Hash] Optional context (not required for Fizzy).
|
|
143
|
+
# @return [Boolean] True if successfully deleted.
|
|
144
|
+
def delete_issue(card_number, context = {})
|
|
145
|
+
path = "cards/#{card_number}.json"
|
|
146
|
+
make_request(:delete, path)
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def handle_status_change(card_number, target_status)
|
|
153
|
+
case target_status
|
|
154
|
+
when :closed
|
|
155
|
+
# POST /cards/:num/closure
|
|
156
|
+
make_request(:post, "cards/#{card_number}/closure.json")
|
|
157
|
+
when :open
|
|
158
|
+
# DELETE /cards/:num/closure (reopen)
|
|
159
|
+
begin
|
|
160
|
+
make_request(:delete, "cards/#{card_number}/closure.json")
|
|
161
|
+
rescue NotFoundError
|
|
162
|
+
# Card wasn't closed, ignore
|
|
163
|
+
end
|
|
164
|
+
when :on_hold
|
|
165
|
+
# POST /cards/:num/not_now
|
|
166
|
+
make_request(:post, "cards/#{card_number}/not_now.json")
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def map_card_data(card_data, board_id)
|
|
171
|
+
# Determine status based on card state and column
|
|
172
|
+
status = determine_card_status(card_data, board_id)
|
|
173
|
+
|
|
174
|
+
# Map creator
|
|
175
|
+
creator = map_user_data(card_data["creator"])
|
|
176
|
+
|
|
177
|
+
# Map assignees if present (Fizzy cards can have multiple assignees)
|
|
178
|
+
assignees = (card_data["assignees"] || []).map { |a| map_user_data(a) }.compact
|
|
179
|
+
|
|
180
|
+
Resources::Issue.new(
|
|
181
|
+
self,
|
|
182
|
+
id: card_data["id"],
|
|
183
|
+
key: card_data["number"]&.to_s,
|
|
184
|
+
title: card_data["title"],
|
|
185
|
+
description: card_data["description"],
|
|
186
|
+
status: status,
|
|
187
|
+
assignees: assignees,
|
|
188
|
+
reporter: creator,
|
|
189
|
+
project_id: board_id || card_data.dig("board", "id"),
|
|
190
|
+
created_at: card_data["created_at"] ? Time.parse(card_data["created_at"]) : nil,
|
|
191
|
+
updated_at: card_data["last_active_at"] ? Time.parse(card_data["last_active_at"]) : nil,
|
|
192
|
+
due_on: nil, # Fizzy doesn't have due dates on cards
|
|
193
|
+
priority: nil,
|
|
194
|
+
adapter_source: :fizzy,
|
|
195
|
+
raw_data: card_data
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def determine_card_status(card_data, board_id)
|
|
200
|
+
# Check for closed status
|
|
201
|
+
return :closed if card_data["closed"] == true
|
|
202
|
+
|
|
203
|
+
# Check for not_now status
|
|
204
|
+
return :on_hold if card_data["not_now"].present?
|
|
205
|
+
|
|
206
|
+
# Check column-based status mapping from config
|
|
207
|
+
column_name = card_data.dig("column", "name")
|
|
208
|
+
if column_name && board_id
|
|
209
|
+
board_mappings = @config.status_mappings[board_id]
|
|
210
|
+
if board_mappings && board_mappings.key?(column_name)
|
|
211
|
+
return board_mappings[column_name]
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Default: published cards are open
|
|
216
|
+
card_data["status"] == "drafted" ? :open : :open
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveProject
|
|
4
|
+
module Adapters
|
|
5
|
+
module Fizzy
|
|
6
|
+
module Projects
|
|
7
|
+
# Lists boards accessible by the configured credentials.
|
|
8
|
+
# Handles pagination automatically using the Link header.
|
|
9
|
+
# @return [Array<ActiveProject::Resources::Project>] An array of project resources.
|
|
10
|
+
def list_projects
|
|
11
|
+
all_boards = []
|
|
12
|
+
path = "boards.json"
|
|
13
|
+
|
|
14
|
+
loop do
|
|
15
|
+
response = @connection.get(path)
|
|
16
|
+
boards_data = parse_response(response)
|
|
17
|
+
break if boards_data.empty?
|
|
18
|
+
|
|
19
|
+
boards_data.each do |board_data|
|
|
20
|
+
all_boards << map_board_data(board_data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
next_url = parse_next_link(response.headers["Link"])
|
|
24
|
+
break unless next_url
|
|
25
|
+
|
|
26
|
+
path = extract_path_from_url(next_url)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
all_boards
|
|
30
|
+
rescue Faraday::Error => e
|
|
31
|
+
handle_faraday_error(e)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Finds a specific board by its ID.
|
|
35
|
+
# @param board_id [String] The ULID of the Fizzy board.
|
|
36
|
+
# @return [ActiveProject::Resources::Project] The project resource.
|
|
37
|
+
def find_project(board_id)
|
|
38
|
+
path = "boards/#{board_id}.json"
|
|
39
|
+
board_data = make_request(:get, path)
|
|
40
|
+
return nil unless board_data
|
|
41
|
+
|
|
42
|
+
map_board_data(board_data)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Creates a new board in Fizzy.
|
|
46
|
+
# @param attributes [Hash] Board attributes. Required: :name. Optional: :all_access, :auto_postpone_period.
|
|
47
|
+
# @return [ActiveProject::Resources::Project] The created project resource.
|
|
48
|
+
def create_project(attributes)
|
|
49
|
+
unless attributes[:name] && !attributes[:name].empty?
|
|
50
|
+
raise ArgumentError, "Missing required attribute for Fizzy board creation: :name"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
path = "boards.json"
|
|
54
|
+
payload = {
|
|
55
|
+
board: {
|
|
56
|
+
name: attributes[:name],
|
|
57
|
+
all_access: attributes.fetch(:all_access, true),
|
|
58
|
+
auto_postpone_period: attributes[:auto_postpone_period]
|
|
59
|
+
}.compact
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Fizzy returns 201 Created with Location header, need to fetch the board
|
|
63
|
+
response = @connection.post(path) do |req|
|
|
64
|
+
req.body = payload.to_json
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Extract board ID from Location header and fetch it
|
|
68
|
+
location = response.headers["Location"]
|
|
69
|
+
if location
|
|
70
|
+
board_id = location.match(%r{/boards/([^/.]+)})[1]
|
|
71
|
+
find_project(board_id)
|
|
72
|
+
else
|
|
73
|
+
# Fallback: parse response body if available
|
|
74
|
+
board_data = parse_response(response)
|
|
75
|
+
map_board_data(board_data)
|
|
76
|
+
end
|
|
77
|
+
rescue Faraday::Error => e
|
|
78
|
+
handle_faraday_error(e)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Deletes a board in Fizzy.
|
|
82
|
+
# @param board_id [String] The ULID of the board to delete.
|
|
83
|
+
# @return [Boolean] true if deletion was successful (API returns 204).
|
|
84
|
+
def delete_project(board_id)
|
|
85
|
+
path = "boards/#{board_id}.json"
|
|
86
|
+
make_request(:delete, path)
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def map_board_data(board_data)
|
|
93
|
+
Resources::Project.new(
|
|
94
|
+
self,
|
|
95
|
+
id: board_data["id"],
|
|
96
|
+
key: nil,
|
|
97
|
+
name: board_data["name"],
|
|
98
|
+
adapter_source: :fizzy,
|
|
99
|
+
raw_data: board_data
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|