1use crate::api::{SingleResponse, api_delete, api_post, api_put};
4use crate::{
5 api::{ApiClient, ApiError},
6 models::{
7 MemberPermission, Project, ProjectCreateRequest, ProjectListParams, ProjectMember,
8 ProjectMembers, ProjectSort, ProjectStatus, ProjectUpdateRequest,
9 },
10};
11use futures_util::{StreamExt, pin_mut};
12use serde::Serialize;
13
14pub async fn list_projects(
20 client: &ApiClient,
21 mut params: ProjectListParams,
22) -> Result<Vec<Project>, ApiError> {
23 ensure_default_fields(&mut params);
24
25 let query = params.to_query();
26 let max_items = params.limit;
27 let stream = client.paginate_with_limit::<Project>("/projects", query, max_items);
28 pin_mut!(stream);
29
30 let mut projects = Vec::new();
31 while let Some(page) = stream.next().await {
32 let mut page = page?;
33 projects.append(&mut page);
34 }
35
36 if !params.filters.is_empty() {
37 projects.retain(|project| project.matches(¶ms.filters));
38 }
39
40 if let Some(sort) = params.sort {
41 sort_projects(&mut projects, sort);
42 }
43
44 Ok(projects)
45}
46
47pub async fn get_project(
53 client: &ApiClient,
54 gid: &str,
55 fields: Vec<String>,
56) -> Result<Project, ApiError> {
57 let mut query = Vec::new();
58 if !fields.is_empty() {
59 query.push(("opt_fields".into(), fields.join(",")));
60 }
61
62 let response: SingleResponse<Project> = client
63 .get_json_with_pairs(&format!("/projects/{gid}"), query)
64 .await?;
65 Ok(response.data)
66}
67
68pub async fn create_project(
74 client: &ApiClient,
75 request: ProjectCreateRequest,
76) -> Result<Project, ApiError> {
77 api_post(client, "/projects", &request).await
78}
79
80pub async fn update_project(
86 client: &ApiClient,
87 gid: &str,
88 request: ProjectUpdateRequest,
89) -> Result<Project, ApiError> {
90 api_put(client, &format!("/projects/{gid}"), &request).await
91}
92
93pub async fn delete_project(client: &ApiClient, gid: &str) -> Result<(), ApiError> {
99 api_delete(client, &format!("/projects/{gid}")).await
100}
101
102pub async fn list_members(client: &ApiClient, gid: &str) -> Result<ProjectMembers, ApiError> {
108 let response: SingleResponse<Vec<ProjectMember>> = client
109 .get_json_with_pairs(&format!("/projects/{gid}/members"), vec![])
110 .await?;
111 Ok(ProjectMembers {
112 project_gid: gid.to_string(),
113 members: response.data,
114 })
115}
116
117pub async fn list_statuses(
123 client: &ApiClient,
124 gid: &str,
125 limit: Option<usize>,
126) -> Result<Vec<ProjectStatus>, ApiError> {
127 let response: SingleResponse<Vec<ProjectStatus>> = client
128 .get_json_with_pairs(&format!("/projects/{gid}/project_statuses"), vec![])
129 .await?;
130 let mut statuses = response.data;
131 if let Some(limit) = limit
132 && statuses.len() > limit
133 {
134 statuses.truncate(limit);
135 }
136 Ok(statuses)
137}
138
139pub async fn add_members(
145 client: &ApiClient,
146 gid: &str,
147 members: Vec<String>,
148 role: Option<MemberPermission>,
149) -> Result<(), ApiError> {
150 let payload = ModifyMembersRequest {
151 data: ModifyMembersData { members, role },
152 };
153 client
154 .post_void(&format!("/projects/{gid}/addMembers"), &payload)
155 .await
156}
157
158pub async fn remove_members(
164 client: &ApiClient,
165 gid: &str,
166 members: Vec<String>,
167) -> Result<(), ApiError> {
168 let payload = ModifyMembersRequest {
169 data: ModifyMembersData {
170 members,
171 role: None,
172 },
173 };
174 client
175 .post_void(&format!("/projects/{gid}/removeMembers"), &payload)
176 .await
177}
178
179pub async fn update_member(
185 client: &ApiClient,
186 membership_gid: &str,
187 role: MemberPermission,
188) -> Result<ProjectMember, ApiError> {
189 let payload = UpdateMemberRequest {
190 data: MemberRoleUpdate { role },
191 };
192 let response: SingleResponse<ProjectMember> = client
193 .put_json(&format!("/project_memberships/{membership_gid}"), &payload)
194 .await?;
195 Ok(response.data)
196}
197
198fn ensure_default_fields(params: &mut ProjectListParams) {
199 let defaults = [
200 "gid",
201 "name",
202 "archived",
203 "color",
204 "created_at",
205 "modified_at",
206 "workspace.name",
207 "workspace.gid",
208 "team.name",
209 "team.gid",
210 "owner.name",
211 "owner.gid",
212 "due_on",
213 "start_on",
214 ];
215 for field in defaults {
216 params.fields.insert(field.to_string());
217 }
218}
219
220fn sort_projects(projects: &mut [Project], sort: ProjectSort) {
221 match sort {
222 ProjectSort::Name => projects.sort_by(|a, b| {
223 a.name
224 .to_ascii_lowercase()
225 .cmp(&b.name.to_ascii_lowercase())
226 }),
227 ProjectSort::CreatedAt => projects.sort_by(|a, b| a.created_at.cmp(&b.created_at)),
228 ProjectSort::ModifiedAt => projects.sort_by(|a, b| a.modified_at.cmp(&b.modified_at)),
229 }
230}
231
232#[derive(Debug, Serialize)]
233struct ModifyMembersRequest {
234 data: ModifyMembersData,
235}
236
237#[derive(Debug, Serialize)]
238struct ModifyMembersData {
239 members: Vec<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 role: Option<MemberPermission>,
242}
243
244#[derive(Debug, Serialize)]
245struct UpdateMemberRequest {
246 data: MemberRoleUpdate,
247}
248
249#[derive(Debug, Serialize)]
250struct MemberRoleUpdate {
251 role: MemberPermission,
252}