Merge branch 'main' into stateless
This commit is contained in:
commit
c8dab0fb2c
|
|
@ -1,4 +1,4 @@
|
||||||
use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab};
|
use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab, UpdatePageParams};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use shared_entity::response::{AppResponse, AppResponseError};
|
use shared_entity::response::{AppResponse, AppResponseError};
|
||||||
|
|
@ -40,6 +40,25 @@ impl Client {
|
||||||
AppResponse::<()>::from_response(resp).await?.into_error()
|
AppResponse::<()>::from_response(resp).await?.into_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_workspace_page_view(
|
||||||
|
&self,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
view_id: String,
|
||||||
|
params: &UpdatePageParams,
|
||||||
|
) -> Result<(), AppResponseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/workspace/{}/page-view/{}",
|
||||||
|
self.base_url, workspace_id, view_id
|
||||||
|
);
|
||||||
|
let resp = self
|
||||||
|
.http_client_with_auth(Method::PATCH, &url)
|
||||||
|
.await?
|
||||||
|
.json(params)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
AppResponse::<()>::from_response(resp).await?.into_error()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_workspace_page_view(
|
pub async fn get_workspace_page_view(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: Uuid,
|
workspace_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
|
||||||
use collab_entity::{CollabType, EncodedCollab};
|
use collab_entity::{CollabType, EncodedCollab};
|
||||||
use database_entity::dto::{AFRole, AFWebUser, AFWorkspaceInvitationStatus, PublishInfo};
|
use database_entity::dto::{AFRole, AFWebUser, AFWorkspaceInvitationStatus, PublishInfo};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use std::{collections::HashMap, ops::Deref};
|
use std::{collections::HashMap, ops::Deref};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -133,6 +134,13 @@ pub struct CreatePageParams {
|
||||||
pub layout: ViewLayout,
|
pub layout: ViewLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdatePageParams {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Option<ViewIcon>,
|
||||||
|
pub extra: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PageCollabData {
|
pub struct PageCollabData {
|
||||||
pub encoded_collab: Vec<u8>,
|
pub encoded_collab: Vec<u8>,
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ use crate::biz::workspace::ops::{
|
||||||
get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment,
|
get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment,
|
||||||
};
|
};
|
||||||
use crate::biz::workspace::page_view::{
|
use crate::biz::workspace::page_view::{
|
||||||
create_page, get_page_view_collab, move_page_to_trash, update_page_collab_data,
|
create_page, get_page_view_collab, move_page_to_trash, update_page, update_page_collab_data,
|
||||||
};
|
};
|
||||||
use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta;
|
use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta;
|
||||||
use crate::domain::compression::{
|
use crate::domain::compression::{
|
||||||
|
|
@ -131,7 +131,8 @@ pub fn workspace_scope() -> Scope {
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{workspace_id}/page-view/{view_id}")
|
web::resource("/{workspace_id}/page-view/{view_id}")
|
||||||
.route(web::get().to(get_page_view_handler)),
|
.route(web::get().to(get_page_view_handler))
|
||||||
|
.route(web::patch().to(update_page_view_handler)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{workspace_id}/page-view/{view_id}/move-to-trash")
|
web::resource("/{workspace_id}/page-view/{view_id}/move-to-trash")
|
||||||
|
|
@ -917,6 +918,33 @@ async fn move_page_to_trash_handler(
|
||||||
Ok(Json(AppResponse::Ok()))
|
Ok(Json(AppResponse::Ok()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_page_view_handler(
|
||||||
|
user_uuid: UserUuid,
|
||||||
|
path: web::Path<(Uuid, String)>,
|
||||||
|
payload: Json<UpdatePageParams>,
|
||||||
|
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();
|
||||||
|
let icon = payload.icon.as_ref();
|
||||||
|
let extra = payload
|
||||||
|
.extra
|
||||||
|
.as_ref()
|
||||||
|
.map(|json_value| json_value.to_string());
|
||||||
|
update_page(
|
||||||
|
&state.pg_pool,
|
||||||
|
&state.collab_access_control_storage,
|
||||||
|
uid,
|
||||||
|
workspace_uuid,
|
||||||
|
&view_id,
|
||||||
|
&payload.name,
|
||||||
|
icon,
|
||||||
|
extra.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(AppResponse::Ok()))
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_page_view_handler(
|
async fn get_page_view_handler(
|
||||||
user_uuid: UserUuid,
|
user_uuid: UserUuid,
|
||||||
path: web::Path<(Uuid, String)>,
|
path: web::Path<(Uuid, String)>,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ use app_error::AppError;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use collab_folder::{Folder, SectionItem, ViewLayout as CollabFolderViewLayout};
|
use collab_folder::{Folder, SectionItem, ViewLayout as CollabFolderViewLayout};
|
||||||
use shared_entity::dto::workspace_dto::{
|
use shared_entity::dto::workspace_dto::{
|
||||||
FavoriteFolderView, FolderView, FolderViewMinimal, RecentFolderView, TrashFolderView, ViewLayout,
|
self, FavoriteFolderView, FolderView, FolderViewMinimal, RecentFolderView, TrashFolderView,
|
||||||
|
ViewLayout,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Return all folders belonging to a workspace, excluding private sections which the user does not have access to.
|
/// Return all folders belonging to a workspace, excluding private sections which the user does not have access to.
|
||||||
|
|
@ -275,3 +276,28 @@ pub fn to_dto_folder_view_miminal(collab_folder_view: &collab_folder::View) -> F
|
||||||
layout: to_dto_view_layout(&collab_folder_view.layout),
|
layout: to_dto_view_layout(&collab_folder_view.layout),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_folder_view_icon(icon: workspace_dto::ViewIcon) -> collab_folder::ViewIcon {
|
||||||
|
collab_folder::ViewIcon {
|
||||||
|
ty: to_folder_view_icon_type(icon.ty),
|
||||||
|
value: icon.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_folder_view_icon_type(icon: workspace_dto::IconType) -> collab_folder::IconType {
|
||||||
|
match icon {
|
||||||
|
workspace_dto::IconType::Emoji => collab_folder::IconType::Emoji,
|
||||||
|
workspace_dto::IconType::Url => collab_folder::IconType::Url,
|
||||||
|
workspace_dto::IconType::Icon => collab_folder::IconType::Icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_folder_view_layout(layout: workspace_dto::ViewLayout) -> collab_folder::ViewLayout {
|
||||||
|
match layout {
|
||||||
|
ViewLayout::Document => collab_folder::ViewLayout::Document,
|
||||||
|
ViewLayout::Grid => collab_folder::ViewLayout::Grid,
|
||||||
|
ViewLayout::Board => collab_folder::ViewLayout::Board,
|
||||||
|
ViewLayout::Calendar => collab_folder::ViewLayout::Calendar,
|
||||||
|
ViewLayout::Chat => collab_folder::ViewLayout::Chat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ use database::user::select_web_user_from_uid;
|
||||||
use database_entity::dto::{CollabParams, QueryCollab, QueryCollabParams, QueryCollabResult};
|
use database_entity::dto::{CollabParams, QueryCollab, QueryCollabParams, QueryCollabResult};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
use shared_entity::dto::workspace_dto::{FolderView, Page, PageCollab, PageCollabData, ViewLayout};
|
use shared_entity::dto::workspace_dto::{
|
||||||
|
FolderView, Page, PageCollab, PageCollabData, ViewIcon, ViewLayout,
|
||||||
|
};
|
||||||
use sqlx::{PgPool, Transaction};
|
use sqlx::{PgPool, Transaction};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -26,7 +28,7 @@ use yrs::Update;
|
||||||
|
|
||||||
use crate::api::metrics::AppFlowyWebMetrics;
|
use crate::api::metrics::AppFlowyWebMetrics;
|
||||||
use crate::biz::collab::folder_view::{
|
use crate::biz::collab::folder_view::{
|
||||||
parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout,
|
parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout, to_folder_view_icon,
|
||||||
};
|
};
|
||||||
use crate::biz::collab::{
|
use crate::biz::collab::{
|
||||||
folder_view::view_is_space,
|
folder_view::view_is_space,
|
||||||
|
|
@ -94,6 +96,31 @@ async fn add_new_view_to_folder(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_view_properties(
|
||||||
|
view_id: &str,
|
||||||
|
folder: &mut Folder,
|
||||||
|
name: &str,
|
||||||
|
icon: Option<&ViewIcon>,
|
||||||
|
extra: Option<impl AsRef<str>>,
|
||||||
|
) -> Result<FolderUpdate, AppError> {
|
||||||
|
let encoded_update = {
|
||||||
|
let mut txn = folder.collab.transact_mut();
|
||||||
|
let icon = icon.map(|icon| to_folder_view_icon(icon.clone()));
|
||||||
|
folder.body.views.update_view(&mut txn, view_id, |update| {
|
||||||
|
update
|
||||||
|
.set_name(name)
|
||||||
|
.set_icon(icon)
|
||||||
|
.set_extra_if_not_none(extra)
|
||||||
|
.done()
|
||||||
|
});
|
||||||
|
txn.encode_update_v1()
|
||||||
|
};
|
||||||
|
Ok(FolderUpdate {
|
||||||
|
updated_encoded_collab: folder_to_encoded_collab(folder)?,
|
||||||
|
encoded_updates: encoded_update,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn move_view_to_trash(view_id: &str, folder: &mut Folder) -> Result<FolderUpdate, AppError> {
|
async fn move_view_to_trash(view_id: &str, folder: &mut Folder) -> Result<FolderUpdate, AppError> {
|
||||||
let mut current_view_and_descendants = folder
|
let mut current_view_and_descendants = folder
|
||||||
.get_views_belong_to(view_id)
|
.get_views_belong_to(view_id)
|
||||||
|
|
@ -227,6 +254,35 @@ pub async fn move_page_to_trash(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn update_page(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
collab_storage: &CollabAccessControlStorage,
|
||||||
|
uid: i64,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
view_id: &str,
|
||||||
|
name: &str,
|
||||||
|
icon: Option<&ViewIcon>,
|
||||||
|
extra: Option<impl AsRef<str>>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let collab_origin = GetCollabOrigin::User { uid };
|
||||||
|
let mut folder =
|
||||||
|
get_latest_collab_folder(collab_storage, collab_origin, &workspace_id.to_string()).await?;
|
||||||
|
let folder_update = update_view_properties(view_id, &mut folder, name, icon, extra).await?;
|
||||||
|
let mut transaction = pg_pool.begin().await?;
|
||||||
|
insert_and_broadcast_workspace_folder_update(
|
||||||
|
uid,
|
||||||
|
workspace_id,
|
||||||
|
folder_update,
|
||||||
|
collab_storage,
|
||||||
|
&mut transaction,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_page_view_collab(
|
pub async fn get_page_view_collab(
|
||||||
pg_pool: &PgPool,
|
pg_pool: &PgPool,
|
||||||
collab_access_control_storage: &CollabAccessControlStorage,
|
collab_access_control_storage: &CollabAccessControlStorage,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ use database::publish::select_published_data_for_view_id;
|
||||||
use database::publish::select_published_metadata_for_view_id;
|
use database::publish::select_published_metadata_for_view_id;
|
||||||
use database_entity::dto::CollabParams;
|
use database_entity::dto::CollabParams;
|
||||||
use shared_entity::dto::publish_dto::{PublishDatabaseData, PublishViewInfo, PublishViewMetaData};
|
use shared_entity::dto::publish_dto::{PublishDatabaseData, PublishViewInfo, PublishViewMetaData};
|
||||||
use shared_entity::dto::workspace_dto;
|
|
||||||
use shared_entity::dto::workspace_dto::ViewLayout;
|
use shared_entity::dto::workspace_dto::ViewLayout;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
@ -42,6 +41,8 @@ use yrs::ArrayRef;
|
||||||
use yrs::Out;
|
use yrs::Out;
|
||||||
use yrs::{Map, MapRef};
|
use yrs::{Map, MapRef};
|
||||||
|
|
||||||
|
use crate::biz::collab::folder_view::to_folder_view_icon;
|
||||||
|
use crate::biz::collab::folder_view::to_folder_view_layout;
|
||||||
use crate::biz::collab::ops::get_latest_collab_encoded;
|
use crate::biz::collab::ops::get_latest_collab_encoded;
|
||||||
|
|
||||||
use super::ops::broadcast_update;
|
use super::ops::broadcast_update;
|
||||||
|
|
@ -1171,31 +1172,6 @@ fn add_to_view_info(acc: &mut HashMap<String, PublishViewInfo>, view_infos: &[Pu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_folder_view_icon(icon: workspace_dto::ViewIcon) -> collab_folder::ViewIcon {
|
|
||||||
collab_folder::ViewIcon {
|
|
||||||
ty: to_folder_view_icon_type(icon.ty),
|
|
||||||
value: icon.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_folder_view_icon_type(icon: workspace_dto::IconType) -> collab_folder::IconType {
|
|
||||||
match icon {
|
|
||||||
workspace_dto::IconType::Emoji => collab_folder::IconType::Emoji,
|
|
||||||
workspace_dto::IconType::Url => collab_folder::IconType::Url,
|
|
||||||
workspace_dto::IconType::Icon => collab_folder::IconType::Icon,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_folder_view_layout(layout: workspace_dto::ViewLayout) -> collab_folder::ViewLayout {
|
|
||||||
match layout {
|
|
||||||
ViewLayout::Document => collab_folder::ViewLayout::Document,
|
|
||||||
ViewLayout::Grid => collab_folder::ViewLayout::Grid,
|
|
||||||
ViewLayout::Board => collab_folder::ViewLayout::Board,
|
|
||||||
ViewLayout::Calendar => collab_folder::ViewLayout::Calendar,
|
|
||||||
ViewLayout::Chat => collab_folder::ViewLayout::Chat,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn collab_to_bin(collab: Collab, collab_type: CollabType) -> Result<Vec<u8>, AppError> {
|
async fn collab_to_bin(collab: Collab, collab_type: CollabType) -> Result<Vec<u8>, AppError> {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let bin = collab
|
let bin = collab
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ use client_api_test::{
|
||||||
use collab::{core::origin::CollabClient, preclude::Collab};
|
use collab::{core::origin::CollabClient, preclude::Collab};
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
use collab_folder::{CollabOrigin, Folder};
|
use collab_folder::{CollabOrigin, Folder};
|
||||||
use shared_entity::dto::workspace_dto::{CreatePageParams, ViewLayout};
|
use serde_json::json;
|
||||||
|
use shared_entity::dto::workspace_dto::{
|
||||||
|
CreatePageParams, IconType, UpdatePageParams, ViewIcon, ViewLayout,
|
||||||
|
};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -168,3 +171,80 @@ async fn move_page_to_trash() {
|
||||||
.iter()
|
.iter()
|
||||||
.any(|v| v.view.view_id == view_id_to_be_deleted.clone());
|
.any(|v| v.view.view_id == view_id_to_be_deleted.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_page() {
|
||||||
|
let registered_user = generate_unique_registered_user().await;
|
||||||
|
let mut app_client = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||||
|
let web_client = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||||
|
let workspace_id = app_client.workspace_id().await;
|
||||||
|
app_client.open_workspace_collab(&workspace_id).await;
|
||||||
|
app_client
|
||||||
|
.wait_object_sync_complete(&workspace_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let folder_view = web_client
|
||||||
|
.api_client
|
||||||
|
.get_workspace_folder(&workspace_id.to_string(), Some(2), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let general_space = &folder_view
|
||||||
|
.children
|
||||||
|
.into_iter()
|
||||||
|
.find(|v| v.name == "General")
|
||||||
|
.unwrap();
|
||||||
|
let view_id_to_be_updated = general_space.children[0].view_id.clone();
|
||||||
|
web_client
|
||||||
|
.api_client
|
||||||
|
.update_workspace_page_view(
|
||||||
|
Uuid::parse_str(&workspace_id).unwrap(),
|
||||||
|
view_id_to_be_updated.clone(),
|
||||||
|
&UpdatePageParams {
|
||||||
|
name: "New Name".to_string(),
|
||||||
|
icon: Some(ViewIcon {
|
||||||
|
ty: IconType::Emoji,
|
||||||
|
value: "🚀".to_string(),
|
||||||
|
}),
|
||||||
|
extra: Some(json!({"key": "value"})),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Wait for websocket to receive update
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
let lock = app_client
|
||||||
|
.collabs
|
||||||
|
.get(&workspace_id)
|
||||||
|
.unwrap()
|
||||||
|
.collab
|
||||||
|
.read()
|
||||||
|
.await;
|
||||||
|
let collab: &Collab = (*lock).borrow();
|
||||||
|
let collab_type = CollabType::Folder;
|
||||||
|
let encoded_collab = collab
|
||||||
|
.encode_collab_v1(|collab| collab_type.validate_require_data(collab))
|
||||||
|
.unwrap();
|
||||||
|
let uid = app_client.uid().await;
|
||||||
|
let folder = Folder::from_collab_doc_state(
|
||||||
|
uid,
|
||||||
|
CollabOrigin::Client(CollabClient::new(uid, app_client.device_id.clone())),
|
||||||
|
encoded_collab.into(),
|
||||||
|
&workspace_id,
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let updated_view = folder.get_view(&view_id_to_be_updated).unwrap();
|
||||||
|
assert_eq!(updated_view.name, "New Name");
|
||||||
|
assert_eq!(
|
||||||
|
updated_view.icon,
|
||||||
|
Some(collab_folder::ViewIcon {
|
||||||
|
ty: collab_folder::IconType::Emoji,
|
||||||
|
value: "🚀".to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
updated_view.extra,
|
||||||
|
Some(json!({"key": "value"}).to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue