1051 lines · 39.5 KB · Rust
RawDownload
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.