Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: API to add database view #1042

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 0 additions & 2 deletions services/appflowy-worker/src/import_worker/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ use redis::{AsyncCommands, RedisResult, Value};
use database::pg_row::AFImportTask;
use serde::{Deserialize, Serialize};
use serde_json::from_str;
use sqlx::types::chrono;
use sqlx::types::chrono::{DateTime, TimeZone, Utc};
use sqlx::PgPool;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -912,7 +911,6 @@ async fn process_unzip_file(
let mut collab_params_list = vec![];
let mut database_view_ids_by_database_id: HashMap<String, Vec<String>> = HashMap::new();
let mut orphan_view_ids = HashSet::new();
let timestamp = chrono::Utc::now().timestamp();

// 3. Collect all collabs and resources
let mut stream = imported.into_collab_stream().await;
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 @@ -1045,6 +1049,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
Loading