feat(space): add sidebar feature and handler modules
zhenyi committed
Added lib/space/features/sidebar/impls.rs lib/space/features/sidebar/impls.rs +18 −0 View lib/space/features/sidebar/impls.rs
@@ -0,0 +1,18 @@
1use uuid::Uuid;
2
3use export::space::sidebar::SidebarTreeView;
4
5use crate::error::SpaceResult;
6use crate::features::SpaceService;
7
8use super::usecase;
9
10impl SpaceService {
11 pub async fn fetch_sidebar(
12 &self,
13 caller: Uuid,
14 space_id: Uuid,
15 ) -> SpaceResult<SidebarTreeView> {
16 usecase::fetch_sidebar(self, caller, space_id).await
17 }
18}
Added lib/space/features/sidebar/mod.rs lib/space/features/sidebar/mod.rs +3 −0 View lib/space/features/sidebar/mod.rs
@@ -0,0 +1,3 @@
1pub mod impls;
2pub mod usecase;
3pub use export::space::sidebar::*;
Added lib/space/features/sidebar/usecase.rs lib/space/features/sidebar/usecase.rs +151 −0 View lib/space/features/sidebar/usecase.rs
@@ -0,0 +1,151 @@
1use std::collections::HashSet;
2
3use db::channel::channels as channel_entity;
4use db::channel::channels::ChannelKind;
5use db::conn::sea_orm::*;
6use db::repo::repos as repo_entity;
7use db::space::space_members as member_entity;
8use db::space::spaces as space_entity;
9use uuid::Uuid;
10
11use export::space::sidebar::{SidebarNode, SidebarNodeKind, SidebarTreeView};
12
13use crate::error::{SpaceError, SpaceResult};
14use crate::features::SpaceService;
15
16const GROUP_CHANNELS: u128 = 1;
17const GROUP_REPOS: u128 = 2;
18const GROUP_MEMBERS: u128 = 3;
19
20/// 校验调用者是空间的有效成员(任何 Active 成员可读侧边栏)
21async fn require_active_member(
22 svc: &SpaceService,
23 caller: Uuid,
24 space_id: Uuid,
25) -> SpaceResult<()> {
26 let db = svc.db.reader();
27 space_entity::Entity::find_by_id(space_id)
28 .one(&db)
29 .await?
30 .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?;
31 let membership = member_entity::Entity::find_by_id((space_id, caller))
32 .one(&db)
33 .await?;
34 match membership {
35 Some(m) if m.status == member_entity::MemberStatus::Active => Ok(()),
36 _ => Err(SpaceError::Forbidden(
37 "you are not an active member of this space".to_string(),
38 )),
39 }
40}
41
42pub async fn fetch_sidebar(
43 svc: &SpaceService,
44 caller: Uuid,
45 space_id: Uuid,
46) -> SpaceResult<SidebarTreeView> {
47 require_active_member(svc, caller, space_id).await?;
48 let db = svc.db.reader();
49
50 // 1. 频道(未归档,按 position 排序)——邻接表,parent_id 组树由前端完成
51 let channels = channel_entity::Entity::find()
52 .filter(channel_entity::Column::SpaceId.eq(space_id))
53 .filter(channel_entity::Column::IsArchived.eq(false))
54 .order_by_asc(channel_entity::Column::Position)
55 .all(&db)
56 .await?;
57
58 // 2. 仓库(channel_id 命中频道集合,未归档)
59 let channel_ids: Vec<Uuid> = channels.iter().map(|c| c.id).collect();
60 let repos = if channel_ids.is_empty() {
61 Vec::new()
62 } else {
63 repo_entity::Entity::find()
64 .filter(repo_entity::Column::ChannelId.is_in(channel_ids))
65 .filter(repo_entity::Column::IsArchived.eq(false))
66 .all(&db)
67 .await?
68 };
69
70 // 3. 成员(Active)
71 let members = member_entity::Entity::find()
72 .filter(member_entity::Column::SpaceId.eq(space_id))
73 .filter(member_entity::Column::Status.eq(member_entity::MemberStatus::Active))
74 .order_by_asc(member_entity::Column::JoinedAt)
75 .all(&db)
76 .await?;
77
78 // 4. repo 对应的 object 频道不进频道列表,作为 Repo 节点
79 let repo_channel_ids: HashSet<Uuid> = repos.iter().map(|r| r.channel_id).collect();
80
81 let channel_nodes: Vec<SidebarNode> = channels
82 .iter()
83 .filter(|c| !(c.kind == ChannelKind::Object && repo_channel_ids.contains(&c.id)))
84 .map(|c| {
85 let kind = if c.kind == ChannelKind::Directory {
86 SidebarNodeKind::Directory
87 } else {
88 SidebarNodeKind::Channel
89 };
90 SidebarNode {
91 id: c.id,
92 kind,
93 name: c.name.clone(),
94 position: c.position,
95 color: c.color.clone(),
96 is_archived: c.is_archived,
97 parent_id: c.parent_id.or(Some(Uuid::from_u128(GROUP_CHANNELS))),
98 }
99 })
100 .collect();
101
102 let repo_nodes: Vec<SidebarNode> = repos
103 .iter()
104 .map(|r| SidebarNode {
105 id: r.id,
106 kind: SidebarNodeKind::Repo,
107 name: r.name.clone(),
108 position: 0,
109 color: None,
110 is_archived: r.is_archived,
111 parent_id: Some(Uuid::from_u128(GROUP_REPOS)),
112 })
113 .collect();
114
115 let member_nodes: Vec<SidebarNode> = members
116 .iter()
117 .map(|m| SidebarNode {
118 id: m.user_id,
119 kind: SidebarNodeKind::Member,
120 name: m
121 .alias_name
122 .clone()
123 .unwrap_or_else(|| m.user_id.to_string()),
124 position: 0,
125 color: None,
126 is_archived: false,
127 parent_id: Some(Uuid::from_u128(GROUP_MEMBERS)),
128 })
129 .collect();
130
131 let group = |id: u128, name: &str, position: i32| SidebarNode {
132 id: Uuid::from_u128(id),
133 kind: SidebarNodeKind::Group,
134 name: name.to_string(),
135 position,
136 color: None,
137 is_archived: false,
138 parent_id: None,
139 };
140
141 let mut tree = vec![
142 group(GROUP_CHANNELS, "频道", 0),
143 group(GROUP_REPOS, "仓库", 1),
144 group(GROUP_MEMBERS, "私信", 2),
145 ];
146 tree.extend(channel_nodes);
147 tree.extend(repo_nodes);
148 tree.extend(member_nodes);
149
150 Ok(SidebarTreeView { tree })
151}
Added lib/space/handler/sidebar/get.rs lib/space/handler/sidebar/get.rs +25 −0 View lib/space/handler/sidebar/get.rs
@@ -0,0 +1,25 @@
1use axum::extract::{Path, State};
2use jwt::extractor::AuthUser;
3
4use crate::error::SpaceResult;
5use crate::features::SpaceService;
6use export::data::AppResponseData;
7use export::space::sidebar::SidebarTreeView;
8
9#[utoipa::path(
10 get,
11 path = "/spaces/{space_name}/sidebar",
12 tag = "space",
13 params(("space_name" = String, Path)),
14 responses((status = 200, description = "Get the sidebar tree for a space", body = AppResponseData<SidebarTreeView>)),
15 security(("bearer_auth" = []))
16)]
17pub(crate) async fn handler(
18 State(svc): State<SpaceService>,
19 AuthUser { claims }: AuthUser,
20 Path(space_name): Path<String>,
21) -> SpaceResult<AppResponseData<SidebarTreeView>> {
22 let space_id = crate::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
23 let tree = svc.fetch_sidebar(claims.sub, space_id).await?;
24 Ok(AppResponseData::ok(tree))
25}
Added lib/space/handler/sidebar/mod.rs lib/space/handler/sidebar/mod.rs +1 −0 View lib/space/handler/sidebar/mod.rs
@@ -0,0 +1 @@
1pub(crate) mod get;