Some checks failed
BotServer CI / build (push) Failing after 1m34s
Split 20+ files over 1000 lines into focused subdirectories for better maintainability and code organization. All changes maintain backward compatibility through re-export wrappers. Major splits: - attendance/llm_assist.rs (2074→7 modules) - basic/keywords/face_api.rs → face_api/ (7 modules) - basic/keywords/file_operations.rs → file_ops/ (8 modules) - basic/keywords/hear_talk.rs → hearing/ (6 modules) - channels/wechat.rs → wechat/ (10 modules) - channels/youtube.rs → youtube/ (5 modules) - contacts/mod.rs → contacts_api/ (6 modules) - core/bootstrap/mod.rs → bootstrap/ (5 modules) - core/shared/admin.rs → admin_*.rs (5 modules) - designer/canvas.rs → canvas_api/ (6 modules) - designer/mod.rs → designer_api/ (6 modules) - docs/handlers.rs → handlers_api/ (11 modules) - drive/mod.rs → drive_handlers.rs, drive_types.rs - learn/mod.rs → types.rs - main.rs → main_module/ (7 modules) - meet/webinar.rs → webinar_api/ (8 modules) - paper/mod.rs → (10 modules) - security/auth.rs → auth_api/ (7 modules) - security/passkey.rs → (4 modules) - sources/mod.rs → sources_api/ (5 modules) - tasks/mod.rs → task_api/ (5 modules) Stats: 38,040 deletions, 1,315 additions across 318 files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1370 lines
40 KiB
Rust
1370 lines
40 KiB
Rust
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
routing::{delete, get, post},
|
|
Json, Router,
|
|
};
|
|
use chrono::{DateTime, Utc};
|
|
use diesel::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use crate::core::bot::get_default_bot;
|
|
use crate::core::shared::schema::workspaces::{
|
|
workspace_comments, workspace_members, workspace_page_versions, workspace_pages,
|
|
workspaces as workspaces_table,
|
|
};
|
|
use crate::core::shared::state::AppState;
|
|
|
|
pub mod blocks;
|
|
pub mod collaboration;
|
|
pub mod pages;
|
|
pub mod templates;
|
|
pub mod ui;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = workspaces_table)]
|
|
pub struct DbWorkspace {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub icon_type: Option<String>,
|
|
pub icon_value: Option<String>,
|
|
pub cover_image: Option<String>,
|
|
pub settings: serde_json::Value,
|
|
pub created_by: Uuid,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = workspace_members)]
|
|
pub struct DbWorkspaceMember {
|
|
pub id: Uuid,
|
|
pub workspace_id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub role: String,
|
|
pub invited_by: Option<Uuid>,
|
|
pub joined_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = workspace_pages)]
|
|
pub struct DbWorkspacePage {
|
|
pub id: Uuid,
|
|
pub workspace_id: Uuid,
|
|
pub parent_id: Option<Uuid>,
|
|
pub title: String,
|
|
pub icon_type: Option<String>,
|
|
pub icon_value: Option<String>,
|
|
pub cover_image: Option<String>,
|
|
pub content: serde_json::Value,
|
|
pub properties: serde_json::Value,
|
|
pub is_template: bool,
|
|
pub template_id: Option<Uuid>,
|
|
pub is_public: bool,
|
|
pub public_edit: bool,
|
|
pub position: i32,
|
|
pub created_by: Uuid,
|
|
pub last_edited_by: Option<Uuid>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = workspace_page_versions)]
|
|
pub struct DbPageVersion {
|
|
pub id: Uuid,
|
|
pub page_id: Uuid,
|
|
pub version_number: i32,
|
|
pub title: String,
|
|
pub content: serde_json::Value,
|
|
pub change_summary: Option<String>,
|
|
pub created_by: Uuid,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = workspace_comments)]
|
|
pub struct DbWorkspaceComment {
|
|
pub id: Uuid,
|
|
pub workspace_id: Uuid,
|
|
pub page_id: Uuid,
|
|
pub block_id: Option<Uuid>,
|
|
pub parent_comment_id: Option<Uuid>,
|
|
pub author_id: Uuid,
|
|
pub content: String,
|
|
pub resolved: bool,
|
|
pub resolved_by: Option<Uuid>,
|
|
pub resolved_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Workspace {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub icon: Option<WorkspaceIcon>,
|
|
pub cover_image: Option<String>,
|
|
pub settings: WorkspaceSettings,
|
|
pub created_by: Uuid,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub members: Vec<WorkspaceMember>,
|
|
pub root_pages: Vec<Uuid>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WorkspaceIcon {
|
|
pub icon_type: IconType,
|
|
pub value: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum IconType {
|
|
Emoji,
|
|
Image,
|
|
Lucide,
|
|
}
|
|
|
|
impl IconType {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Emoji => "emoji",
|
|
Self::Image => "image",
|
|
Self::Lucide => "lucide",
|
|
}
|
|
}
|
|
|
|
fn from_str(s: &str) -> Self {
|
|
match s {
|
|
"emoji" => Self::Emoji,
|
|
"image" => Self::Image,
|
|
"lucide" => Self::Lucide,
|
|
_ => Self::Emoji,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WorkspaceMember {
|
|
pub user_id: Uuid,
|
|
pub role: WorkspaceRole,
|
|
pub joined_at: DateTime<Utc>,
|
|
pub invited_by: Option<Uuid>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum WorkspaceRole {
|
|
Owner,
|
|
Admin,
|
|
Editor,
|
|
Commenter,
|
|
Viewer,
|
|
}
|
|
|
|
impl WorkspaceRole {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Owner => "owner",
|
|
Self::Admin => "admin",
|
|
Self::Editor => "editor",
|
|
Self::Commenter => "commenter",
|
|
Self::Viewer => "viewer",
|
|
}
|
|
}
|
|
|
|
fn from_str(s: &str) -> Self {
|
|
match s {
|
|
"owner" => Self::Owner,
|
|
"admin" => Self::Admin,
|
|
"editor" => Self::Editor,
|
|
"commenter" => Self::Commenter,
|
|
"viewer" => Self::Viewer,
|
|
_ => Self::Viewer,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct WorkspaceSettings {
|
|
#[serde(default)]
|
|
pub default_page_width: PageWidth,
|
|
#[serde(default)]
|
|
pub allow_public_pages: bool,
|
|
#[serde(default = "default_true")]
|
|
pub enable_comments: bool,
|
|
#[serde(default = "default_true")]
|
|
pub enable_reactions: bool,
|
|
#[serde(default = "default_true")]
|
|
pub enable_gb_assist: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub gb_bot_id: Option<Uuid>,
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum PageWidth {
|
|
Small,
|
|
#[default]
|
|
Normal,
|
|
Wide,
|
|
Full,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Page {
|
|
pub id: Uuid,
|
|
pub workspace_id: Uuid,
|
|
pub parent_id: Option<Uuid>,
|
|
pub title: String,
|
|
pub icon: Option<WorkspaceIcon>,
|
|
pub cover_image: Option<String>,
|
|
pub blocks: Vec<Block>,
|
|
pub children: Vec<Uuid>,
|
|
pub properties: HashMap<String, PropertyValue>,
|
|
pub permissions: PagePermissions,
|
|
pub is_template: bool,
|
|
pub template_id: Option<Uuid>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub created_by: Uuid,
|
|
pub last_edited_by: Uuid,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct PagePermissions {
|
|
#[serde(default = "default_true")]
|
|
pub inherit_from_parent: bool,
|
|
#[serde(default)]
|
|
pub public: bool,
|
|
#[serde(default)]
|
|
pub public_edit: bool,
|
|
#[serde(default)]
|
|
pub allowed_users: Vec<Uuid>,
|
|
#[serde(default)]
|
|
pub allowed_roles: Vec<WorkspaceRole>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Block {
|
|
pub id: Uuid,
|
|
pub block_type: BlockType,
|
|
pub content: BlockContent,
|
|
pub properties: BlockProperties,
|
|
pub children: Vec<Block>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub created_by: Uuid,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum BlockType {
|
|
Paragraph,
|
|
Heading1,
|
|
Heading2,
|
|
Heading3,
|
|
BulletedList,
|
|
NumberedList,
|
|
Checklist,
|
|
Toggle,
|
|
Quote,
|
|
Callout,
|
|
Divider,
|
|
Table,
|
|
Code,
|
|
Image,
|
|
Video,
|
|
File,
|
|
Embed,
|
|
Bookmark,
|
|
LinkToPage,
|
|
SyncedBlock,
|
|
TableOfContents,
|
|
Breadcrumb,
|
|
Equation,
|
|
ColumnList,
|
|
Column,
|
|
GbComponent,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum BlockContent {
|
|
Text { text: RichText },
|
|
Media { url: String, caption: Option<String> },
|
|
Table { rows: Vec<TableRow> },
|
|
Code { code: String, language: Option<String> },
|
|
Embed { url: String, embed_type: Option<String> },
|
|
Callout { icon: Option<String>, text: RichText },
|
|
Toggle { title: RichText, expanded: bool },
|
|
Checklist { items: Vec<ChecklistItem> },
|
|
GbComponent { component_type: String, config: serde_json::Value },
|
|
Empty,
|
|
}
|
|
|
|
impl Default for BlockContent {
|
|
fn default() -> Self {
|
|
Self::Empty
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct RichText {
|
|
pub segments: Vec<TextSegment>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TextSegment {
|
|
pub text: String,
|
|
#[serde(default)]
|
|
pub annotations: TextAnnotations,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub link: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub mention: Option<Mention>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct TextAnnotations {
|
|
#[serde(default)]
|
|
pub bold: bool,
|
|
#[serde(default)]
|
|
pub italic: bool,
|
|
#[serde(default)]
|
|
pub underline: bool,
|
|
#[serde(default)]
|
|
pub strikethrough: bool,
|
|
#[serde(default)]
|
|
pub code: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub background_color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Mention {
|
|
pub mention_type: MentionType,
|
|
pub target_id: Uuid,
|
|
pub display_text: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MentionType {
|
|
User,
|
|
Page,
|
|
Date,
|
|
Database,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TableRow {
|
|
pub id: Uuid,
|
|
pub cells: Vec<TableCell>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TableCell {
|
|
pub content: RichText,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub background_color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChecklistItem {
|
|
pub id: Uuid,
|
|
pub text: RichText,
|
|
#[serde(default)]
|
|
pub checked: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub assignee: Option<Uuid>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct BlockProperties {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub background_color: Option<String>,
|
|
#[serde(default)]
|
|
pub indent_level: u32,
|
|
#[serde(default)]
|
|
pub collapsed: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum PropertyValue {
|
|
Text(String),
|
|
Number(f64),
|
|
Boolean(bool),
|
|
Date(DateTime<Utc>),
|
|
Select(String),
|
|
MultiSelect(Vec<String>),
|
|
User(Uuid),
|
|
Users(Vec<Uuid>),
|
|
Url(String),
|
|
Email(String),
|
|
Phone(String),
|
|
Relation(Vec<Uuid>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PageVersion {
|
|
pub id: Uuid,
|
|
pub page_id: Uuid,
|
|
pub version_number: i32,
|
|
pub title: String,
|
|
pub blocks: Vec<Block>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub created_by: Uuid,
|
|
pub change_summary: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Comment {
|
|
pub id: Uuid,
|
|
pub page_id: Uuid,
|
|
pub block_id: Option<Uuid>,
|
|
pub parent_comment_id: Option<Uuid>,
|
|
pub author_id: Uuid,
|
|
pub content: String,
|
|
pub resolved: bool,
|
|
pub resolved_by: Option<Uuid>,
|
|
pub resolved_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PageTreeNode {
|
|
pub id: Uuid,
|
|
pub title: String,
|
|
pub icon: Option<WorkspaceIcon>,
|
|
pub children: Vec<PageTreeNode>,
|
|
pub has_children: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PageSearchResult {
|
|
pub page_id: Uuid,
|
|
pub title: String,
|
|
pub icon: Option<WorkspaceIcon>,
|
|
pub snippet: String,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SlashCommand {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub icon: String,
|
|
pub category: SlashCommandCategory,
|
|
pub keywords: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum SlashCommandCategory {
|
|
GbAssist,
|
|
General,
|
|
Media,
|
|
Embed,
|
|
Advanced,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum WorkspacesError {
|
|
WorkspaceNotFound,
|
|
PageNotFound,
|
|
BlockNotFound,
|
|
CommentNotFound,
|
|
VersionNotFound,
|
|
MemberNotFound,
|
|
MemberAlreadyExists,
|
|
CannotRemoveLastOwner,
|
|
PermissionDenied,
|
|
InvalidOperation(String),
|
|
DbError(String),
|
|
}
|
|
|
|
impl std::fmt::Display for WorkspacesError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::WorkspaceNotFound => write!(f, "Workspace not found"),
|
|
Self::PageNotFound => write!(f, "Page not found"),
|
|
Self::BlockNotFound => write!(f, "Block not found"),
|
|
Self::CommentNotFound => write!(f, "Comment not found"),
|
|
Self::VersionNotFound => write!(f, "Version not found"),
|
|
Self::MemberNotFound => write!(f, "Member not found"),
|
|
Self::MemberAlreadyExists => write!(f, "Member already exists in workspace"),
|
|
Self::CannotRemoveLastOwner => write!(f, "Cannot remove the last owner"),
|
|
Self::PermissionDenied => write!(f, "Permission denied"),
|
|
Self::InvalidOperation(e) => write!(f, "Invalid operation: {e}"),
|
|
Self::DbError(e) => write!(f, "Database error: {e}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for WorkspacesError {}
|
|
|
|
impl IntoResponse for WorkspacesError {
|
|
fn into_response(self) -> axum::response::Response {
|
|
let (status, message) = match &self {
|
|
Self::WorkspaceNotFound
|
|
| Self::PageNotFound
|
|
| Self::BlockNotFound
|
|
| Self::CommentNotFound
|
|
| Self::VersionNotFound
|
|
| Self::MemberNotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
|
Self::PermissionDenied => (StatusCode::FORBIDDEN, self.to_string()),
|
|
Self::MemberAlreadyExists | Self::CannotRemoveLastOwner | Self::InvalidOperation(_) => {
|
|
(StatusCode::BAD_REQUEST, self.to_string())
|
|
}
|
|
Self::DbError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()),
|
|
};
|
|
(status, Json(serde_json::json!({"error": message}))).into_response()
|
|
}
|
|
}
|
|
|
|
fn get_bot_context(state: &AppState) -> (Uuid, Uuid) {
|
|
let Ok(mut conn) = state.conn.get() else {
|
|
return (Uuid::nil(), Uuid::nil());
|
|
};
|
|
let (bot_id, _bot_name) = get_default_bot(&mut conn);
|
|
let org_id = Uuid::nil();
|
|
(org_id, bot_id)
|
|
}
|
|
|
|
fn db_to_workspace(db: DbWorkspace, members: Vec<WorkspaceMember>, root_pages: Vec<Uuid>) -> Workspace {
|
|
let icon = match (&db.icon_type, &db.icon_value) {
|
|
(Some(t), Some(v)) => Some(WorkspaceIcon {
|
|
icon_type: IconType::from_str(t),
|
|
value: v.clone(),
|
|
}),
|
|
_ => None,
|
|
};
|
|
let settings: WorkspaceSettings =
|
|
serde_json::from_value(db.settings).unwrap_or_default();
|
|
|
|
Workspace {
|
|
id: db.id,
|
|
org_id: db.org_id,
|
|
name: db.name,
|
|
description: db.description,
|
|
icon,
|
|
cover_image: db.cover_image,
|
|
settings,
|
|
created_by: db.created_by,
|
|
created_at: db.created_at,
|
|
updated_at: db.updated_at,
|
|
members,
|
|
root_pages,
|
|
}
|
|
}
|
|
|
|
fn db_to_page(db: DbWorkspacePage, children: Vec<Uuid>) -> Page {
|
|
let icon = match (&db.icon_type, &db.icon_value) {
|
|
(Some(t), Some(v)) => Some(WorkspaceIcon {
|
|
icon_type: IconType::from_str(t),
|
|
value: v.clone(),
|
|
}),
|
|
_ => None,
|
|
};
|
|
let blocks: Vec<Block> = serde_json::from_value(db.content).unwrap_or_default();
|
|
let properties: HashMap<String, PropertyValue> =
|
|
serde_json::from_value(db.properties).unwrap_or_default();
|
|
|
|
Page {
|
|
id: db.id,
|
|
workspace_id: db.workspace_id,
|
|
parent_id: db.parent_id,
|
|
title: db.title,
|
|
icon,
|
|
cover_image: db.cover_image,
|
|
blocks,
|
|
children,
|
|
properties,
|
|
permissions: PagePermissions {
|
|
inherit_from_parent: true,
|
|
public: db.is_public,
|
|
public_edit: db.public_edit,
|
|
allowed_users: vec![],
|
|
allowed_roles: vec![],
|
|
},
|
|
is_template: db.is_template,
|
|
template_id: db.template_id,
|
|
created_at: db.created_at,
|
|
updated_at: db.updated_at,
|
|
created_by: db.created_by,
|
|
last_edited_by: db.last_edited_by.unwrap_or(db.created_by),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateWorkspaceRequest {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateWorkspaceRequest {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub icon: Option<WorkspaceIcon>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreatePageRequest {
|
|
pub title: String,
|
|
pub parent_id: Option<Uuid>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdatePageRequest {
|
|
pub title: Option<String>,
|
|
pub icon: Option<WorkspaceIcon>,
|
|
pub blocks: Option<Vec<Block>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AddMemberRequest {
|
|
pub user_id: Uuid,
|
|
pub role: WorkspaceRole,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateCommentRequest {
|
|
pub content: String,
|
|
pub block_id: Option<Uuid>,
|
|
pub parent_comment_id: Option<Uuid>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ListQuery {
|
|
pub search: Option<String>,
|
|
pub limit: Option<i64>,
|
|
pub offset: Option<i64>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SearchQuery {
|
|
pub q: Option<String>,
|
|
}
|
|
|
|
async fn list_workspaces(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(query): Query<ListQuery>,
|
|
) -> Result<Json<Vec<Workspace>>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let limit = query.limit.unwrap_or(50);
|
|
let offset = query.offset.unwrap_or(0);
|
|
|
|
let mut q = workspaces_table::table
|
|
.filter(workspaces_table::org_id.eq(org_id))
|
|
.filter(workspaces_table::bot_id.eq(bot_id))
|
|
.into_boxed();
|
|
|
|
if let Some(search) = query.search {
|
|
let pattern = format!("%{search}%");
|
|
q = q.filter(
|
|
workspaces_table::name
|
|
.ilike(pattern.clone())
|
|
.or(workspaces_table::description.ilike(pattern)),
|
|
);
|
|
}
|
|
|
|
let db_workspaces: Vec<DbWorkspace> = q
|
|
.order(workspaces_table::updated_at.desc())
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.load(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let mut result = Vec::with_capacity(db_workspaces.len());
|
|
for ws in db_workspaces {
|
|
let db_members: Vec<DbWorkspaceMember> = workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(ws.id))
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let members: Vec<WorkspaceMember> = db_members
|
|
.into_iter()
|
|
.map(|m| WorkspaceMember {
|
|
user_id: m.user_id,
|
|
role: WorkspaceRole::from_str(&m.role),
|
|
joined_at: m.joined_at,
|
|
invited_by: m.invited_by,
|
|
})
|
|
.collect();
|
|
|
|
let root_pages: Vec<Uuid> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(ws.id))
|
|
.filter(workspace_pages::parent_id.is_null())
|
|
.select(workspace_pages::id)
|
|
.order(workspace_pages::position.asc())
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
result.push(db_to_workspace(ws, members, root_pages));
|
|
}
|
|
|
|
Ok(Json(result))
|
|
}
|
|
|
|
async fn create_workspace(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreateWorkspaceRequest>,
|
|
) -> Result<Json<Workspace>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
let user_id = Uuid::nil();
|
|
|
|
let settings = WorkspaceSettings::default();
|
|
let settings_json = serde_json::to_value(&settings).unwrap_or_else(|_| serde_json::json!({}));
|
|
|
|
let db_workspace = DbWorkspace {
|
|
id,
|
|
org_id,
|
|
bot_id,
|
|
name: req.name,
|
|
description: req.description,
|
|
icon_type: None,
|
|
icon_value: None,
|
|
cover_image: None,
|
|
settings: settings_json,
|
|
created_by: user_id,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
diesel::insert_into(workspaces_table::table)
|
|
.values(&db_workspace)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let member = DbWorkspaceMember {
|
|
id: Uuid::new_v4(),
|
|
workspace_id: id,
|
|
user_id,
|
|
role: WorkspaceRole::Owner.as_str().to_string(),
|
|
invited_by: None,
|
|
joined_at: now,
|
|
};
|
|
|
|
diesel::insert_into(workspace_members::table)
|
|
.values(&member)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let members = vec![WorkspaceMember {
|
|
user_id,
|
|
role: WorkspaceRole::Owner,
|
|
joined_at: now,
|
|
invited_by: None,
|
|
}];
|
|
|
|
let workspace = db_to_workspace(db_workspace, members, vec![]);
|
|
Ok(Json(workspace))
|
|
}
|
|
|
|
async fn get_workspace(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
) -> Result<Json<Workspace>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let db_workspace: DbWorkspace = workspaces_table::table
|
|
.filter(workspaces_table::id.eq(workspace_id))
|
|
.first(&mut conn)
|
|
.map_err(|_| WorkspacesError::WorkspaceNotFound)?;
|
|
|
|
let db_members: Vec<DbWorkspaceMember> = workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(workspace_id))
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let members: Vec<WorkspaceMember> = db_members
|
|
.into_iter()
|
|
.map(|m| WorkspaceMember {
|
|
user_id: m.user_id,
|
|
role: WorkspaceRole::from_str(&m.role),
|
|
joined_at: m.joined_at,
|
|
invited_by: m.invited_by,
|
|
})
|
|
.collect();
|
|
|
|
let root_pages: Vec<Uuid> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(workspace_id))
|
|
.filter(workspace_pages::parent_id.is_null())
|
|
.select(workspace_pages::id)
|
|
.order(workspace_pages::position.asc())
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let workspace = db_to_workspace(db_workspace, members, root_pages);
|
|
Ok(Json(workspace))
|
|
}
|
|
|
|
async fn update_workspace(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
Json(req): Json<UpdateWorkspaceRequest>,
|
|
) -> Result<Json<Workspace>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let mut db_workspace: DbWorkspace = workspaces_table::table
|
|
.filter(workspaces_table::id.eq(workspace_id))
|
|
.first(&mut conn)
|
|
.map_err(|_| WorkspacesError::WorkspaceNotFound)?;
|
|
|
|
if let Some(name) = req.name {
|
|
db_workspace.name = name;
|
|
}
|
|
if let Some(desc) = req.description {
|
|
db_workspace.description = Some(desc);
|
|
}
|
|
if let Some(icon) = req.icon {
|
|
db_workspace.icon_type = Some(icon.icon_type.as_str().to_string());
|
|
db_workspace.icon_value = Some(icon.value);
|
|
}
|
|
db_workspace.updated_at = Utc::now();
|
|
|
|
diesel::update(workspaces_table::table.filter(workspaces_table::id.eq(workspace_id)))
|
|
.set(&db_workspace)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let db_members: Vec<DbWorkspaceMember> = workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(workspace_id))
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let members: Vec<WorkspaceMember> = db_members
|
|
.into_iter()
|
|
.map(|m| WorkspaceMember {
|
|
user_id: m.user_id,
|
|
role: WorkspaceRole::from_str(&m.role),
|
|
joined_at: m.joined_at,
|
|
invited_by: m.invited_by,
|
|
})
|
|
.collect();
|
|
|
|
let root_pages: Vec<Uuid> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(workspace_id))
|
|
.filter(workspace_pages::parent_id.is_null())
|
|
.select(workspace_pages::id)
|
|
.order(workspace_pages::position.asc())
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let workspace = db_to_workspace(db_workspace, members, root_pages);
|
|
Ok(Json(workspace))
|
|
}
|
|
|
|
async fn delete_workspace(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
) -> Result<StatusCode, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
diesel::delete(workspace_comments::table.filter(workspace_comments::workspace_id.eq(workspace_id)))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
|
|
let page_ids: Vec<Uuid> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(workspace_id))
|
|
.select(workspace_pages::id)
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
if !page_ids.is_empty() {
|
|
diesel::delete(workspace_page_versions::table.filter(
|
|
workspace_page_versions::page_id.eq_any(&page_ids)
|
|
))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
}
|
|
|
|
diesel::delete(workspace_pages::table.filter(workspace_pages::workspace_id.eq(workspace_id)))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
|
|
diesel::delete(workspace_members::table.filter(workspace_members::workspace_id.eq(workspace_id)))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
|
|
let deleted = diesel::delete(workspaces_table::table.filter(workspaces_table::id.eq(workspace_id)))
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
if deleted > 0 {
|
|
Ok(StatusCode::NO_CONTENT)
|
|
} else {
|
|
Err(WorkspacesError::WorkspaceNotFound)
|
|
}
|
|
}
|
|
|
|
async fn list_pages(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
) -> Result<Json<Vec<PageTreeNode>>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let db_pages: Vec<DbWorkspacePage> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(workspace_id))
|
|
.order(workspace_pages::position.asc())
|
|
.load(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
fn build_tree(pages: &[DbWorkspacePage], parent_id: Option<Uuid>) -> Vec<PageTreeNode> {
|
|
pages
|
|
.iter()
|
|
.filter(|p| p.parent_id == parent_id)
|
|
.map(|p| {
|
|
let icon = match (&p.icon_type, &p.icon_value) {
|
|
(Some(t), Some(v)) => Some(WorkspaceIcon {
|
|
icon_type: IconType::from_str(t),
|
|
value: v.clone(),
|
|
}),
|
|
_ => None,
|
|
};
|
|
let children = build_tree(pages, Some(p.id));
|
|
PageTreeNode {
|
|
id: p.id,
|
|
title: p.title.clone(),
|
|
icon,
|
|
has_children: !children.is_empty(),
|
|
children,
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
let tree = build_tree(&db_pages, None);
|
|
Ok(Json(tree))
|
|
}
|
|
|
|
async fn create_page(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
Json(req): Json<CreatePageRequest>,
|
|
) -> Result<Json<Page>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let _: DbWorkspace = workspaces_table::table
|
|
.filter(workspaces_table::id.eq(workspace_id))
|
|
.first(&mut conn)
|
|
.map_err(|_| WorkspacesError::WorkspaceNotFound)?;
|
|
|
|
let now = Utc::now();
|
|
let user_id = Uuid::nil();
|
|
let id = Uuid::new_v4();
|
|
|
|
let max_position: Option<i32> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(workspace_id))
|
|
.filter(workspace_pages::parent_id.is_not_distinct_from(req.parent_id))
|
|
.select(diesel::dsl::max(workspace_pages::position))
|
|
.first(&mut conn)
|
|
.ok()
|
|
.flatten();
|
|
|
|
let db_page = DbWorkspacePage {
|
|
id,
|
|
workspace_id,
|
|
parent_id: req.parent_id,
|
|
title: req.title,
|
|
icon_type: None,
|
|
icon_value: None,
|
|
cover_image: None,
|
|
content: serde_json::json!([]),
|
|
properties: serde_json::json!({}),
|
|
is_template: false,
|
|
template_id: None,
|
|
is_public: false,
|
|
public_edit: false,
|
|
position: max_position.unwrap_or(0) + 1,
|
|
created_by: user_id,
|
|
last_edited_by: Some(user_id),
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
diesel::insert_into(workspace_pages::table)
|
|
.values(&db_page)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let page = db_to_page(db_page, vec![]);
|
|
Ok(Json(page))
|
|
}
|
|
|
|
async fn get_page(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(page_id): Path<Uuid>,
|
|
) -> Result<Json<Page>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let db_page: DbWorkspacePage = workspace_pages::table
|
|
.filter(workspace_pages::id.eq(page_id))
|
|
.first(&mut conn)
|
|
.map_err(|_| WorkspacesError::PageNotFound)?;
|
|
|
|
let children: Vec<Uuid> = workspace_pages::table
|
|
.filter(workspace_pages::parent_id.eq(page_id))
|
|
.select(workspace_pages::id)
|
|
.order(workspace_pages::position.asc())
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let page = db_to_page(db_page, children);
|
|
Ok(Json(page))
|
|
}
|
|
|
|
async fn update_page(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(page_id): Path<Uuid>,
|
|
Json(req): Json<UpdatePageRequest>,
|
|
) -> Result<Json<Page>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let mut db_page: DbWorkspacePage = workspace_pages::table
|
|
.filter(workspace_pages::id.eq(page_id))
|
|
.first(&mut conn)
|
|
.map_err(|_| WorkspacesError::PageNotFound)?;
|
|
|
|
if let Some(title) = req.title {
|
|
db_page.title = title;
|
|
}
|
|
if let Some(icon) = req.icon {
|
|
db_page.icon_type = Some(icon.icon_type.as_str().to_string());
|
|
db_page.icon_value = Some(icon.value);
|
|
}
|
|
if let Some(blocks) = req.blocks {
|
|
db_page.content = serde_json::to_value(&blocks).unwrap_or_else(|_| serde_json::json!([]));
|
|
}
|
|
db_page.updated_at = Utc::now();
|
|
db_page.last_edited_by = Some(Uuid::nil());
|
|
|
|
diesel::update(workspace_pages::table.filter(workspace_pages::id.eq(page_id)))
|
|
.set(&db_page)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let children: Vec<Uuid> = workspace_pages::table
|
|
.filter(workspace_pages::parent_id.eq(page_id))
|
|
.select(workspace_pages::id)
|
|
.order(workspace_pages::position.asc())
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let page = db_to_page(db_page, children);
|
|
Ok(Json(page))
|
|
}
|
|
|
|
async fn delete_page(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(page_id): Path<Uuid>,
|
|
) -> Result<StatusCode, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
diesel::delete(workspace_comments::table.filter(workspace_comments::page_id.eq(page_id)))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
|
|
diesel::delete(workspace_page_versions::table.filter(workspace_page_versions::page_id.eq(page_id)))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
|
|
diesel::delete(workspace_pages::table.filter(workspace_pages::parent_id.eq(page_id)))
|
|
.execute(&mut conn)
|
|
.ok();
|
|
|
|
let deleted = diesel::delete(workspace_pages::table.filter(workspace_pages::id.eq(page_id)))
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
if deleted > 0 {
|
|
Ok(StatusCode::NO_CONTENT)
|
|
} else {
|
|
Err(WorkspacesError::PageNotFound)
|
|
}
|
|
}
|
|
|
|
async fn add_member(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
Json(req): Json<AddMemberRequest>,
|
|
) -> Result<StatusCode, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let existing: Option<DbWorkspaceMember> = workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(workspace_id))
|
|
.filter(workspace_members::user_id.eq(req.user_id))
|
|
.first(&mut conn)
|
|
.optional()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
if existing.is_some() {
|
|
return Err(WorkspacesError::MemberAlreadyExists);
|
|
}
|
|
|
|
let now = Utc::now();
|
|
let member = DbWorkspaceMember {
|
|
id: Uuid::new_v4(),
|
|
workspace_id,
|
|
user_id: req.user_id,
|
|
role: req.role.as_str().to_string(),
|
|
invited_by: Some(Uuid::nil()),
|
|
joined_at: now,
|
|
};
|
|
|
|
diesel::insert_into(workspace_members::table)
|
|
.values(&member)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
Ok(StatusCode::CREATED)
|
|
}
|
|
|
|
async fn remove_member(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((workspace_id, user_id)): Path<(Uuid, Uuid)>,
|
|
) -> Result<StatusCode, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let owner_count: i64 = workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(workspace_id))
|
|
.filter(workspace_members::role.eq("owner"))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let member: Option<DbWorkspaceMember> = workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(workspace_id))
|
|
.filter(workspace_members::user_id.eq(user_id))
|
|
.first(&mut conn)
|
|
.optional()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
if let Some(m) = member {
|
|
if m.role == "owner" && owner_count <= 1 {
|
|
return Err(WorkspacesError::CannotRemoveLastOwner);
|
|
}
|
|
} else {
|
|
return Err(WorkspacesError::MemberNotFound);
|
|
}
|
|
|
|
diesel::delete(
|
|
workspace_members::table
|
|
.filter(workspace_members::workspace_id.eq(workspace_id))
|
|
.filter(workspace_members::user_id.eq(user_id)),
|
|
)
|
|
.execute(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
async fn search_pages(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workspace_id): Path<Uuid>,
|
|
Query(params): Query<SearchQuery>,
|
|
) -> Result<Json<Vec<PageSearchResult>>, WorkspacesError> {
|
|
let mut conn = state
|
|
.conn
|
|
.get()
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let query = params.q.unwrap_or_default();
|
|
if query.is_empty() {
|
|
return Ok(Json(vec![]));
|
|
}
|
|
|
|
let pattern = format!("%{query}%");
|
|
let db_pages: Vec<DbWorkspacePage> = workspace_pages::table
|
|
.filter(workspace_pages::workspace_id.eq(workspace_id))
|
|
.filter(workspace_pages::title.ilike(&pattern))
|
|
.order(workspace_pages::updated_at.desc())
|
|
.limit(20)
|
|
.load(&mut conn)
|
|
.map_err(|e| WorkspacesError::DbError(e.to_string()))?;
|
|
|
|
let results: Vec<PageSearchResult> = db_pages
|
|
.into_iter()
|
|
.map(|p| {
|
|
let icon = match (&p.icon_type, &p.icon_value) {
|
|
(Some(t), Some(v)) => Some(WorkspaceIcon {
|
|
icon_type: IconType::from_str(t),
|
|
value: v.clone(),
|
|
}),
|
|
_ => None,
|
|
};
|
|
PageSearchResult {
|
|
page_id: p.id,
|
|
title: p.title,
|
|
icon,
|
|
snippet: String::new(),
|
|
updated_at: p.updated_at,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(results))
|
|
}
|
|
|
|
async fn get_slash_commands_handler(
|
|
State(_state): State<Arc<AppState>>,
|
|
) -> Json<Vec<SlashCommand>> {
|
|
Json(vec![
|
|
SlashCommand {
|
|
id: "paragraph".to_string(),
|
|
name: "Text".to_string(),
|
|
description: "Plain text paragraph".to_string(),
|
|
icon: "type".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["text".to_string(), "paragraph".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "heading1".to_string(),
|
|
name: "Heading 1".to_string(),
|
|
description: "Large section heading".to_string(),
|
|
icon: "heading-1".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["h1".to_string(), "heading".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "heading2".to_string(),
|
|
name: "Heading 2".to_string(),
|
|
description: "Medium section heading".to_string(),
|
|
icon: "heading-2".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["h2".to_string(), "heading".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "bulleted_list".to_string(),
|
|
name: "Bulleted list".to_string(),
|
|
description: "Create a bulleted list".to_string(),
|
|
icon: "list".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["bullet".to_string(), "list".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "numbered_list".to_string(),
|
|
name: "Numbered list".to_string(),
|
|
description: "Create a numbered list".to_string(),
|
|
icon: "list-ordered".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["number".to_string(), "list".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "checklist".to_string(),
|
|
name: "Checklist".to_string(),
|
|
description: "Create a checklist".to_string(),
|
|
icon: "check-square".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["todo".to_string(), "checkbox".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "code".to_string(),
|
|
name: "Code".to_string(),
|
|
description: "Create a code block".to_string(),
|
|
icon: "code".to_string(),
|
|
category: SlashCommandCategory::General,
|
|
keywords: vec!["code".to_string(), "snippet".to_string()],
|
|
},
|
|
SlashCommand {
|
|
id: "image".to_string(),
|
|
name: "Image".to_string(),
|
|
description: "Upload or embed an image".to_string(),
|
|
icon: "image".to_string(),
|
|
category: SlashCommandCategory::Media,
|
|
keywords: vec!["image".to_string(), "picture".to_string()],
|
|
},
|
|
])
|
|
}
|
|
|
|
pub fn configure_workspaces_routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.route("/api/workspaces", get(list_workspaces).post(create_workspace))
|
|
.route(
|
|
"/api/workspaces/{workspace_id}",
|
|
get(get_workspace).put(update_workspace).delete(delete_workspace),
|
|
)
|
|
.route(
|
|
"/api/workspaces/{workspace_id}/pages",
|
|
get(list_pages).post(create_page),
|
|
)
|
|
.route("/api/workspaces/{workspace_id}/members", post(add_member))
|
|
.route(
|
|
"/api/workspaces/{workspace_id}/members/{user_id}",
|
|
delete(remove_member),
|
|
)
|
|
.route("/api/workspaces/{workspace_id}/search", get(search_pages))
|
|
.route(
|
|
"/api/pages/{page_id}",
|
|
get(get_page).put(update_page).delete(delete_page),
|
|
)
|
|
.route("/api/workspaces/commands", get(get_slash_commands_handler))
|
|
}
|