Skip to content

Commit

Permalink
feat: api to add database view
Browse files Browse the repository at this point in the history
  • Loading branch information
khorshuheng committed Dec 6, 2024
1 parent ee87bb9 commit b93f39d
Show file tree
Hide file tree
Showing 10 changed files with 803 additions and 194 deletions.
22 changes: 21 additions & 1 deletion libs/client-api/src/http_view.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use client_api_entity::workspace_dto::{
CreatePageParams, CreateSpaceParams, Page, PageCollab, Space, UpdatePageParams, UpdateSpaceParams,
CreatePageDatabaseViewParams, CreatePageParams, CreateSpaceParams, Page, PageCollab, Space,
UpdatePageParams, UpdateSpaceParams,
};
use reqwest::Method;
use serde_json::json;
Expand Down Expand Up @@ -148,4 +149,23 @@ impl Client {
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}

pub async fn create_database_view(
&self,
workspace_id: Uuid,
view_id: &str,
params: &CreatePageDatabaseViewParams,
) -> Result<(), AppResponseError> {
let url = format!(
"{}/api/workspace/{}/page-view/{}/database-view",
self.base_url, workspace_id, view_id
);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(params)
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
}
6 changes: 6 additions & 0 deletions libs/shared-entity/src/dto/workspace_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ pub struct CreatePageParams {
pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatePageDatabaseViewParams {
pub layout: ViewLayout,
pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatePageParams {
pub name: String,
Expand Down
27 changes: 26 additions & 1 deletion src/api/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use crate::biz::workspace::ops::{
get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment,
};
use crate::biz::workspace::page_view::{
create_page, create_space, get_page_view_collab, move_page_to_trash,
create_database_view, create_page, create_space, get_page_view_collab, move_page_to_trash,
restore_all_pages_from_trash, restore_page_from_trash, update_page, update_page_collab_data,
update_space,
};
Expand Down Expand Up @@ -149,6 +149,10 @@ pub fn workspace_scope() -> Scope {
web::resource("/{workspace_id}/page-view/{view_id}/restore-from-trash")
.route(web::post().to(restore_page_from_trash_handler)),
)
.service(
web::resource("/{workspace_id}/page-view/{view_id}/database-view")
.route(web::post().to(post_page_database_view_handler)),
)
.service(
web::resource("/{workspace_id}/restore-all-pages-from-trash")
.route(web::post().to(restore_all_pages_from_trash_handler)),
Expand Down Expand Up @@ -1043,6 +1047,27 @@ async fn restore_all_pages_from_trash_handler(
Ok(Json(AppResponse::Ok()))
}

async fn post_page_database_view_handler(
user_uuid: UserUuid,
path: web::Path<(Uuid, String)>,
payload: Json<CreatePageDatabaseViewParams>,
state: Data<AppState>,
) -> Result<Json<AppResponse<()>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let (workspace_uuid, view_id) = path.into_inner();
create_database_view(
&state.pg_pool,
&state.collab_access_control_storage,
uid,
workspace_uuid,
&view_id,
&payload.layout,
payload.name.as_deref(),
)
.await?;
Ok(Json(AppResponse::Ok()))
}

async fn update_page_view_handler(
user_uuid: UserUuid,
path: web::Path<(Uuid, String)>,
Expand Down
251 changes: 251 additions & 0 deletions src/biz/collab/database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
use app_error::AppError;
use collab_database::{
database::{gen_database_group_id, gen_field_id},
entity::FieldType,
fields::{
date_type_option::DateTypeOption, default_field_settings_for_fields,
select_type_option::SingleSelectTypeOption, Field, TypeOptionData,
},
views::{
BoardLayoutSetting, CalendarLayoutSetting, DatabaseLayout, FieldSettingsByFieldIdMap, Group,
GroupSetting, GroupSettingMap, LayoutSettings,
},
};

pub struct LinkedViewDependencies {
pub layout_settings: LayoutSettings,
pub field_settings: FieldSettingsByFieldIdMap,
pub group_settings: Vec<GroupSettingMap>,
pub deps_fields: Vec<Field>,
}

pub fn resolve_dependencies_when_create_database_linked_view(
database_layout: DatabaseLayout,
fields: &[Field],
) -> Result<LinkedViewDependencies, AppError> {
match database_layout {
DatabaseLayout::Grid => resolve_grid_dependencies(fields),
DatabaseLayout::Board => resolve_board_dependencies(fields),
DatabaseLayout::Calendar => resolve_calendar_dependencies(fields),
}
}

fn resolve_grid_dependencies(fields: &[Field]) -> Result<LinkedViewDependencies, AppError> {
Ok(LinkedViewDependencies {
layout_settings: LayoutSettings::default(),
field_settings: default_field_settings_for_fields(fields, DatabaseLayout::Grid),
group_settings: vec![],
deps_fields: vec![],
})
}

fn resolve_board_dependencies(
original_fields: &[Field],
) -> Result<LinkedViewDependencies, AppError> {
let database_layout = DatabaseLayout::Board;
let (group_field, all_fields, deps_fields) = match original_fields
.iter()
.find(|f| FieldType::from(f.field_type).can_be_group())
{
Some(field) => (field.clone(), original_fields.to_vec(), vec![]),
None => {
let card_status_field = create_card_status_field();
let mut fields = original_fields.to_vec();
fields.push(card_status_field.clone());
(card_status_field.clone(), fields, vec![card_status_field])
},
};
let field_settings = default_field_settings_for_fields(&all_fields, database_layout);
let group_ids = match FieldType::from(group_field.field_type) {
FieldType::SingleSelect => {
let mut group_ids = vec![group_field.id.clone()];
let single_select_type_option_ids = single_select_type_option_ids_from_field(&group_field)?;
group_ids.extend(single_select_type_option_ids);
Ok(group_ids)
},
FieldType::Checklist => Ok(vec!["Yes".to_string(), "No".to_string()]),
_ => Err(AppError::Internal(anyhow::anyhow!(
"invalid dep field for board layout"
))),
}?;

let groups = group_ids.iter().map(|id| Group::new(id.clone())).collect();
let group_settings: Vec<GroupSettingMap> = vec![GroupSetting {
id: gen_database_group_id(),
field_id: group_field.id.clone(),
field_type: group_field.field_type,
groups,
content: Default::default(),
}
.into()];

let mut layout_settings = LayoutSettings::default();
layout_settings.insert(database_layout, BoardLayoutSetting::new().into());
Ok(LinkedViewDependencies {
layout_settings,
field_settings,
group_settings,
deps_fields,
})
}

fn single_select_type_option_ids_from_field(field: &Field) -> Result<Vec<String>, AppError> {
let type_option_data: Option<&TypeOptionData> =
field.type_options.get(&FieldType::SingleSelect.to_string());
match type_option_data {
Some(type_option_data) => {
let single_select_type_option = SingleSelectTypeOption::from(type_option_data.to_owned());
let single_select_type_option_ids: Vec<String> = single_select_type_option
.options
.iter()
.map(|option| option.id.clone())
.collect();
Ok(single_select_type_option_ids)
},
None => Err(AppError::Internal(anyhow::anyhow!(
"invalid field for single select type options",
))),
}
}

fn resolve_calendar_dependencies(fields: &[Field]) -> Result<LinkedViewDependencies, AppError> {
let database_layout = DatabaseLayout::Calendar;
let (date_time_field, all_fields, deps_fields) = match fields
.iter()
.find(|f| FieldType::from(f.field_type) == FieldType::DateTime)
{
Some(field) => {
let all_fields = fields.to_vec();
(field.clone(), all_fields, vec![])
},
None => {
let date_field = create_date_field();
let mut all_fields = fields.to_vec();
all_fields.push(date_field.clone());
(date_field.clone(), all_fields, vec![date_field])
},
};
let field_settings = default_field_settings_for_fields(&all_fields, database_layout);
let mut layout_settings = LayoutSettings::default();
let layout_setting = CalendarLayoutSetting::new(date_time_field.id.clone()).into();
layout_settings.insert(database_layout, layout_setting);
Ok(LinkedViewDependencies {
layout_settings,
field_settings,
group_settings: vec![],
deps_fields,
})
}

fn create_date_field() -> Field {
let field_type = FieldType::DateTime;
let default_date_type_option = DateTypeOption::default();
let field_id = gen_field_id();
Field::new(field_id, "Date".to_string(), field_type.into(), false)
.with_type_option_data(field_type, default_date_type_option.into())
}

fn create_card_status_field() -> Field {
let field_type = FieldType::SingleSelect;
let default_select_type_option = SingleSelectTypeOption::default();
let field_id = gen_field_id();
Field::new(field_id, "Status".to_string(), field_type.into(), false)
.with_type_option_data(field_type, default_select_type_option.into())
}

#[cfg(test)]
mod tests {
use collab_database::{
fields::select_type_option::{SelectOption, SelectOptionColor},
views::LayoutSetting,
};

use super::*;

#[test]
fn test_resolve_dependencies_when_create_database_linked_view_grid() {
let database_layout = DatabaseLayout::Grid;
let fields: Vec<Field> = vec![];
let dependencies =
resolve_dependencies_when_create_database_linked_view(database_layout, &fields).unwrap();
assert!(dependencies.deps_fields.is_empty());
let fields: Vec<Field> = vec![
Field::from_field_type("name", FieldType::RichText, true),
Field::from_field_type("description", FieldType::RichText, false),
];
let dependencies =
resolve_dependencies_when_create_database_linked_view(database_layout, &fields).unwrap();
assert!(dependencies.deps_fields.is_empty());
}

#[test]
fn test_resolve_dependencies_when_create_database_linked_view_board() {
let database_layout = DatabaseLayout::Board;
let fields: Vec<Field> = vec![];
let dependencies =
resolve_dependencies_when_create_database_linked_view(database_layout, &fields).unwrap();
assert_eq!(dependencies.deps_fields.len(), 1);
let deps_field = dependencies.deps_fields[0].clone();
assert_eq!(deps_field.field_type, FieldType::SingleSelect as i64);
assert_eq!(dependencies.group_settings.len(), 1);
let group_setting_map: GroupSettingMap = dependencies.group_settings[0].clone();
let group_setting = GroupSetting::try_from(group_setting_map).unwrap();
assert_eq!(group_setting.groups.len(), 1);
assert_eq!(group_setting.groups[0].id, deps_field.id);

let select_option = SelectOption::with_color("Done", SelectOptionColor::Purple);
let options = vec![select_option];
let card_status_option_ids: Vec<String> =
options.iter().map(|option| option.id.clone()).collect();
let mut card_status_options = SingleSelectTypeOption::default();
card_status_options.options.extend(options);
let mut card_status_field = Field::new(
gen_field_id(),
"Status".to_string(),
FieldType::SingleSelect.into(),
false,
);
card_status_field.type_options.insert(
FieldType::SingleSelect.to_string(),
card_status_options.into(),
);
let fields = vec![card_status_field.clone()];
let dependencies =
resolve_dependencies_when_create_database_linked_view(database_layout, &fields).unwrap();
assert!(dependencies.deps_fields.is_empty());
assert_eq!(dependencies.group_settings.len(), 1);
let group_setting_map: GroupSettingMap = dependencies.group_settings[0].clone();
let group_setting = GroupSetting::try_from(group_setting_map).unwrap();
assert_eq!(group_setting.groups.len(), 2);
assert_eq!(group_setting.groups[0].id, card_status_field.id);
assert_eq!(group_setting.groups[1].id, card_status_option_ids[0]);
}

#[test]
fn test_resolve_dependencies_when_create_database_linked_view_calendar() {
let database_layout = DatabaseLayout::Calendar;
let fields: Vec<Field> = vec![];
let dependencies =
resolve_dependencies_when_create_database_linked_view(database_layout, &fields).unwrap();
assert_eq!(dependencies.deps_fields.len(), 1);
assert_eq!(
dependencies.deps_fields[0].field_type,
FieldType::DateTime as i64
);
let date_field = Field::from_field_type("datetime", FieldType::DateTime, false);
let fields: Vec<Field> = vec![
Field::from_field_type("title", FieldType::RichText, true),
date_field.clone(),
];
let dependencies =
resolve_dependencies_when_create_database_linked_view(database_layout, &fields).unwrap();
assert!(dependencies.deps_fields.is_empty());
let layout_setting: LayoutSetting = dependencies
.layout_settings
.get(&DatabaseLayout::Calendar)
.unwrap()
.to_owned();
let calendar_layout_setting = CalendarLayoutSetting::from(layout_setting);
assert_eq!(calendar_layout_setting.field_id, date_field.id);
}
}
1 change: 1 addition & 0 deletions src/biz/collab/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod database;
pub mod folder_view;
pub mod ops;
pub mod publish_outline;
Loading

0 comments on commit b93f39d

Please sign in to comment.