use std::any::Any;use std::collections::{HashMap, HashSet};use std::fmt::Debug;use std::sync::Arc; use anyhow::Result;use async_trait::async_trait;use chrono::{Duration, Utc};use derivative::Derivative;use lazy_static::lazy_static;use regex::Regex;use url::Url;use warp_core::channel::Channel;use warp_core::features::FeatureFlag;use warp_graphql::queries::get_updated_cloud_objects::UpdatedObjectInput;use warp_graphql::scalars::time::ServerTimestamp;use warpui::{AppContext, SingletonEntity}; use self::breadcrumbs::ContainingObject;use self::model::actions::ObjectActions;use self::model::generic_string_model::{ GenericStringModel, GenericStringObjectId, Serializer, StringModel,};use self::model::persistence::CloudModel;use crate::appearance::Appearance;use crate::auth::UserUid;use crate::channel::ChannelState;use crate::drive::items::WarpDriveItem;use crate::drive::{CloudObjectTypeAndId, OpenWarpDriveObjectArgs, OpenWarpDriveObjectSettings};use crate::persistence::ModelEvent;use crate::server::cloud_objects::update_manager::InitiatedBy;use crate::server::ids::{HashableId, HashedSqliteId, ObjectUid, ServerId, SyncId, ToServerId};use crate::server::server_api::object::ObjectClient;use crate::server::sync_queue::{QueueItem, SerializedModel};use crate::util::time_format::format_approx_duration_from_now_utc;use crate::workflows::{CloudWorkflow, WorkflowSource};use crate::workspaces::user_profiles::UserProfiles;use crate::workspaces::user_workspaces::UserWorkspaces; pub mod breadcrumbs;pub mod grab_edit_access_modal;pub mod model;pub mod toast_message; pub use cloud_objects::cloud_object::*; /// A CloudObject represents/// therefore shareable and editable (i.e. Notebooks and Workflows). In order/// to support collaborative editing of these objects, they must each store local/// revision numbers to ensure a stable way of accepting and rejecting edits.////// Note that this trait must be object-safe and non-generic. The reason for this/// is that (a) we need to be able to store instances of it as trait objects in/// CloudModel and (b) we need to be able to support mixed collections of different/// instances of it (e.g. in the map of id -> CloudObject in CloudModel).////// There are two closely related types to this:/// 1) GenericCloudObject: This is the concrete generic implementation of CloudObject that/// holds onto a model of type CloudModelType and an id of type SyncId./// 2) CloudModelType: This is a trait that defines the model type for a CloudObject -/// this is what implementors of new cloud types typically have to implement.////// These types are tightly coupled. In an ideal world, rust would allow a mechanism/// for us having a single interface that new model types could implement that could/// be generic on id and model types, but as far as I (zach) can tell, that's not currently/// possible.////// The typical usage pattern for these types is to use dyn CloudObject whenever you/// don't need access to a model or id, and to downcast to a GenericCloudObject whenever you do.////// This implies that, for now, *all* CloudObjects must implement GenericCloudObject.////// Additionally, they must support the "grab the baton" UX for editing, where any/// user can grab edit access of an object, revoking it from anyone else currently/// editing.////// For more info on revisions: https://docs.google.com/document/d/1SGtX_5AiSJmUxXCRk5NzGTzrC_XrxQRsio-KZOec_ng/edit/// And grab the baton: https://docs.google.com/document/d/1LgGaz8bB40AONTzC0ZFOw5Kg0SD8_RM10V_nyt3zOvY/edit#heading=h.tcup5oqi82p4pub trait CloudObject: Debug { /// Returns the name of this model type (e.g. Workflow, Folder, Notebook) fn model_type_name(&self) -> &'static str; /// Returns the uid for this object. fn uid(&self) -> ObjectUid; /// Returns the [`SyncId`] that currently identifies this object. fn sync_id(&self) -> SyncId; /// Returns the id used to index into sqlite, this is the object's UID with its type /// prefixed, such as "Workflow-{UID}" fn hashed_sqlite_id(&self) -> HashedSqliteId; /// Returns the CloudObjectMetadata struct associated with this object. fn metadata(&self) -> &CloudObjectMetadata; /// Returns a mutable reference to the CloudObjectMetadata struct associated with this object. fn metadata_mut(&mut self) -> &mut CloudObjectMetadata; /// Returns the CloudObjectPermissions struct associated with this object. fn permissions(&self) -> &CloudObjectPermissions; /// Returnsa mutable reference to the CloudObjectPermissions struct associated with this object. fn permissions_mut(&mut self) -> &mut CloudObjectPermissions; /// Returns the ObjectType i.e. 'Workflow' or 'Notebook' fn object_type(&self) -> ObjectType; /// Returns the CloudObjectTypeAndId for this object. fn cloud_object_type_and_id(&self) -> CloudObjectTypeAndId; /// Sets the server id on this object. fn set_server_id(&mut self, server_id: ServerId); /// Returns whether this object can be moved to the given space. fn can_move_to_space(&self, _space: Space, _app: &AppContext) -> bool { true } // Whether to clear this object from the local SQLite DB on a unique key conflict. fn should_clear_on_unique_key_conflict(&self) -> bool { false } /// Whether to show a warning if this object is unsaved at quit time /// (which typically blocks the user from quitting) fn warn_if_unsaved_at_quit(&self) -> bool { true } /// Returns the "upsert" event for inserting / updating this object in the SQLite DB. fn upsert_event(&self) -> ModelEvent; // Returns the name of the object. fn display_name(&self) -> String; /// Returns an optional UpdatedObjectInput to use during initial load, where /// the object's timestamps are sent to the server for comparison fn versions(&self, app: &AppContext) -> Option<UpdatedObjectInput>; /// Returns an optional sync queue item of this object that would allow it to /// created properly on the server. Returns None if it's already been created /// server-side. fn create_object_queue_item( &self, entrypoint: CloudObjectEventEntrypoint, initiated_by: InitiatedBy, ) -> Option<QueueItem>; /// Returns a sync queue item of this object that would allow it to be updated /// properly on the server. Takes an optional revision_ts to set as the revision /// in the sync queue item. fn update_object_queue_item(&self, revision_ts: Option<Revision>) -> QueueItem; /// Returns whether this model type should render as a warp drive item. fn renders_in_warp_drive(&self) -> bool; /// Returns whether this model type should show update toasts in the UI. fn should_show_activity_toasts(&self) -> bool { true } /// Creates a new Warp Drive item for this object. Returns None if this /// object is not rendered in Warp Drive. fn to_warp_drive_item(&self, appearance: &Appearance) -> Option<Box<dyn WarpDriveItem>>; /// Returns the web link of this object. Will return none if we do not support web links /// for this particular object (i.e. if it's not yet sync'd to the server, or if we don't /// yet support linking to that object type). /// /// The format of an objects link follows the pattern: /// {channel}/drive/{object-type}/{object-name}-{uid}. For more information on this, /// see the linkable objects PRD (https://docs.google.com/document/d/1VQZ4sgLs4M9r2NDYyecfOalLlPmcf2fd_rDdqG35Zd8/edit) /// or tech doc (https://docs.google.com/document/d/1_TK19mRcD_0eLwbr5uFRabacIzfKocfahjEvoRcs5ko/edit) fn object_link(&self) -> Option<String>; /// The space containing this object. /// /// If the object is shared with the current user, the space will reflect that, not the /// object's actual owner. fn space(&self, app: &AppContext) -> Space { UserWorkspaces::as_ref(app).owner_to_space(self.permissions().owner, app) } /// Whether or not this object can be "left". For shared objects, this removes all ACLs for the /// current user. Only top-level items in the shared space can be left. fn can_leave(&self, app: &AppContext) -> bool { if self.space(app) == Space::Shared { self.metadata() .folder_id .is_none_or(|parent| CloudModel::as_ref(app).get_folder(&parent).is_none()) } else { false } } /// Returns the name of the containing "object" for this object. /// This could be a folder, or in the case of top-level objects, /// the name of the space it belongs to. fn containing_object_name(&self, app: &AppContext) -> String { self.containing_objects_path(app) .into_iter() .next_back() .expect("Object should have at least one ancestor") .name } // Returns the path of all the containing "objects" for this object. // This could include folders or spaces. fn containing_objects_path(&self, app: &AppContext) -> Vec<ContainingObject> { let space = self.space(app); match self.metadata().folder_id { Some(folder_id) => { let cloud_model = CloudModel::as_ref(app); if let Some(folder) = cloud_model.get_folder_by_uid(&folder_id.uid()) { let mut path = vec![]; let ancestors = folder.containing_objects_path(app); path.extend(ancestors); path.push(folder.into()); path } else { // if for whatever reason the folder id is messed up, // just default to showing the top-level space it wound up in vec![space.into_containing_object(app)] } } None => vec![space.into_containing_object(app)], } } fn breadcrumbs(&self, app: &AppContext) -> String { self.containing_objects_path(app) .into_iter() .map(|object| object.name) .collect::<Vec<String>>() .join(" / ") } /// Returns whether this CloudObject is in the given space fn is_in_space(&self, space: Space, app: &AppContext) -> bool { self.space(app) == space } fn is_welcome_object(&self) -> bool { self.metadata().is_welcome_object } /// Returns the direct location of the object. If the object /// is not in a folder, this will be the object's space. Otherwise, it will /// be the folder the object is placed in directly, even if that folder is nested. fn location(&self, cloud_model: &CloudModel, app: &AppContext) -> CloudObjectLocation { if let Some(folder_id) = self.metadata().folder_id && cloud_model.get_folder(&folder_id).is_some() { return CloudObjectLocation::Folder(folder_id); } CloudObjectLocation::Space(self.space(app)) } /// Return true is this object or any of its ancestors are trashed. Also returns true /// if a cycle is detected. fn is_trashed(&self, cloud_model: &CloudModel) -> bool { self.is_trashed_internal(cloud_model, &mut HashSet::new()) } /// Helper function for is_trashed. fn is_trashed_internal( &self, cloud_model: &CloudModel, ancestors: &mut HashSet<String>, ) -> bool { // Base case: If the object is trashed, return true. if self.metadata().trashed_ts.is_some() { return true; } // Else: return true if the object's parent is trashed. Return false if the object has no parent. match self.metadata().folder_id.map(|parent_id| parent_id.uid()) { Some(hashed_parent_id) => { // We need to check for cycles to avoid causing a stack overflow. If a cycle is detected, return that the object is trashed. if ancestors.contains(&hashed_parent_id) { return true; } let parent = cloud_model.get_by_uid(&hashed_parent_id); // Insert before checking parent to avoid infinite recursion in case of cycles. ancestors.insert(hashed_parent_id); match parent { Some(parent) => parent.is_trashed_internal(cloud_model, ancestors), None => { // If the object has a parent, but the parent is not in CloudModel, assume // the object is shared, but not its parent. For backwards compatibility, // if sharing is disabled, default to trashed rather than untrashed. !FeatureFlag::SharedWithMe.is_enabled() } } } None => false, } } /// Returns whether this object has conflicting changes with the server. fn has_conflicting_changes(&self) -> bool; /// Returns the revision of the conflicting object, if any. /// This is used for object-safe access to conflict information. fn conflicting_object_revision(&self) -> Option<Revision>; /// Clears the conflict status back to NoConflicts. fn clear_conflict_status(&mut self); /// Updates the object to deal with any conflict status. fn replace_object_with_conflict(&mut self); /// Sets the content sync status of this object to `InFlight` (if it wasn't already) and /// increments the number of in flight requests tracked in the `InFlight` enum. fn increment_in_flight_request_count(&mut self) { let new_reqs = match &self.metadata().pending_changes_statuses.content_sync_status { CloudObjectSyncStatus::InFlight(reqs) => reqs.0 + 1, _ => 1, }; self.set_pending_content_changes_status(CloudObjectSyncStatus::InFlight( NumInFlightRequests(new_reqs), )) } /// Decrements the number of in flight requests tracked in this object's `InFlight` enum. If /// that number becomes 0, it's no longer in flight, so it will be set to `status_if_no_reqs`. /// Returns true if the object is no longer in flight. fn decrement_in_flight_request_count( &mut self, status_if_no_reqs: CloudObjectSyncStatus, ) -> bool { match &self.metadata().pending_changes_statuses.content_sync_status { CloudObjectSyncStatus::InFlight(reqs) => { if reqs.0 - 1 == 0 { self.set_pending_content_changes_status(status_if_no_reqs); return true; } else { self.set_pending_content_changes_status(CloudObjectSyncStatus::InFlight( NumInFlightRequests(reqs.0 - 1), )); return false; } } _ => report_error!( "called decrement_in_flight_request_count with a non-`InFlight` cloud status" ), } true } /// Sets the content change status on this object's metadata fn set_pending_content_changes_status( &mut self, pending_content_changes_status: CloudObjectSyncStatus, ) { self.metadata_mut() .pending_changes_statuses .content_sync_status = pending_content_changes_status; } /// Whether or not this object can be exported. fn can_export(&self) -> bool; /// Returns this object as a ref to the Any type. Needed for typecasts. fn as_any(&self) -> &dyn Any; /// Returns this object as a mut ref to Any type. Needed for typecasts. fn as_any_mut(&mut self) -> &mut dyn Any; /// Returns the trait object as a concrete type reference by downcasting it. /// Returns None if the downcast fails. fn as_model_type<K, M>(cloud_object: &dyn CloudObject) -> Option<&GenericCloudObject<K, M>> where Self: Sized, K: HashableId + ToServerId + Debug + Into<String> + Clone + 'static, M: CloudModelType<IdType = K, CloudObjectType = GenericCloudObject<K, M>> + 'static, { cloud_object .as_any() .downcast_ref::<GenericCloudObject<K, M>>() } /// Returns the trait object as a concrete mutable type reference by downcasting it. /// Returns None if the downcast fails. fn as_model_type_mut<K, M>( cloud_object: &mut dyn CloudObject, ) -> Option<&mut GenericCloudObject<K, M>> where Self: Sized, K: HashableId + ToServerId + Debug + Into<String> + Clone + 'static, M: CloudModelType<IdType = K, CloudObjectType = GenericCloudObject<K, M>> + 'static, { cloud_object .as_any_mut() .downcast_mut::<GenericCloudObject<K, M>>() } /// Returns a cloned boxed version of this cloud object. /// Note that we can't force the CloudObject trait to derive from Cloned /// directly because that would make the trait not object safe. This /// is a workaround. fn clone_box(&self) -> Box<dyn CloudObject>;} /// Defines a common trait for cloud models to implement./// The "model" is the domain specific piece of data for a cloud object,/// e.g. it contains the notebook, workflow, or folder specific data, but has/// no logic around metadata, permissions, or sync status.////// See the comments for CloudObject to understand the relationship between/// this trait, CloudObject and GenericCloudObject. They are tightly coupled.////// When building new model types (e.g. for settings or launch configs) we should just/// have to implement this trait, and not the entire CloudObject trait.#[cfg_attr(not(target_family = "wasm"), async_trait)]#[cfg_attr(target_family = "wasm", async_trait(?Send))]pub trait CloudModelType: Debug + Clone + Send + Sync { /// The associated CloudObject type for this model (e.g. CloudNotebook, CloudWorkflow, etc) type CloudObjectType: CloudObject + 'static; // TODO: @ianhodge - remove for sync ID refactor. type IdType: HashableId + ToServerId + Debug + Into<String> + Clone + 'static; /// Returns the name of this model type (e.g. Workflow, Folder, Notebook) fn model_type_name(&self) -> &'static str; /// Returns the CloudObjectTypeAndId for this object. fn cloud_object_type_and_id(&self, id: SyncId) -> CloudObjectTypeAndId; /// Returns the ObjectType for this model. fn object_type(&self) -> ObjectType; /// Returns whether this model type should render as a warp drive item. fn renders_in_warp_drive(&self) -> bool; /// Returns whether this model type should show update toasts in the UI. fn should_show_activity_toasts(&self) -> bool { true } /// Whether to show a warning if this model is unsaved at quit time /// (which typically blocks the user from quitting) fn warn_if_unsaved_at_quit(&self) -> bool { true } /// Creates a new warp drive item for this model type. Returns None /// if this object does not render in Warp Drive. fn to_warp_drive_item( &self, id: SyncId, appearance: &Appearance, object: &Self::CloudObjectType, ) -> Option<Box<dyn WarpDriveItem>>; /// Returns the display name for this model (e.g. to show in the Warp Drive index) fn display_name(&self) -> String; /// Sets the display name to show in the Warp Drive Index. Setting the name /// is not currently supported by all object types, hence the default empty /// implementation. fn set_display_name(&mut self, _name: &str) {} /// Returns the upsert event for putting this model into the SQLite database. fn upsert_event(params: CloudObjectUpsertParams<Self>) -> ModelEvent where Self: Sized; /// Returns a bulk upsert event for putting a list of this model into the SQLite database. fn bulk_upsert_event(objects: Vec<CloudObjectUpsertParams<Self>>) -> ModelEvent where Self: Sized; /// Returns the sync queue item for creating this model on the server. fn create_object_queue_item( &self, object: &Self::CloudObjectType, entrypoint: CloudObjectEventEntrypoint, initiated_by: InitiatedBy, ) -> Option<QueueItem>; /// Returns the sync queue item for updating this model on the server. /// Takes an optional revision timestamp to set in the queue item. fn update_object_queue_item( &self, revision_ts: Option<Revision>, object: &Self::CloudObjectType, ) -> QueueItem; /// Returns a serialized model. fn serialized(&self) -> SerializedModel; /// Sends a request to the server to create this model. async fn send_create_request(Lines 1–500 of 1051. Load more
use std::any::Any; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use chrono::{Duration, Utc}; use derivative::Derivative; use lazy_static::lazy_static; use regex::Regex; use url::Url; use warp_core::channel::Channel; use warp_core::features::FeatureFlag; use warp_graphql::queries::get_updated_cloud_objects::UpdatedObjectInput; use warp_graphql::scalars::time::ServerTimestamp; use warpui::{AppContext, SingletonEntity}; use self::breadcrumbs::ContainingObject; use self::model::actions::ObjectActions; use self::model::generic_string_model::{ GenericStringModel, GenericStringObjectId, Serializer, StringModel, }; use self::model::persistence::CloudModel; use crate::appearance::Appearance; use crate::auth::UserUid; use crate::channel::ChannelState; use crate::drive::items::WarpDriveItem; use crate::drive::{CloudObjectTypeAndId, OpenWarpDriveObjectArgs, OpenWarpDriveObjectSettings}; use crate::persistence::ModelEvent; use crate::server::cloud_objects::update_manager::InitiatedBy; use crate::server::ids::{HashableId, HashedSqliteId, ObjectUid, ServerId, SyncId, ToServerId}; use crate::server::server_api::object::ObjectClient; use crate::server::sync_queue::{QueueItem, SerializedModel}; use crate::util::time_format::format_approx_duration_from_now_utc; use crate::workflows::{CloudWorkflow, WorkflowSource}; use crate::workspaces::user_profiles::UserProfiles; use crate::workspaces::user_workspaces::UserWorkspaces; pub mod breadcrumbs; pub mod grab_edit_access_modal; pub mod model; pub mod toast_message; pub use cloud_objects::cloud_object::*; /// A CloudObject represents /// therefore shareable and editable (i.e. Notebooks and Workflows). In order /// to support collaborative editing of these objects, they must each store local /// revision numbers to ensure a stable way of accepting and rejecting edits. /// /// Note that this trait must be object-safe and non-generic. The reason for this /// is that (a) we need to be able to store instances of it as trait objects in /// CloudModel and (b) we need to be able to support mixed collections of different /// instances of it (e.g. in the map of id -> CloudObject in CloudModel). /// /// There are two closely related types to this: /// 1) GenericCloudObject: This is the concrete generic implementation of CloudObject that /// holds onto a model of type CloudModelType and an id of type SyncId. /// 2) CloudModelType: This is a trait that defines the model type for a CloudObject - /// this is what implementors of new cloud types typically have to implement. /// /// These types are tightly coupled. In an ideal world, rust would allow a mechanism /// for us having a single interface that new model types could implement that could /// be generic on id and model types, but as far as I (zach) can tell, that's not currently /// possible. /// /// The typical usage pattern for these types is to use dyn CloudObject whenever you /// don't need access to a model or id, and to downcast to a GenericCloudObject whenever you do. /// /// This implies that, for now, *all* CloudObjects must implement GenericCloudObject. /// /// Additionally, they must support the "grab the baton" UX for editing, where any /// user can grab edit access of an object, revoking it from anyone else currently /// editing. /// /// For more info on revisions: https://docs.google.com/document/d/1SGtX_5AiSJmUxXCRk5NzGTzrC_XrxQRsio-KZOec_ng/edit /// And grab the baton: https://docs.google.com/document/d/1LgGaz8bB40AONTzC0ZFOw5Kg0SD8_RM10V_nyt3zOvY/edit#heading=h.tcup5oqi82p4 pub trait CloudObject: Debug { /// Returns the name of this model type (e.g. Workflow, Folder, Notebook) fn model_type_name(&self) -> &'static str; /// Returns the uid for this object. fn uid(&self) -> ObjectUid; /// Returns the [`SyncId`] that currently identifies this object. fn sync_id(&self) -> SyncId; /// Returns the id used to index into sqlite, this is the object's UID with its type /// prefixed, such as "Workflow-{UID}" fn hashed_sqlite_id(&self) -> HashedSqliteId; /// Returns the CloudObjectMetadata struct associated with this object. fn metadata(&self) -> &CloudObjectMetadata; /// Returns a mutable reference to the CloudObjectMetadata struct associated with this object. fn metadata_mut(&mut self) -> &mut CloudObjectMetadata; /// Returns the CloudObjectPermissions struct associated with this object. fn permissions(&self) -> &CloudObjectPermissions; /// Returnsa mutable reference to the CloudObjectPermissions struct associated with this object. fn permissions_mut(&mut self) -> &mut CloudObjectPermissions; /// Returns the ObjectType i.e. 'Workflow' or 'Notebook' fn object_type(&self) -> ObjectType; /// Returns the CloudObjectTypeAndId for this object. fn cloud_object_type_and_id(&self) -> CloudObjectTypeAndId; /// Sets the server id on this object. fn set_server_id(&mut self, server_id: ServerId); /// Returns whether this object can be moved to the given space. fn can_move_to_space(&self, _space: Space, _app: &AppContext) -> bool { true } // Whether to clear this object from the local SQLite DB on a unique key conflict. fn should_clear_on_unique_key_conflict(&self) -> bool { false } /// Whether to show a warning if this object is unsaved at quit time /// (which typically blocks the user from quitting) fn warn_if_unsaved_at_quit(&self) -> bool { true } /// Returns the "upsert" event for inserting / updating this object in the SQLite DB. fn upsert_event(&self) -> ModelEvent; // Returns the name of the object. fn display_name(&self) -> String; /// Returns an optional UpdatedObjectInput to use during initial load, where /// the object's timestamps are sent to the server for comparison fn versions(&self, app: &AppContext) -> Option<UpdatedObjectInput>; /// Returns an optional sync queue item of this object that would allow it to /// created properly on the server. Returns None if it's already been created /// server-side. fn create_object_queue_item( &self, entrypoint: CloudObjectEventEntrypoint, initiated_by: InitiatedBy, ) -> Option<QueueItem>; /// Returns a sync queue item of this object that would allow it to be updated /// properly on the server. Takes an optional revision_ts to set as the revision /// in the sync queue item. fn update_object_queue_item(&self, revision_ts: Option<Revision>) -> QueueItem; /// Returns whether this model type should render as a warp drive item. fn renders_in_warp_drive(&self) -> bool; /// Returns whether this model type should show update toasts in the UI. fn should_show_activity_toasts(&self) -> bool { true } /// Creates a new Warp Drive item for this object. Returns None if this /// object is not rendered in Warp Drive. fn to_warp_drive_item(&self, appearance: &Appearance) -> Option<Box<dyn WarpDriveItem>>; /// Returns the web link of this object. Will return none if we do not support web links /// for this particular object (i.e. if it's not yet sync'd to the server, or if we don't /// yet support linking to that object type). /// /// The format of an objects link follows the pattern: /// {channel}/drive/{object-type}/{object-name}-{uid}. For more information on this, /// see the linkable objects PRD (https://docs.google.com/document/d/1VQZ4sgLs4M9r2NDYyecfOalLlPmcf2fd_rDdqG35Zd8/edit) /// or tech doc (https://docs.google.com/document/d/1_TK19mRcD_0eLwbr5uFRabacIzfKocfahjEvoRcs5ko/edit) fn object_link(&self) -> Option<String>; /// The space containing this object. /// /// If the object is shared with the current user, the space will reflect that, not the /// object's actual owner. fn space(&self, app: &AppContext) -> Space { UserWorkspaces::as_ref(app).owner_to_space(self.permissions().owner, app) } /// Whether or not this object can be "left". For shared objects, this removes all ACLs for the /// current user. Only top-level items in the shared space can be left. fn can_leave(&self, app: &AppContext) -> bool { if self.space(app) == Space::Shared { self.metadata() .folder_id .is_none_or(|parent| CloudModel::as_ref(app).get_folder(&parent).is_none()) } else { false } } /// Returns the name of the containing "object" for this object. /// This could be a folder, or in the case of top-level objects, /// the name of the space it belongs to. fn containing_object_name(&self, app: &AppContext) -> String { self.containing_objects_path(app) .into_iter() .next_back() .expect("Object should have at least one ancestor") .name } // Returns the path of all the containing "objects" for this object. // This could include folders or spaces. fn containing_objects_path(&self, app: &AppContext) -> Vec<ContainingObject> { let space = self.space(app); match self.metadata().folder_id { Some(folder_id) => { let cloud_model = CloudModel::as_ref(app); if let Some(folder) = cloud_model.get_folder_by_uid(&folder_id.uid()) { let mut path = vec![]; let ancestors = folder.containing_objects_path(app); path.extend(ancestors); path.push(folder.into()); path } else { // if for whatever reason the folder id is messed up, // just default to showing the top-level space it wound up in vec![space.into_containing_object(app)] } } None => vec![space.into_containing_object(app)], } } fn breadcrumbs(&self, app: &AppContext) -> String { self.containing_objects_path(app) .into_iter() .map(|object| object.name) .collect::<Vec<String>>() .join(" / ") } /// Returns whether this CloudObject is in the given space fn is_in_space(&self, space: Space, app: &AppContext) -> bool { self.space(app) == space } fn is_welcome_object(&self) -> bool { self.metadata().is_welcome_object } /// Returns the direct location of the object. If the object /// is not in a folder, this will be the object's space. Otherwise, it will /// be the folder the object is placed in directly, even if that folder is nested. fn location(&self, cloud_model: &CloudModel, app: &AppContext) -> CloudObjectLocation { if let Some(folder_id) = self.metadata().folder_id && cloud_model.get_folder(&folder_id).is_some() { return CloudObjectLocation::Folder(folder_id); } CloudObjectLocation::Space(self.space(app)) } /// Return true is this object or any of its ancestors are trashed. Also returns true /// if a cycle is detected. fn is_trashed(&self, cloud_model: &CloudModel) -> bool { self.is_trashed_internal(cloud_model, &mut HashSet::new()) } /// Helper function for is_trashed. fn is_trashed_internal( &self, cloud_model: &CloudModel, ancestors: &mut HashSet<String>, ) -> bool { // Base case: If the object is trashed, return true. if self.metadata().trashed_ts.is_some() { return true; } // Else: return true if the object's parent is trashed. Return false if the object has no parent. match self.metadata().folder_id.map(|parent_id| parent_id.uid()) { Some(hashed_parent_id) => { // We need to check for cycles to avoid causing a stack overflow. If a cycle is detected, return that the object is trashed. if ancestors.contains(&hashed_parent_id) { return true; } let parent = cloud_model.get_by_uid(&hashed_parent_id); // Insert before checking parent to avoid infinite recursion in case of cycles. ancestors.insert(hashed_parent_id); match parent { Some(parent) => parent.is_trashed_internal(cloud_model, ancestors), None => { // If the object has a parent, but the parent is not in CloudModel, assume // the object is shared, but not its parent. For backwards compatibility, // if sharing is disabled, default to trashed rather than untrashed. !FeatureFlag::SharedWithMe.is_enabled() } } } None => false, } } /// Returns whether this object has conflicting changes with the server. fn has_conflicting_changes(&self) -> bool; /// Returns the revision of the conflicting object, if any. /// This is used for object-safe access to conflict information. fn conflicting_object_revision(&self) -> Option<Revision>; /// Clears the conflict status back to NoConflicts. fn clear_conflict_status(&mut self); /// Updates the object to deal with any conflict status. fn replace_object_with_conflict(&mut self); /// Sets the content sync status of this object to `InFlight` (if it wasn't already) and /// increments the number of in flight requests tracked in the `InFlight` enum. fn increment_in_flight_request_count(&mut self) { let new_reqs = match &self.metadata().pending_changes_statuses.content_sync_status { CloudObjectSyncStatus::InFlight(reqs) => reqs.0 + 1, _ => 1, }; self.set_pending_content_changes_status(CloudObjectSyncStatus::InFlight( NumInFlightRequests(new_reqs), )) } /// Decrements the number of in flight requests tracked in this object's `InFlight` enum. If /// that number becomes 0, it's no longer in flight, so it will be set to `status_if_no_reqs`. /// Returns true if the object is no longer in flight. fn decrement_in_flight_request_count( &mut self, status_if_no_reqs: CloudObjectSyncStatus, ) -> bool { match &self.metadata().pending_changes_statuses.content_sync_status { CloudObjectSyncStatus::InFlight(reqs) => { if reqs.0 - 1 == 0 { self.set_pending_content_changes_status(status_if_no_reqs); return true; } else { self.set_pending_content_changes_status(CloudObjectSyncStatus::InFlight( NumInFlightRequests(reqs.0 - 1), )); return false; } } _ => report_error!( "called decrement_in_flight_request_count with a non-`InFlight` cloud status" ), } true } /// Sets the content change status on this object's metadata fn set_pending_content_changes_status( &mut self, pending_content_changes_status: CloudObjectSyncStatus, ) { self.metadata_mut() .pending_changes_statuses .content_sync_status = pending_content_changes_status; } /// Whether or not this object can be exported. fn can_export(&self) -> bool; /// Returns this object as a ref to the Any type. Needed for typecasts. fn as_any(&self) -> &dyn Any; /// Returns this object as a mut ref to Any type. Needed for typecasts. fn as_any_mut(&mut self) -> &mut dyn Any; /// Returns the trait object as a concrete type reference by downcasting it. /// Returns None if the downcast fails. fn as_model_type<K, M>(cloud_object: &dyn CloudObject) -> Option<&GenericCloudObject<K, M>> where Self: Sized, K: HashableId + ToServerId + Debug + Into<String> + Clone + 'static, M: CloudModelType<IdType = K, CloudObjectType = GenericCloudObject<K, M>> + 'static, { cloud_object .as_any() .downcast_ref::<GenericCloudObject<K, M>>() } /// Returns the trait object as a concrete mutable type reference by downcasting it. /// Returns None if the downcast fails. fn as_model_type_mut<K, M>( cloud_object: &mut dyn CloudObject, ) -> Option<&mut GenericCloudObject<K, M>> where Self: Sized, K: HashableId + ToServerId + Debug + Into<String> + Clone + 'static, M: CloudModelType<IdType = K, CloudObjectType = GenericCloudObject<K, M>> + 'static, { cloud_object .as_any_mut() .downcast_mut::<GenericCloudObject<K, M>>() } /// Returns a cloned boxed version of this cloud object. /// Note that we can't force the CloudObject trait to derive from Cloned /// directly because that would make the trait not object safe. This /// is a workaround. fn clone_box(&self) -> Box<dyn CloudObject>; } /// Defines a common trait for cloud models to implement. /// The "model" is the domain specific piece of data for a cloud object, /// e.g. it contains the notebook, workflow, or folder specific data, but has /// no logic around metadata, permissions, or sync status. /// /// See the comments for CloudObject to understand the relationship between /// this trait, CloudObject and GenericCloudObject. They are tightly coupled. /// /// When building new model types (e.g. for settings or launch configs) we should just /// have to implement this trait, and not the entire CloudObject trait. #[cfg_attr(not(target_family = "wasm"), async_trait)] #[cfg_attr(target_family = "wasm", async_trait(?Send))] pub trait CloudModelType: Debug + Clone + Send + Sync { /// The associated CloudObject type for this model (e.g. CloudNotebook, CloudWorkflow, etc) type CloudObjectType: CloudObject + 'static; // TODO: @ianhodge - remove for sync ID refactor. type IdType: HashableId + ToServerId + Debug + Into<String> + Clone + 'static; /// Returns the name of this model type (e.g. Workflow, Folder, Notebook) fn model_type_name(&self) -> &'static str; /// Returns the CloudObjectTypeAndId for this object. fn cloud_object_type_and_id(&self, id: SyncId) -> CloudObjectTypeAndId; /// Returns the ObjectType for this model. fn object_type(&self) -> ObjectType; /// Returns whether this model type should render as a warp drive item. fn renders_in_warp_drive(&self) -> bool; /// Returns whether this model type should show update toasts in the UI. fn should_show_activity_toasts(&self) -> bool { true } /// Whether to show a warning if this model is unsaved at quit time /// (which typically blocks the user from quitting) fn warn_if_unsaved_at_quit(&self) -> bool { true } /// Creates a new warp drive item for this model type. Returns None /// if this object does not render in Warp Drive. fn to_warp_drive_item( &self, id: SyncId, appearance: &Appearance, object: &Self::CloudObjectType, ) -> Option<Box<dyn WarpDriveItem>>; /// Returns the display name for this model (e.g. to show in the Warp Drive index) fn display_name(&self) -> String; /// Sets the display name to show in the Warp Drive Index. Setting the name /// is not currently supported by all object types, hence the default empty /// implementation. fn set_display_name(&mut self, _name: &str) {} /// Returns the upsert event for putting this model into the SQLite database. fn upsert_event(params: CloudObjectUpsertParams<Self>) -> ModelEvent where Self: Sized; /// Returns a bulk upsert event for putting a list of this model into the SQLite database. fn bulk_upsert_event(objects: Vec<CloudObjectUpsertParams<Self>>) -> ModelEvent where Self: Sized; /// Returns the sync queue item for creating this model on the server. fn create_object_queue_item( &self, object: &Self::CloudObjectType, entrypoint: CloudObjectEventEntrypoint, initiated_by: InitiatedBy, ) -> Option<QueueItem>; /// Returns the sync queue item for updating this model on the server. /// Takes an optional revision timestamp to set in the queue item. fn update_object_queue_item( &self, revision_ts: Option<Revision>, object: &Self::CloudObjectType, ) -> QueueItem; /// Returns a serialized model. fn serialized(&self) -> SerializedModel; /// Sends a request to the server to create this model. async fn send_create_request(