feat(issues,space): boards create DirKind::Object node in dir tree
ZhenYi committed
Modified lib/issues/src/services/boards.rs lib/issues/src/services/boards.rs +106 −39 View lib/issues/src/services/boards.rs
@@ -7,7 +7,7 @@
77use models::issue::milestone;
88use sea_orm::{
99 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
10 QuerySelect, Set, TransactionTrait,
10 QuerySelect, Set, TransactionTrait, Unchanged,
1111};
1212use serde::{Deserialize, Serialize};
1313use time::OffsetDateTime;
@@ -41,8 +41,9 @@
4141
4242#[derive(Debug, Clone, Deserialize, ToSchema)]
4343pub struct CreateBoardCmd {
44 pub dir_id: Uuid,
4544 pub name: String,
45 #[serde(default, alias = "dir_id")]
46 pub parent_dir_id: Option<Uuid>,
4647 #[serde(default)]
4748 pub icon: Option<String>,
4849 #[serde(default)]
@@ -150,38 +151,75 @@
150151 return Err(IssueError::BadRequest("board name is empty or too long".to_string()));
151152 }
152153
153 let dir = dirs::Entity::find_by_id(cmd.dir_id)
154 .filter(dirs::Column::SpaceId.eq(Some(space.id)))
155 .one(self.db().read())
156 .await?
157 .ok_or(IssueError::NotFound)?;
154 if let Some(p) = cmd.parent_dir_id {
155 let parent = dirs::Entity::find_by_id(p)
156 .filter(dirs::Column::SpaceId.eq(Some(space.id)))
157 .one(self.db().read())
158 .await?
159 .ok_or(IssueError::NotFound)?;
158160
159 if dir.kind != DirKind::Dir {
160 return Err(IssueError::BadRequest("target dir must be a dir node".to_string()));
161 if parent.kind != DirKind::Dir {
162 return Err(IssueError::BadRequest("parent node must be a dir".to_string()));
163 }
161164 }
162165
163 let now = OffsetDateTime::now_utc();
164 let row = issues::ActiveModel {
165 id: Set(Uuid::now_v7()),
166 dir_id: Set(cmd.dir_id),
167 name: Set(name.to_string()),
168 icon: Set(cmd.icon),
169 description: Set(cmd.description),
170 created_at: Set(now),
171 updated_at: Set(None),
166 let (_dir, board) = {
167 let txn = self.db().write().begin().await?;
168 let position = {
169 let max_node = dirs::Entity::find()
170 .filter(dirs::Column::SpaceId.eq(Some(space.id)))
171 .filter(dirs::Column::ParentDirId.eq(cmd.parent_dir_id))
172 .order_by_desc(dirs::Column::Position)
173 .lock(sea_orm::sea_query::LockType::Update)
174 .one(&txn)
175 .await?;
176 max_node.map(|n| n.position + 1).unwrap_or(0)
177 };
178 let now = OffsetDateTime::now_utc();
179 let dir_row = dirs::ActiveModel {
180 id: Set(Uuid::now_v7()),
181 space_id: Set(Some(space.id)),
182 name: Set(name.to_string()),
183 kind: Set(DirKind::Object),
184 parent_dir_id: Set(cmd.parent_dir_id),
185 color: Set(None),
186 position: Set(position),
187 icon: Set(cmd.icon.clone()),
188 topic: Set(None),
189 purpose: Set(cmd.description.clone()),
190 is_archived: Set(false),
191 archived_at: Set(None),
192 archived_by: Set(None),
193 created_at: Set(now),
194 updated_at: Set(now),
195 created_by_uid: Set(caller),
196 };
197 let dir = dir_row.insert(&txn).await?;
198
199 let board_row = issues::ActiveModel {
200 id: Set(Uuid::now_v7()),
201 dir_id: Set(dir.id),
202 name: Set(name.to_string()),
203 icon: Set(cmd.icon),
204 description: Set(cmd.description),
205 created_at: Set(now),
206 updated_at: Set(None),
207 };
208 let board = board_row.insert(&txn).await?;
209 txn.commit().await?;
210 (dir, board)
172211 };
173212
174 let inserted = row.insert(self.db().write()).await?;
175213 Ok(BoardDto {
176 id: inserted.id,
177 dir_id: inserted.dir_id,
178 name: inserted.name,
179 icon: inserted.icon,
180 description: inserted.description,
214 id: board.id,
215 dir_id: board.dir_id,
216 name: board.name,
217 icon: board.icon,
218 description: board.description,
181219 nums_open_issue: 0,
182220 nums_closed_issue: 0,
183 created_at: inserted.created_at,
184 updated_at: inserted.updated_at,
221 created_at: board.created_at,
222 updated_at: board.updated_at,
185223 })
186224 }
187225
@@ -201,6 +239,7 @@
201239
202240 let dirs_in_space = dirs::Entity::find()
203241 .filter(dirs::Column::SpaceId.eq(Some(space.id)))
242 .filter(dirs::Column::Kind.eq(DirKind::Object))
204243 .all(self.db().read())
205244 .await?;
206245
@@ -211,21 +250,25 @@
211250 });
212251 }
213252
214 let dir_ids: Vec<Uuid> = dirs_in_space.into_iter().map(|d| d.id).collect();
253 let dir_ids: Vec<Uuid> = if let Some(target_dir) = q.dir_id {
254 dirs_in_space
255 .into_iter()
256 .filter(|d| d.parent_dir_id == Some(target_dir) || d.id == target_dir)
257 .map(|d| d.id)
258 .collect()
259 } else {
260 dirs_in_space.into_iter().map(|d| d.id).collect()
261 };
215262
216 if q.dir_id.is_some_and(|target_dir| !dir_ids.contains(&target_dir)) {
263 if dir_ids.is_empty() {
217264 return Ok(BoardPage {
218265 items: Vec::new(),
219266 total: 0,
220267 });
221268 }
222269
223 let mut select = issues::Entity::find();
224 if let Some(target_dir) = q.dir_id {
225 select = select.filter(issues::Column::DirId.eq(target_dir));
226 } else {
227 select = select.filter(issues::Column::DirId.is_in(dir_ids));
228 }
270 let mut select = issues::Entity::find()
271 .filter(issues::Column::DirId.is_in(dir_ids));
229272
230273 if let Some(ref search) = q.q {
231274 let kw = search.trim();
@@ -282,25 +325,47 @@
282325 require_board_manage(self, &space, caller).await?;
283326 let board = lookup_board(self, &space, board_id).await?;
284327
285 let mut am: issues::ActiveModel = board.into();
328 let now = OffsetDateTime::now_utc();
329 let mut am: issues::ActiveModel = board.clone().into();
330 let mut dir_am = dirs::ActiveModel {
331 id: Unchanged(board.dir_id),
332 ..Default::default()
333 };
334 let mut dir_changed = false;
335
286336 if let Some(new_name) = cmd.name {
287337 let name = new_name.trim();
288338 if name.is_empty() || name.chars().count() > self.config().issues.max_board_name_len {
289339 return Err(IssueError::BadRequest("board name is empty or too long".to_string()));
290340 }
291341 am.name = Set(name.to_string());
342 dir_am.name = Set(name.to_string());
343 dir_changed = true;
292344 }
293345 if let Some(icon) = cmd.icon {
294 am.icon = Set(icon);
346 am.icon = Set(icon.clone());
347 dir_am.icon = Set(icon);
348 dir_changed = true;
295349 }
296350 if let Some(desc) = cmd.description {
297 am.description = Set(desc);
351 am.description = Set(desc.clone());
352 dir_am.purpose = Set(desc);
353 dir_changed = true;
298354 }
299355
300 let now = OffsetDateTime::now_utc();
301356 am.updated_at = Set(Some(now));
302 let updated = am.update(self.db().write()).await?;
303357
358 let updated = {
359 let txn = self.db().write().begin().await?;
360 if dir_changed {
361 dir_am.updated_at = Set(now);
362 dir_am.update(&txn).await?;
363 }
364 let updated = am.update(&txn).await?;
365 txn.commit().await?;
366 updated
367 };
368
304369 self.build_board_dto(updated).await
305370 }
306371
@@ -340,6 +405,8 @@
340405
341406 issues::Entity::delete_by_id(board.id).exec(txn).await?;
342407
408 dirs::Entity::delete_by_id(board.dir_id).exec(txn).await?;
409
343410 Ok(())
344411 })
345412 })
Modified lib/issues/src/share/lookup.rs lib/issues/src/share/lookup.rs +1 −1 View lib/issues/src/share/lookup.rs
@@ -41,7 +41,7 @@
4141 .await?
4242 .ok_or(IssueError::NotFound)?;
4343
44 if dir.kind != DirKind::Dir {
44 if dir.kind != DirKind::Object {
4545 return Err(IssueError::NotFound);
4646 }
4747
Modified lib/space/src/services/dir.rs lib/space/src/services/dir.rs +0 −83 View lib/space/src/services/dir.rs
@@ -773,86 +773,3 @@
773773 Ok(())
774774 }
775775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 fn group_ow(dir_id: Uuid, group_id: Uuid, allow: i64, deny: i64) -> dir_overwrite_group::Model {
782 dir_overwrite_group::Model {
783 dir_id,
784 group_id,
785 allow_mask: allow,
786 deny_mask: deny,
787 }
788 }
789
790 fn user_ow(dir_id: Uuid, user_id: Uuid, allow: i64, deny: i64) -> dir_overwrite_users::Model {
791 dir_overwrite_users::Model {
792 dir_id,
793 user_id,
794 allow_mask: allow,
795 deny_mask: deny,
796 }
797 }
798
799 #[test]
800 fn node_without_overwrite_is_visible() {
801 let caller = Uuid::now_v7();
802 assert!(resolve_node_visible(caller, &HashSet::new(), &[], &[]));
803 }
804
805 #[test]
806 fn user_allow_for_one_keeps_others_visible_by_default() {
807 let a = Uuid::now_v7();
808 let b = Uuid::now_v7();
809 let dir_id = Uuid::now_v7();
810 let ows = vec![user_ow(dir_id, a, PERM_DIR_VISIBLE, 0)];
811
812 assert!(resolve_node_visible(a, &HashSet::new(), &[], &ows));
813 assert!(resolve_node_visible(b, &HashSet::new(), &[], &ows));
814 }
815
816 #[test]
817 fn user_deny_hides_target() {
818 let a = Uuid::now_v7();
819 let dir_id = Uuid::now_v7();
820 let ows = vec![user_ow(dir_id, a, 0, PERM_DIR_VISIBLE)];
821
822 assert!(!resolve_node_visible(a, &HashSet::new(), &[], &ows));
823 }
824
825 #[test]
826 fn group_deny_hides_member() {
827 let member = Uuid::now_v7();
828 let group = Uuid::now_v7();
829 let dir_id = Uuid::now_v7();
830 let ows = vec![group_ow(dir_id, group, 0, PERM_DIR_VISIBLE)];
831 let my_groups = HashSet::from([group]);
832
833 assert!(!resolve_node_visible(member, &my_groups, &ows, &[]));
834 }
835
836 #[test]
837 fn group_allow_grants_visibility() {
838 let member = Uuid::now_v7();
839 let group = Uuid::now_v7();
840 let dir_id = Uuid::now_v7();
841 let ows = vec![group_ow(dir_id, group, PERM_DIR_VISIBLE, 0)];
842 let my_groups = HashSet::from([group]);
843
844 assert!(resolve_node_visible(member, &my_groups, &ows, &[]));
845 }
846
847 #[test]
848 fn unrelated_group_overwrite_defaults_visible() {
849 let member = Uuid::now_v7();
850 let group = Uuid::now_v7();
851 let other = Uuid::now_v7();
852 let dir_id = Uuid::now_v7();
853 let ows = vec![group_ow(dir_id, other, 0, PERM_DIR_VISIBLE)];
854 let my_groups = HashSet::from([group]);
855
856 assert!(resolve_node_visible(member, &my_groups, &ows, &[]));
857 }
858}