first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+6 -5
View File
@@ -1,10 +1,11 @@
use super::{CloudObject, Space};
use crate::{
drive::{folders::CloudFolder, items::WarpDriveItemId, CloudObjectTypeAndId},
ui_components::breadcrumb::Breadcrumb,
};
use galaxyui::AppContext;
use super::{CloudObject, Space};
use crate::drive::folders::CloudFolder;
use crate::drive::items::WarpDriveItemId;
use crate::drive::CloudObjectTypeAndId;
use crate::ui_components::breadcrumb::Breadcrumb;
// Encapsulates an object that can contain other objects, and keeps
// information necessary to show breadcrumbs.
#[derive(Clone, Debug)]
@@ -1,14 +1,13 @@
use galaxyui::elements::{Container, Element, MouseStateHandle, Text};
use galaxyui::fonts::{Properties, Style, Weight};
use galaxyui::platform::Cursor;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::{elements::Element, AppContext, View};
use galaxyui::{Entity, SingletonEntity, TypedActionView, ViewContext};
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use crate::appearance::Appearance;
use crate::ui_components::buttons::close_button;
use crate::ui_components::dialog::{dialog_styles, Dialog};
use galaxyui::elements::{Container, MouseStateHandle, Text};
use galaxyui::ui_components::components::UiComponent;
const EDIT_ANYWAY_CTA_LABEL: &str = "Edit anyway";
const CANCEL_CTA_LABEL: &str = "Cancel";
File diff suppressed because it is too large Load Diff
+68 -171
View File
@@ -1,189 +1,86 @@
use std::collections::HashMap;
use chrono::{DateTime, Duration, Utc};
pub use cloud_object_client::{
ObjectAction, ObjectActionHistory, ObjectActionSubtype, ObjectActionType,
};
use galaxyui::{Entity, ModelContext, SingletonEntity};
use crate::{
persistence::model::PersistedObjectAction,
server::ids::{parse_sqlite_id_to_uid, HashedSqliteId, ObjectUid},
};
use crate::server::ids::{HashedSqliteId, ObjectUid};
pub enum ObjectActionsEvent {}
/// The type of action that occurred on an object, such as an execution, selection, so on
/// and so forth.
#[derive(Clone, Debug, PartialEq)]
pub enum ObjectActionType {
Execute,
}
#[cfg(not(target_family = "wasm"))]
pub fn object_action_from_persisted(
other: crate::persistence::model::PersistedObjectAction,
) -> Result<ObjectAction, ()> {
// Each persisted object action is either a single action or a bundled action.
// If there's any inconsistencies from the SQL row, we return an error.
let action_subtype = if let Some(count) = other.count {
let oldest_timestamp = other
.oldest_timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
let latest_timestamp = other
.latest_timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
// In order to convert from a graphql type and from a SQLite read, the action type
// implements to_string().
//
// Temporarily suppress clippy warnings about the `ToString` impl until we
// move `ObjectType` away from using `std::fmt::Display` for serialization.
#[allow(clippy::to_string_trait_impl)]
impl ToString for ObjectActionType {
fn to_string(&self) -> String {
match self {
ObjectActionType::Execute => String::from("EXECUTE"),
// When the db row is a bundled action, the processed_at_timestamp field refers
// to the latest processed_at_timestamp in the bundle. Because bundled actions come
// from the server, this is a value, not an option.
let latest_processed_at_timestamp = other
.processed_at_timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
ObjectActionSubtype::BundledActions {
count,
oldest_timestamp,
latest_timestamp,
latest_processed_at_timestamp,
}
}
}
} else {
let timestamp = other
.timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
let pending = other.pending.ok_or(())?;
impl ObjectActionType {
fn singular(&self) -> String {
match self {
ObjectActionType::Execute => "run".to_string(),
// The processed_at_timestamp is still None when the action hasn't been synced.
let processed_at_timestamp = other
.processed_at_timestamp
.as_ref()
.map(|time| time.and_utc());
ObjectActionSubtype::SingleAction {
timestamp,
data: other.data,
pending,
processed_at_timestamp,
}
}
};
fn plural(&self) -> String {
match self {
ObjectActionType::Execute => "runs".to_string(),
}
}
}
// The object_sync_id stored in SQLite is the hashed id that's used to index into the ObjectActions
// model.
let hashed_object_id = other.hashed_object_id;
let action_type = match other.action.as_str() {
s if s == ObjectActionType::Execute.to_string() => ObjectActionType::Execute,
_ => return Err(()),
};
/// We track object actions, both those that have been sent to the server and not, through this
/// type. A single ObjectAction represents an object_id, action pair and a subtype that contains data
/// about the action(s). Each ObjectAction either represents one action or a summary of identical actions
/// that occurred at different times. We summarize old actions in order to save memory footprint on the client.
#[derive(Clone, Debug, PartialEq)]
pub struct ObjectAction {
pub action_type: ObjectActionType,
pub uid: ObjectUid,
pub hashed_sqlite_id: HashedSqliteId,
// This action either represents one action or a consolidation of multiple actions.
pub action_subtype: ObjectActionSubtype,
}
// NOTE: This is needed since we only store the sqlite hash, but we need the uid (the second part of the hash)
// to index into CloudModel and store the object actions in memory.
let uid = crate::server::ids::parse_sqlite_id_to_uid(hashed_object_id.clone())?;
impl ObjectAction {
pub fn is_pending(&self) -> bool {
match self.action_subtype {
ObjectActionSubtype::SingleAction { pending, .. } => pending,
_ => false,
}
}
}
impl TryFrom<PersistedObjectAction> for ObjectAction {
type Error = ();
fn try_from(other: PersistedObjectAction) -> Result<Self, Self::Error> {
// Each persisted object action is either a single action or a bundled action.
// If there's any inconsistencies from the SQL row, we return an error.
let action_subtype = if let Some(count) = other.count {
let oldest_timestamp = other
.oldest_timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
let latest_timestamp = other
.latest_timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
// When the db row is a bundled action, the processed_at_timestamp field refers
// to the latest processed_at_timestamp in the bundle. Because bundled actions come
// from the server, this is a value, not an option.
let latest_processed_at_timestamp = other
.processed_at_timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
ObjectActionSubtype::BundledActions {
count,
oldest_timestamp,
latest_timestamp,
latest_processed_at_timestamp,
}
} else {
let timestamp = other
.timestamp
.as_ref()
.map(|time| time.and_utc())
.ok_or(())?;
let pending = other.pending.ok_or(())?;
// The processed_at_timestamp is still None when the action hasn't been synced.
let processed_at_timestamp = other
.processed_at_timestamp
.as_ref()
.map(|time| time.and_utc());
ObjectActionSubtype::SingleAction {
timestamp,
data: other.data,
pending,
processed_at_timestamp,
}
};
// The object_sync_id stored in SQLite is the hashed id that's used to index into the ObjectActions
// model.
let hashed_object_id = other.hashed_object_id;
let action_type = match other.action.as_str() {
s if s == ObjectActionType::Execute.to_string() => ObjectActionType::Execute,
_ => return Err(()),
};
// NOTE: This is needed since we only store the sqlite hash, but we need the uid (the second part of the hash)
// to index into CloudModel and store the object actions in memory.
let uid = parse_sqlite_id_to_uid(hashed_object_id.clone())?;
Ok(ObjectAction {
uid: uid.to_string(),
hashed_sqlite_id: hashed_object_id,
action_type,
action_subtype,
})
}
}
/// The server communicates the action history of an object via an "ObjectActionHistory" type that
/// contains the uid, a list of actions (single or bundled), and the timestamp of the most recent action
/// (which is redundant from the list of actions). We use this type to convert from the graphql layer into
/// an identical type the sync_queue and update_manager can pass around.
#[derive(Clone, Debug, PartialEq)]
pub struct ObjectActionHistory {
pub uid: ObjectUid,
pub hashed_sqlite_id: HashedSqliteId,
pub latest_processed_at_timestamp: DateTime<Utc>,
pub actions: Vec<ObjectAction>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ObjectActionSubtype {
SingleAction {
// When the action occurred.
timestamp: DateTime<Utc>,
// When the action was processed by the server (used to order actions against eachother).
// None if the action has not been synced.
processed_at_timestamp: Option<DateTime<Utc>>,
// A JSON representation of anything else we might want to track about the action.
// For example, the exit code of a workflow execution.
data: Option<String>,
// Whether or not this action has been successfully synced to the server.
pending: bool,
},
BundledActions {
// The number of distinct actions that are coalesced into one entry here.
count: i32,
// The timestamp of the oldest action within this bundle.
oldest_timestamp: DateTime<Utc>,
// The timestamp of the most recent action within the bundle.
latest_timestamp: DateTime<Utc>,
// The most recent processed_at timestamp contained in the bundle (used to order actions and determine
// how up-to-date the client's actions are.)
latest_processed_at_timestamp: DateTime<Utc>,
},
Ok(ObjectAction {
uid: uid.to_string(),
hashed_sqlite_id: hashed_object_id,
action_type,
action_subtype,
})
}
/// A singleton model representing the actions that have occurred on a per-object basis. These
+1 -2
View File
@@ -1,9 +1,8 @@
use chrono::{Duration, Utc};
use galaxyui::App;
use super::{ObjectAction, ObjectActionSubtype, ObjectActionType, ObjectActions};
use chrono::{Duration, Utc};
#[test]
fn test_object_actions_daily() {
App::test((), |mut app| async move {
@@ -1,25 +1,26 @@
use std::{fmt::Debug, sync::Arc};
use std::fmt::Debug;
use std::sync::Arc;
use crate::server::cloud_objects::update_manager::InitiatedBy;
use crate::{
appearance::Appearance,
cloud_object::{
CloudModelType, CloudObject, CloudObjectEventEntrypoint, CreateCloudObjectResult,
CreateObjectRequest, GenericCloudObject, GenericServerObject, GenericStringObjectFormat,
GenericStringObjectUniqueKey, ObjectType, Revision, ServerCloudObject,
UpdateCloudObjectResult,
},
drive::{items::WarpDriveItem, CloudObjectTypeAndId},
persistence::ModelEvent,
server::{
ids::{ObjectUid, ServerId, SyncId},
server_api::object::ObjectClient,
sync_queue::{QueueItem, SerializedModel},
},
};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use cloud_objects::cloud_object::CloudObjectUpsertParams;
// Re-exported from cloud_objects.
pub use cloud_objects::cloud_object::{GenericStringModel, Serializer};
pub use warp_server_client::ids::GenericStringObjectId;
use crate::appearance::Appearance;
use crate::cloud_object::{
CloudModelType, CloudObject, CloudObjectEventEntrypoint, CreateCloudObjectResult,
CreateObjectRequest, GenericCloudObject, GenericServerObject, GenericStringObjectFormat,
GenericStringObjectUniqueKey, ObjectType, Revision, UpdateCloudObjectResult,
};
use crate::drive::items::WarpDriveItem;
use crate::drive::CloudObjectTypeAndId;
use crate::persistence::ModelEvent;
use crate::server::cloud_objects::update_manager::InitiatedBy;
use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::object::ObjectClient;
use crate::server::sync_queue::{QueueItem, SerializedModel};
/// A trait that generic string-based objects should implement.
pub trait CloudStringObject: CloudObject + Send + Sync {
@@ -108,9 +109,6 @@ pub trait StringModel: Clone + Debug + PartialEq + Send + Sync + 'static {
object: &Self::CloudObjectType,
) -> QueueItem;
/// Returns a new instance from a server update, or None if the update should be ignored.
fn new_from_server_update(&self, server_cloud_object: &ServerCloudObject) -> Option<Self>;
/// Returns whether this model type should clear on a unique key conflict.
fn should_clear_on_unique_key_conflict(&self) -> bool {
false
@@ -122,27 +120,6 @@ pub trait StringModel: Clone + Debug + PartialEq + Send + Sync + 'static {
fn uniqueness_key(&self) -> Option<GenericStringObjectUniqueKey>;
}
/// A serializer goes from a model to a string and back.
pub trait Serializer<M>: Debug + Clone + 'static {
fn serialize(model: &M) -> SerializedModel;
fn deserialize_owned(serialized: &str) -> Result<M>
where
Self: Sized;
}
/// A `GenericStringModel` is a generic implementation of model types that can serialize to/from string.
/// given a particular serializer.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct GenericStringModel<M, S>
where
M: StringModel<
CloudObjectType = GenericCloudObject<GenericStringObjectId, GenericStringModel<M, S>>,
>,
S: Serializer<M>,
{
pub string_model: M,
}
impl<M, S> CloudStringObject for GenericCloudObject<GenericStringObjectId, GenericStringModel<M, S>>
where
M: StringModel<
@@ -159,7 +136,7 @@ where
}
fn serialized(&self) -> SerializedModel {
self.model.serialized()
self.model().serialized()
}
fn clone_box(&self) -> Box<dyn CloudStringObject> {
@@ -207,8 +184,9 @@ where
self.string_model.set_display_name(name);
}
fn upsert_event(&self, object: &GenericCloudObject<GenericStringObjectId, Self>) -> ModelEvent {
let object = object as &dyn CloudStringObject;
fn upsert_event(params: CloudObjectUpsertParams<Self>) -> ModelEvent {
let object = GenericCloudObject::<GenericStringObjectId, Self>::from(params);
let object = &object as &dyn CloudStringObject;
ModelEvent::UpsertGenericStringObject {
object: CloudStringObject::clone_box(object),
}
@@ -230,11 +208,16 @@ where
self.string_model.can_export()
}
fn bulk_upsert_event(
objects: &[GenericCloudObject<GenericStringObjectId, Self>],
) -> ModelEvent {
fn bulk_upsert_event(objects: Vec<CloudObjectUpsertParams<Self>>) -> ModelEvent {
ModelEvent::UpsertGenericStringObjects(
objects.iter().map(CloudStringObject::clone_box).collect(),
objects
.into_iter()
.map(|params| {
Box::new(GenericCloudObject::<GenericStringObjectId, Self>::from(
params,
)) as Box<dyn CloudStringObject>
})
.collect(),
)
}
@@ -250,7 +233,7 @@ where
owner: object.permissions.owner,
id: client_id,
title: None,
serialized_model: Some(object.model.serialized().into()),
serialized_model: Some(object.model().serialized().into()),
initial_folder_id: object.metadata.folder_id,
entrypoint,
initiated_by,
@@ -276,12 +259,6 @@ where
true
}
fn new_from_server_update(&self, server_cloud_object: &ServerCloudObject) -> Option<Self> {
self.string_model
.new_from_server_update(server_cloud_object)
.map(Self::new)
}
fn serialized(&self) -> SerializedModel {
S::serialize(&self.string_model)
}
@@ -349,42 +326,3 @@ where
self.string_model.to_warp_drive_item(id, appearance, object)
}
}
impl<M, S> GenericStringModel<M, S>
where
M: StringModel<
CloudObjectType = GenericCloudObject<GenericStringObjectId, GenericStringModel<M, S>>,
>,
S: Serializer<M>,
{
pub fn deserialize_owned(serialized: &str) -> Result<Self> {
S::deserialize_owned(serialized).map(Self::new)
}
pub fn new(model: M) -> Self {
Self {
string_model: model,
}
}
pub fn json_model(&self) -> &M {
&self.string_model
}
}
/// Object id type that is common for all generic string objects.
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct GenericStringObjectId(ServerId);
crate::server_id_traits! { GenericStringObjectId, "GenericStringObject" }
impl From<GenericStringObjectId> for SyncId {
fn from(id: GenericStringObjectId) -> Self {
Self::ServerId(id.into())
}
}
impl GenericStringObjectId {
pub fn uid(&self) -> ObjectUid {
self.0.uid()
}
}
+9 -3
View File
@@ -1,8 +1,10 @@
use serde::{de::DeserializeOwned, Serialize};
use crate::{cloud_object::JsonObjectType, server::sync_queue::SerializedModel};
use cloud_objects::cloud_object::GenericStringObjectFormat;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::generic_string_model::{Serializer, StringModel};
use crate::cloud_object::JsonObjectType;
use crate::server::sync_queue::SerializedModel;
/// A `JsonModel` is a string model that can be serialized to and deserialized from JSON.
pub trait JsonModel: StringModel + Serialize + DeserializeOwned + 'static {
@@ -10,10 +12,14 @@ pub trait JsonModel: StringModel + Serialize + DeserializeOwned + 'static {
fn json_object_type() -> JsonObjectType;
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Default)]
pub struct JsonSerializer;
impl<M: JsonModel> Serializer<M> for JsonSerializer {
fn model_format() -> GenericStringObjectFormat {
M::model_format()
}
fn serialize(model: &M) -> SerializedModel {
SerializedModel::new(serde_json::to_string(model).expect("model should serialize"))
}
@@ -1,67 +1,49 @@
use chrono::Utc;
use galaxyui::{App, ModelHandle};
use lazy_static::lazy_static;
use settings::SyncToCloud;
use std::sync::Arc;
use std::time::Duration;
use chrono::Utc;
use cloud_object_client::MockObjectClient;
use lazy_static::lazy_static;
use mockall::Sequence;
use rand::Rng;
use settings::{RespectUserSyncSetting, SyncToCloud};
use galaxyui::{App, ModelHandle};
use super::*;
use crate::auth::auth_manager::AuthManager;
use crate::auth::user::TEST_USER_UID;
use crate::auth::AuthStateProvider;
use crate::auth::UserUid;
use crate::auth::{AuthStateProvider, UserUid};
use crate::cloud_object::model::actions::ObjectActions;
use crate::cloud_object::model::generic_string_model::GenericStringModel;
use crate::cloud_object::model::view::CloudViewModel;
use crate::cloud_object::model::view::EditorState;
use crate::cloud_object::model::view::UpdateTimestamp;
use crate::cloud_object::model::view::EDITOR_TIMEOUT_DURATION_MINUTES;
use crate::cloud_object::CloudObjectMetadata;
use crate::cloud_object::CloudObjectPermissions;
use crate::cloud_object::CloudObjectStatuses;
use crate::cloud_object::CloudObjectSyncStatus;
use crate::cloud_object::NumInFlightRequests;
use crate::cloud_object::ObjectIdType;
use crate::cloud_object::Owner;
use crate::cloud_object::ServerMetadata;
use crate::cloud_object::ServerPermissions;
use crate::drive::folders::CloudFolderModel;
use crate::drive::folders::FolderId;
use crate::cloud_object::model::view::{
CloudViewModel, EditorState, UpdateTimestamp, EDITOR_TIMEOUT_DURATION_MINUTES,
};
use crate::cloud_object::{
CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses, CloudObjectSyncStatus,
NumInFlightRequests, ObjectIdType, Owner, ServerMetadata, ServerPermissions,
};
use crate::drive::folders::{CloudFolderModel, FolderId};
use crate::drive::DriveIndexVariant;
use crate::features::FeatureFlag;
use crate::notebooks::CloudNotebookModel;
use crate::notebooks::NotebookId;
use crate::notebooks::{CloudNotebookModel, NotebookId};
use crate::server::cloud_objects::listener::ObjectUpdateMessage;
use crate::server::cloud_objects::update_manager::InitialLoadResponse;
use crate::server::ids::ServerId;
use crate::server::ids::ServerIdAndType;
use crate::server::ids::{ServerId, ServerIdAndType};
use crate::server::server_api::object::ObjectClient;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
use crate::settings::init_and_register_user_preferences;
use crate::settings::Preference;
use crate::settings::{init_and_register_user_preferences, Preference};
use crate::system::SystemStats;
use crate::workflows::CloudWorkflowModel;
use crate::workspaces::team::Team;
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_profiles::UserProfiles;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::workspaces::workspace::Workspace;
use crate::workflows::CloudWorkflowModel;
use crate::workspaces::workspace::WorkspaceUid;
use crate::NetworkStatus;
use crate::UpdateManager;
use mockall::Sequence;
use rand::Rng;
use std::sync::Arc;
use std::time::Duration;
use super::*;
#[cfg(test)]
use crate::server::server_api::object::MockObjectClient;
#[cfg(test)]
use crate::server::server_api::team::MockTeamClient;
#[cfg(test)]
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::workspaces::workspace::{Workspace, WorkspaceUid};
use crate::{NetworkStatus, UpdateManager};
fn create_cloud_model(
app: &mut App,
@@ -173,14 +155,16 @@ fn mock_server_workflows(
number_of_workflows: i64,
) -> Vec<ServerWorkflow> {
(0..number_of_workflows)
.map(|idx| ServerWorkflow {
id: SyncId::ServerId((start_id + idx).into()),
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
model: CloudWorkflowModel::new(Workflow::new(
format!("w{}", start_id + idx),
format!("c{}", start_id + idx),
)),
.map(|idx| {
ServerWorkflow::new(
SyncId::ServerId((start_id + idx).into()),
CloudWorkflowModel::new(Workflow::new(
format!("w{}", start_id + idx),
format!("c{}", start_id + idx),
)),
mock_server_metadata(),
mock_server_permissions(owner),
)
})
.collect()
}
@@ -194,11 +178,13 @@ fn mock_random_folders(start_id: i64, owner: Owner) -> Vec<ServerFolder> {
fn mock_server_folders(start_id: i64, owner: Owner, number_of_folders: i64) -> Vec<ServerFolder> {
(0..number_of_folders)
.map(|idx| ServerFolder {
id: SyncId::ServerId((start_id + idx).into()),
model: CloudFolderModel::new(&format!("f{}", start_id + idx), false),
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
.map(|idx| {
ServerFolder::new(
SyncId::ServerId((start_id + idx).into()),
CloudFolderModel::new(&format!("f{}", start_id + idx), false),
mock_server_metadata(),
mock_server_permissions(owner),
)
})
.collect()
}
@@ -206,50 +192,50 @@ fn mock_server_folders(start_id: i64, owner: Owner, number_of_folders: i64) -> V
fn mock_server_notebooks() -> Vec<ServerNotebook> {
let owner = Owner::mock_current_user();
vec![
ServerNotebook {
id: SyncId::ServerId(1.into()),
model: CloudNotebookModel {
ServerNotebook::new(
SyncId::ServerId(1.into()),
CloudNotebookModel {
title: "t1".to_string(),
data: "d1".to_string(),
ai_document_id: None,
conversation_id: None,
},
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
},
ServerNotebook {
id: SyncId::ServerId(2.into()),
model: CloudNotebookModel {
mock_server_metadata(),
mock_server_permissions(owner),
),
ServerNotebook::new(
SyncId::ServerId(2.into()),
CloudNotebookModel {
title: "t2".to_string(),
data: "d2".to_string(),
ai_document_id: None,
conversation_id: None,
},
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
},
ServerNotebook {
id: SyncId::ServerId(3.into()),
model: CloudNotebookModel {
mock_server_metadata(),
mock_server_permissions(owner),
),
ServerNotebook::new(
SyncId::ServerId(3.into()),
CloudNotebookModel {
title: "t3".to_string(),
data: "d3".to_string(),
ai_document_id: None,
conversation_id: None,
},
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
},
ServerNotebook {
id: SyncId::ServerId(4.into()),
model: CloudNotebookModel {
mock_server_metadata(),
mock_server_permissions(owner),
),
ServerNotebook::new(
SyncId::ServerId(4.into()),
CloudNotebookModel {
title: "t4".to_string(),
data: "d4".to_string(),
ai_document_id: None,
conversation_id: None,
},
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
},
mock_server_metadata(),
mock_server_permissions(owner),
),
]
}
@@ -1049,18 +1035,18 @@ fn test_collapse_all_in_location() {
let folder_1 = folder_from_cloud_model(model, folder_1_id);
let folder_4 = folder_from_cloud_model(model, folder_4_id);
let folder_5 = folder_from_cloud_model(model, folder_5_id);
assert!(!folder_1.model.is_open);
assert!(!folder_4.model.is_open);
assert!(!folder_5.model.is_open);
assert!(!folder_1.model().is_open);
assert!(!folder_4.model().is_open);
assert!(!folder_5.model().is_open);
// but the others are still open
let folder_2 = folder_from_cloud_model(model, folder_2_id);
let folder_3 = folder_from_cloud_model(model, folder_3_id);
let folder_6 = folder_from_cloud_model(model, folder_6_id);
let folder_7 = folder_from_cloud_model(model, folder_7_id);
assert!(folder_2.model.is_open);
assert!(folder_3.model.is_open);
assert!(folder_6.model.is_open);
assert!(folder_7.model.is_open);
assert!(folder_2.model().is_open);
assert!(folder_3.model().is_open);
assert!(folder_6.model().is_open);
assert!(folder_7.model().is_open);
model.collapse_all_in_location(
CloudObjectLocation::Space(Default::default()),
@@ -1075,13 +1061,13 @@ fn test_collapse_all_in_location() {
let folder_5 = folder_from_cloud_model(model, folder_5_id);
let folder_6 = folder_from_cloud_model(model, folder_6_id);
let folder_7 = folder_from_cloud_model(model, folder_7_id);
assert!(!folder_1.model.is_open);
assert!(!folder_2.model.is_open);
assert!(!folder_3.model.is_open);
assert!(!folder_4.model.is_open);
assert!(!folder_5.model.is_open);
assert!(!folder_6.model.is_open);
assert!(!folder_7.model.is_open);
assert!(!folder_1.model().is_open);
assert!(!folder_2.model().is_open);
assert!(!folder_3.model().is_open);
assert!(!folder_4.model().is_open);
assert!(!folder_5.model().is_open);
assert!(!folder_6.model().is_open);
assert!(!folder_7.model().is_open);
});
})
}
@@ -1146,19 +1132,19 @@ fn test_collapse_all_in_trash() {
// folders 1, 4 should be collapsed
let folder_1 = folder_from_cloud_model(model, folder_1_id);
let folder_4 = folder_from_cloud_model(model, folder_4_id);
assert!(!folder_1.model.is_open);
assert!(!folder_4.model.is_open);
assert!(!folder_1.model().is_open);
assert!(!folder_4.model().is_open);
// but the others, including folder 5, are still open
let folder_2 = folder_from_cloud_model(model, folder_2_id);
let folder_3 = folder_from_cloud_model(model, folder_3_id);
let folder_5 = folder_from_cloud_model(model, folder_5_id);
let folder_6 = folder_from_cloud_model(model, folder_6_id);
let folder_7 = folder_from_cloud_model(model, folder_7_id);
assert!(folder_2.model.is_open);
assert!(folder_3.model.is_open);
assert!(folder_5.model.is_open);
assert!(folder_6.model.is_open);
assert!(folder_7.model.is_open);
assert!(folder_2.model().is_open);
assert!(folder_3.model().is_open);
assert!(folder_5.model().is_open);
assert!(folder_6.model().is_open);
assert!(folder_7.model().is_open);
model.collapse_all_in_location(
CloudObjectLocation::Space(Default::default()),
@@ -1173,13 +1159,13 @@ fn test_collapse_all_in_trash() {
let folder_5 = folder_from_cloud_model(model, folder_5_id);
let folder_6 = folder_from_cloud_model(model, folder_6_id);
let folder_7 = folder_from_cloud_model(model, folder_7_id);
assert!(!folder_1.model.is_open);
assert!(!folder_2.model.is_open);
assert!(!folder_3.model.is_open);
assert!(!folder_4.model.is_open);
assert!(!folder_5.model.is_open);
assert!(!folder_6.model.is_open);
assert!(!folder_7.model.is_open);
assert!(!folder_1.model().is_open);
assert!(!folder_2.model().is_open);
assert!(!folder_3.model().is_open);
assert!(!folder_4.model().is_open);
assert!(!folder_5.model().is_open);
assert!(!folder_6.model().is_open);
assert!(!folder_7.model().is_open);
});
})
}
@@ -1335,15 +1321,15 @@ fn test_update_folder_timestamp_from_child_update() {
let new_ts = initial_ts + chrono::Duration::seconds(5);
receive_rtc_update(
ObjectUpdateMessage::ObjectContentChanged {
server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook {
id: SyncId::ServerId(notebook_id),
model: CloudNotebookModel {
server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook::new(
SyncId::ServerId(notebook_id),
CloudNotebookModel {
title: "Test Notebook".to_string(),
data: "test2".into(),
ai_document_id: None,
conversation_id: None,
},
metadata: ServerMetadata {
ServerMetadata {
uid: notebook_id,
revision: new_ts.into(),
metadata_last_updated_ts: new_ts.into(),
@@ -1354,8 +1340,8 @@ fn test_update_folder_timestamp_from_child_update() {
last_editor_uid: None,
current_editor_uid: None,
},
permissions: mock_server_permissions(Owner::mock_current_user()),
})),
mock_server_permissions(Owner::mock_current_user()),
))),
last_editor: None,
},
&mut app,
@@ -1466,15 +1452,15 @@ fn test_update_folder_timestamp_from_new_child() {
// Create a notebook inside the folder.
receive_rtc_update(
ObjectUpdateMessage::ObjectContentChanged {
server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook {
id: SyncId::ServerId(notebook_id),
model: CloudNotebookModel {
server_object: Box::new(ServerCloudObject::Notebook(ServerNotebook::new(
SyncId::ServerId(notebook_id),
CloudNotebookModel {
title: "Test Notebook".to_string(),
data: "test".to_string(),
ai_document_id: None,
conversation_id: None,
},
metadata: ServerMetadata {
ServerMetadata {
uid: notebook_id,
revision: t2.into(),
metadata_last_updated_ts: t2.into(),
@@ -1485,8 +1471,8 @@ fn test_update_folder_timestamp_from_new_child() {
last_editor_uid: None,
current_editor_uid: None,
},
permissions: mock_server_permissions(Owner::mock_current_user()),
})),
mock_server_permissions(Owner::mock_current_user()),
))),
last_editor: None,
},
&mut app,
+22 -22
View File
@@ -1,7 +1,18 @@
use std::collections::{HashMap, HashSet};
use std::sync::mpsc::SyncSender;
use chrono::{DateTime, Duration, Utc};
use itertools::Itertools;
use rand::Rng;
use galaxy_core::features::FeatureFlag;
use warp_graphql::scalars::time::ServerTimestamp;
use warpui::{AppContext, Entity, ModelContext, SingletonEntity};
use super::generic_string_model::GenericStringObjectId;
use crate::ai::execution_profiles::CloudAIExecutionProfile;
use crate::auth::AuthStateProvider;
use crate::cloud_object::{
CloudModelType, CloudObjectLocation, CloudObjectPermissions, GenericCloudObject,
CloudModelType, CloudObject, CloudObjectLocation, CloudObjectPermissions, GenericCloudObject,
GenericServerObject, GenericStringObjectFormat, JsonObjectType, ObjectIdType, ObjectType,
ObjectsToUpdate, Owner, Revision, RevisionAndLastEditor, ServerCloudObject, ServerCreationInfo,
ServerFolder, ServerMetadata, ServerNotebook, ServerPermissions, ServerWorkflow, Space,
@@ -21,20 +32,6 @@ use crate::workflows::workflow_enum::{CloudWorkflowEnum, CloudWorkflowEnumModel,
use crate::workflows::{CloudWorkflow, CloudWorkflowModel};
use crate::workspaces::user_workspaces::UserWorkspaces;
use galaxy_graphql::scalars::time::ServerTimestamp;
use itertools::Itertools;
use std::collections::{HashMap, HashSet};
use std::sync::mpsc::SyncSender;
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
use crate::cloud_object::CloudObject;
use chrono::{DateTime, Duration, Utc};
use galaxy_core::features::FeatureFlag;
use rand::Rng;
use super::generic_string_model::GenericStringObjectId;
// Equivalent to 24 hours
const MIN_MINUTES_UNTIL_NEXT_FORCE_REFRESH: i64 = 1440;
@@ -177,7 +174,7 @@ impl CloudModel {
///
/// We do NOT support
/// - Moving folders across spaces
/// - Transfering from team space to personal space
/// - Transferring from team space to personal space
/// - Moving directly into a folder across spaces
pub fn can_move_object_to_location(
&self,
@@ -547,7 +544,7 @@ impl CloudModel {
ctx: &mut ModelContext<Self>,
) {
if let Some(folder) = self.get_folder(&server_folder.id) {
server_folder.model.is_open = folder.model.is_open;
server_folder.model.is_open = folder.model().is_open;
}
self.upsert_from_server_object(server_folder, ctx);
@@ -893,13 +890,13 @@ impl CloudModel {
let is_open = match open_state {
FolderOpenState::Open => true,
FolderOpenState::Closed => false,
FolderOpenState::Reversed => !folder.model.is_open,
FolderOpenState::Reversed => !folder.model().is_open,
};
folder.set_model(CloudFolderModel {
is_open,
is_warp_pack: folder.model.is_warp_pack,
name: folder.model.name.clone(),
is_warp_pack: folder.model().is_warp_pack,
name: folder.model().name.clone(),
});
let folder_clone = folder.clone();
@@ -1783,7 +1780,10 @@ impl CloudModel {
if let Some(model_event_sender) = &self.model_event_sender {
if let Err(e) = model_event_sender.send(M::bulk_upsert_event(
objects_without_pending_changes.as_slice(),
objects_without_pending_changes
.iter()
.map(|object| object.upsert_params(object.object_type()))
.collect(),
)) {
log::error!("Error saving team objects to cache: {e:?}");
}
@@ -1826,5 +1826,5 @@ impl Entity for CloudModel {
impl SingletonEntity for CloudModel {}
#[cfg(test)]
#[path = "model_test.rs"]
#[path = "model_tests.rs"]
mod tests;
+21 -21
View File
@@ -1,27 +1,21 @@
use std::{cell::RefCell, collections::HashMap};
use std::cell::RefCell;
use std::collections::HashMap;
use chrono::{Duration, Utc};
use galaxy_graphql::scalars::time::ServerTimestamp;
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
use crate::{
auth::{AuthStateProvider, UserUid},
cloud_object::{CloudObject, CloudObjectLocation, Space},
drive::{
folders::CloudFolder,
sharing::{ContentEditability, SharingAccessLevel},
},
safe_info,
server::{
cloud_objects::update_manager::{
ObjectOperation, OperationSuccessType, UpdateManager, UpdateManagerEvent,
},
ids::{ObjectUid, SyncId},
},
workspaces::user_profiles::UserProfiles,
};
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use super::persistence::{CloudModel, CloudModelEvent};
use crate::auth::{AuthStateProvider, UserUid};
use crate::cloud_object::{CloudObject, CloudObjectLocation, Space};
use crate::drive::folders::CloudFolder;
use crate::drive::sharing::{ContentEditability, SharingAccessLevel};
use crate::safe_info;
use crate::server::cloud_objects::update_manager::{
ObjectOperation, OperationSuccessType, UpdateManager, UpdateManagerEvent,
};
use crate::server::ids::{ObjectUid, SyncId};
use crate::workspaces::user_profiles::UserProfiles;
pub const EDITOR_TIMEOUT_DURATION_MINUTES: i64 = 15;
@@ -55,7 +49,7 @@ impl Editor {
}
/// Singleton model for storing and querying the data and logic logic needed by various view, based on the information
/// stored in [CloudModel]. As a general, rule, any new API that requires logic beyond just retriving the raw value
/// stored in [CloudModel]. As a general, rule, any new API that requires logic beyond just retrieving the raw value
/// in [CloudModel], should be stored here. This includes logic such as object trashed status, the object current editor,
/// and object location.
///
@@ -309,7 +303,12 @@ impl CloudViewModel {
}
}
fn handle_cloud_model_event(&mut self, event: &CloudModelEvent, ctx: &mut ModelContext<Self>) {
fn handle_cloud_model_event(
&mut self,
_: ModelHandle<CloudModel>,
event: &CloudModelEvent,
ctx: &mut ModelContext<Self>,
) {
match event {
CloudModelEvent::ObjectUpdated { type_and_id, .. }
| CloudModelEvent::ObjectTrashed { type_and_id, .. }
@@ -370,6 +369,7 @@ impl CloudViewModel {
fn handle_update_manager_event(
&mut self,
_: ModelHandle<UpdateManager>,
event: &UpdateManagerEvent,
ctx: &mut ModelContext<Self>,
) {
+1 -2
View File
@@ -1,11 +1,10 @@
use galaxyui::AppContext;
use super::{CloudObject, GenericStringObjectFormat, JsonObjectType, ObjectType};
use crate::server::cloud_objects::update_manager::{
InitiatedBy, ObjectOperation, OperationSuccessType,
};
use super::{CloudObject, GenericStringObjectFormat, JsonObjectType, ObjectType};
pub struct CloudObjectToastMessage;
impl CloudObjectToastMessage {