Complete local-first content migration slice

This commit is contained in:
2026-08-05 16:24:04 -05:00
parent 993abb96df
commit f850bae77c
60 changed files with 2755 additions and 2729 deletions
@@ -3,15 +3,10 @@ use galaxyui::{Entity, ModelContext, SingletonEntity};
use super::CloudEnvVarCollectionModel;
use crate::cloud_object::breadcrumbs::ContainingObject;
use crate::cloud_object::model::persistence::CloudModelEvent;
use crate::cloud_object::model::view::CloudViewModel;
use crate::cloud_object::{CloudObject, Owner, Revision, Space};
use crate::drive::sharing::{ContentEditability, SharingAccessLevel};
use crate::env_vars::CloudEnvVarCollection;
use crate::server::cloud_objects::update_manager::{
ObjectOperation, OperationSuccessType, UpdateManagerEvent,
};
use crate::server::ids::{ClientId, ServerId, SyncId};
use crate::{AppContext, CloudModel, UpdateManager};
use crate::server::ids::{ClientId, SyncId};
use crate::{AppContext, CloudModel};
#[derive(Default, Clone)]
pub enum ActiveEnvVarCollection {
@@ -42,12 +37,6 @@ pub struct ActiveEnvVarCollectionData {
impl ActiveEnvVarCollectionData {
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
let update_manager = UpdateManager::handle(ctx);
ctx.subscribe_to_model(&update_manager, |me, _, event, ctx| {
me.handle_update_manager_event(event, ctx);
});
let cloud_model = CloudModel::handle(ctx);
ctx.subscribe_to_model(&cloud_model, |me, _, event, ctx| {
@@ -69,85 +58,6 @@ impl ActiveEnvVarCollectionData {
}
}
fn handle_update_manager_event(
&mut self,
event: &UpdateManagerEvent,
ctx: &mut ModelContext<Self>,
) {
let cloud_model = CloudModel::as_ref(ctx);
let UpdateManagerEvent::ObjectOperationComplete { result } = event else {
return;
};
match (&result.operation, &result.success_type) {
(ObjectOperation::Create { .. }, OperationSuccessType::Success) => {
if let Some(current_id) = self.id() {
if current_id.into_client() == result.client_id {
let server_id = result.server_id.expect("Expect server id on success");
let env_var_collection_id = SyncId::ServerId(server_id);
if let Some(env_var_collection) =
cloud_model.get_env_var_collection(&env_var_collection_id)
{
self.saving_status = SavingStatus::Saved;
self.active_env_var_collection =
ActiveEnvVarCollection::CommittedEnvVarCollection(
env_var_collection_id,
);
self.revision_ts
.clone_from(&env_var_collection.metadata.revision);
ctx.emit(ActiveEnvVarCollectionDataEvent::CreatedOnServer(server_id));
ctx.notify();
}
}
}
}
(ObjectOperation::Update, OperationSuccessType::Success) => {
if let Some(current_id) = self.id() {
// If we match on a non-None client id or a non-None server id then
// update the data
if (current_id.into_client().is_some()
&& current_id.into_client() == result.client_id)
|| (current_id.into_server().is_some()
&& current_id.into_server() == result.server_id)
{
let server_id = result.server_id.expect("Expect server id on success");
let env_var_collection_id = SyncId::ServerId(server_id);
if let Some(env_var_collection) =
cloud_model.get_env_var_collection(&env_var_collection_id)
{
self.saving_status = SavingStatus::Saved;
self.active_env_var_collection =
ActiveEnvVarCollection::CommittedEnvVarCollection(
env_var_collection_id,
);
self.revision_ts
.clone_from(&env_var_collection.metadata.revision);
ctx.notify();
}
}
}
}
(ObjectOperation::Trash, OperationSuccessType::Success)
| (ObjectOperation::Untrash, OperationSuccessType::Success) => {
let server_id = result.server_id.expect("Expect server id on success");
if let Some(current_id) = self.id() {
if current_id.into_client() == result.client_id
&& cloud_model
.get_env_var_collection(&SyncId::ServerId(server_id))
.is_some()
{
ctx.emit(ActiveEnvVarCollectionDataEvent::TrashStatusChanged);
}
}
}
_ => {}
}
}
pub fn reset(&mut self) {
self.active_env_var_collection = ActiveEnvVarCollection::None;
}
@@ -197,35 +107,21 @@ impl ActiveEnvVarCollectionData {
}
/// The current user's access level on this env var collection.
pub fn access_level(&self, app: &AppContext) -> SharingAccessLevel {
match &self.active_env_var_collection {
ActiveEnvVarCollection::CommittedEnvVarCollection(sync_id) => {
CloudViewModel::as_ref(app).access_level(&sync_id.uid(), app)
}
ActiveEnvVarCollection::None | ActiveEnvVarCollection::NewEnvVarCollection(_) => {
SharingAccessLevel::Full
}
}
pub fn access_level(&self, _app: &AppContext) -> crate::drive::sharing::SharingAccessLevel {
crate::drive::sharing::SharingAccessLevel::Full
}
pub fn editability(&self, app: &AppContext) -> ContentEditability {
match &self.active_env_var_collection {
ActiveEnvVarCollection::CommittedEnvVarCollection(sync_id) => {
CloudViewModel::as_ref(app).object_editability(&sync_id.uid(), app)
}
ActiveEnvVarCollection::None | ActiveEnvVarCollection::NewEnvVarCollection(_) => {
ContentEditability::Editable
}
}
pub fn editability(&self, _app: &AppContext) -> crate::drive::sharing::ContentEditability {
crate::drive::sharing::ContentEditability::Editable
}
/// The space that this env var collection is in.
pub fn space(&self, app: &AppContext) -> Option<Space> {
match &self.active_env_var_collection {
ActiveEnvVarCollection::None => None,
ActiveEnvVarCollection::CommittedEnvVarCollection(sync_id) => {
CloudViewModel::as_ref(app).object_space(&sync_id.uid(), app)
}
ActiveEnvVarCollection::CommittedEnvVarCollection(sync_id) => CloudModel::as_ref(app)
.get_env_var_collection(sync_id)
.map(|collection| collection.space(app)),
ActiveEnvVarCollection::NewEnvVarCollection(env_var_collection) => {
Some(env_var_collection.space(app))
}
@@ -236,11 +132,11 @@ impl ActiveEnvVarCollectionData {
self.active_env_var_collection.clone()
}
/// Whether or not the EVC has been synced to the server.
/// Whether or not the EVC has been persisted locally.
pub fn is_on_server(&self) -> bool {
matches!(
&self.active_env_var_collection,
ActiveEnvVarCollection::CommittedEnvVarCollection(SyncId::ServerId(_))
ActiveEnvVarCollection::CommittedEnvVarCollection(_)
)
}
@@ -295,11 +191,6 @@ pub enum TrashStatus {
pub enum ActiveEnvVarCollectionDataEvent {
/// The EVC's breadcrumbs were updated.
BreadcrumbsChanged,
/// The EVC was synced to the server for the first time.
CreatedOnServer(ServerId),
/// The EVC was trashed or untrashed
/// (used for refreshing the pane overflow items)
TrashStatusChanged,
}
impl Entity for ActiveEnvVarCollectionData {
+37 -129
View File
@@ -22,9 +22,9 @@ use super::menus::Menus;
use crate::ai::blocklist::block::secret_redaction::find_secrets_in_text_with_levels;
use crate::cloud_object::breadcrumbs::ContainingObject;
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
use crate::cloud_object::{CloudObjectEventEntrypoint, Owner};
use crate::cloud_object::Owner;
use crate::drive::items::WarpDriveItemId;
use crate::drive::sharing::{ContentEditability, ShareableObject};
use crate::drive::sharing::ContentEditability;
use crate::editor::EditorView;
use crate::env_vars::active_env_var_collection_data::{
ActiveEnvVarCollection, ActiveEnvVarCollectionData, ActiveEnvVarCollectionDataEvent,
@@ -35,14 +35,13 @@ use crate::env_vars::{
EnvVarCollectionType, EnvVarValue,
};
use crate::external_secrets::SecretManager;
use crate::local_object_repository::LocalObjectRepository;
use crate::menu::MenuItem;
use crate::network::{NetworkStatus, NetworkStatusEvent};
use crate::pane_group::focus_state::PaneFocusHandle;
use crate::pane_group::pane::view;
use crate::pane_group::{BackingView, PaneConfiguration, PaneEvent};
use crate::search::external_secrets::view::ExternalSecretsMenu;
use crate::server::cloud_objects::update_manager::{FetchSingleObjectOption, UpdateManager};
use crate::server::ids::{ServerId, SyncId};
use crate::server::ids::SyncId;
use crate::terminal::model::secrets::SecretLevel;
use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode;
use crate::ui_components::breadcrumb::{render_breadcrumbs, BreadcrumbState};
@@ -498,11 +497,6 @@ impl EnvVarCollectionView {
Self::handle_active_env_var_collection_change,
);
ctx.subscribe_to_model(
&NetworkStatus::handle(ctx),
Self::handle_network_status_event,
);
let title_editor = Self::create_editor_handle(
ctx,
Some(PLACEHOLDER_FONT_SIZE),
@@ -593,57 +587,21 @@ impl EnvVarCollectionView {
window_id: WindowId,
ctx: &mut ViewContext<Self>,
) {
let initial_load_complete = UpdateManager::handle(ctx).update(ctx, |update_manager, _| {
update_manager.initial_load_complete()
});
ctx.spawn(initial_load_complete, move |me, _, ctx| {
let env_var_collection = CloudModel::as_ref(ctx)
.get_env_var_collection(&env_var_collection_id)
.cloned();
if let Some(env_var_collection) = env_var_collection {
me.load(env_var_collection, ctx);
} else if let Some(server_id) = env_var_collection_id.into_server() {
me.fetch_and_load_env_var_collection(server_id, window_id, ctx);
} else {
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast_by_type(
ToastType::CloudObjectNotFound,
window_id,
ctx,
);
});
log::warn!("Tried to open unknown env var collection {env_var_collection_id:?}");
}
});
}
fn fetch_and_load_env_var_collection(
&mut self,
env_var_collection_id: ServerId,
window_id: WindowId,
ctx: &mut ViewContext<Self>,
) {
let fetch_cloud_object_rx =
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
update_manager.fetch_single_cloud_object(
&env_var_collection_id,
FetchSingleObjectOption::None,
if let Some(env_var_collection) = CloudModel::as_ref(ctx)
.get_env_var_collection(&env_var_collection_id)
.cloned()
{
self.load(env_var_collection, ctx);
} else {
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast_by_type(
ToastType::CloudObjectNotFound,
window_id,
ctx,
)
);
});
ctx.spawn(fetch_cloud_object_rx, move |me, _, ctx| {
if let Some(env_var_collection) = CloudModel::as_ref(ctx)
.get_env_var_collection(&SyncId::ServerId(env_var_collection_id))
.cloned()
{
me.load(env_var_collection, ctx);
} else {
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast_by_type(ToastType::CloudObjectNotFound, window_id, ctx);
});
log::warn!("Tried to open unknown env var collection {env_var_collection_id:?} after fetching");
}
});
log::warn!("Tried to open unknown local env var collection {env_var_collection_id:?}");
}
}
pub fn load(&mut self, env_var_collection: CloudEnvVarCollection, ctx: &mut ViewContext<Self>) {
@@ -659,13 +617,6 @@ impl EnvVarCollectionView {
let title = collection.title.clone().unwrap_or_default();
self.set_pane_title(if title.is_empty() { "Untitled" } else { &title }, ctx);
if let Some(server_id) = env_var_collection.id.into_server() {
self.pane_configuration.update(ctx, |pane_config, ctx| {
pane_config
.set_shareable_object(Some(ShareableObject::WarpDriveObject(server_id)), ctx);
});
}
let description = collection.description.clone().unwrap_or_default();
self.title_editor.update(ctx, |editor, ctx| {
@@ -758,7 +709,7 @@ impl EnvVarCollectionView {
}
}
fn save_env_var_collection(&self, ctx: &mut ViewContext<Self>) {
fn save_env_var_collection(&mut self, ctx: &mut ViewContext<Self>) {
if self.should_disable_save(ctx) {
return;
}
@@ -823,41 +774,28 @@ impl EnvVarCollectionView {
.active_env_var_collection();
match active_env_var_collection {
// If the EVC has already been committed, then update the local
// memory and server data via update manager
ActiveEnvVarCollection::CommittedEnvVarCollection(id) => UpdateManager::handle(ctx)
.update(ctx, |update_manager, ctx| {
update_manager.update_env_var_collection(
new_env_var_collection,
ActiveEnvVarCollection::CommittedEnvVarCollection(id) => {
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
repository.update_env_var_collection(id, new_env_var_collection, ctx);
});
self.set_saving_status(SavingStatus::Saved, ctx);
}
ActiveEnvVarCollection::NewEnvVarCollection(env_var_collection) => {
let id = env_var_collection.id;
let folder_id = env_var_collection.metadata.folder_id;
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
repository.create_env_var_collection_with_id(
id,
self.active_env_var_collection_data
.update(ctx, |data, _| data.revision_ts.clone()),
folder_id,
new_env_var_collection,
ctx,
);
}),
// If the EVC hasn't been committed yet, create the EVC through update
// manager, and update the active EVC
ActiveEnvVarCollection::NewEnvVarCollection(env_var_collection) => {
if let Some(client_id) = env_var_collection.id.into_client() {
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
update_manager.create_env_var_collection(
client_id,
env_var_collection.permissions.owner,
env_var_collection.metadata.folder_id,
CloudEnvVarCollectionModel::new(new_env_var_collection),
CloudObjectEventEntrypoint::Unknown,
true,
ctx,
);
});
self.active_env_var_collection_data.update(ctx, |data, _| {
data.active_env_var_collection =
ActiveEnvVarCollection::CommittedEnvVarCollection(SyncId::ClientId(
client_id,
))
});
}
});
self.active_env_var_collection_data.update(ctx, |data, _| {
data.active_env_var_collection =
ActiveEnvVarCollection::CommittedEnvVarCollection(id);
data.saving_status = SavingStatus::Saved;
});
}
ActiveEnvVarCollection::None => {
log::error!("Tried to save EVC, but none were active")
@@ -954,20 +892,6 @@ impl EnvVarCollectionView {
self.update_breadcrumbs(ctx);
ctx.notify()
}
ActiveEnvVarCollectionDataEvent::CreatedOnServer(server_id) => {
self.update_breadcrumbs(ctx);
self.pane_configuration.update(ctx, |pane_config, ctx| {
pane_config.set_shareable_object(
Some(ShareableObject::WarpDriveObject(*server_id)),
ctx,
);
});
}
ActiveEnvVarCollectionDataEvent::TrashStatusChanged => {
self.pane_configuration.update(ctx, |pane_config, ctx| {
pane_config.refresh_pane_header_overflow_menu_items(ctx)
});
}
}
}
@@ -1045,22 +969,6 @@ impl EnvVarCollectionView {
.max_by_key(|error| error.secret_level.priority())
}
pub(super) fn is_online(&self, app: &AppContext) -> bool {
NetworkStatus::as_ref(app).is_online()
}
fn handle_network_status_event(
&mut self,
_handle: ModelHandle<NetworkStatus>,
event: &NetworkStatusEvent,
ctx: &mut ViewContext<Self>,
) {
let NetworkStatusEvent::NetworkStatusChanged { new_status: _ } = event;
self.pane_configuration.update(ctx, |pane_config, ctx| {
pane_config.refresh_pane_header_overflow_menu_items(ctx)
});
}
pub fn set_saving_status(&mut self, status: SavingStatus, ctx: &mut ViewContext<Self>) {
self.active_env_var_collection_data
.update(ctx, |data, _| data.saving_status = status);
+22 -55
View File
@@ -4,20 +4,20 @@ use galaxyui::{SingletonEntity, ViewContext, ViewHandle};
use pathfinder_geometry::vector::Vector2F;
use super::env_var_collection::{EnvVarCollectionAction, EnvVarCollectionView, VariableRowIndex};
use crate::cloud_object::{CloudObject, GenericStringObjectFormat, Space};
use crate::cloud_object::{CloudObject, GenericStringObjectFormat};
use crate::drive::drive_helpers::has_feature_gated_anonymous_user_reached_env_var_limit;
use crate::drive::export::ExportManager;
use crate::drive::CloudObjectTypeAndId;
use crate::env_vars::active_env_var_collection_data::TrashStatus;
use crate::external_secrets::SecretManager;
use crate::local_object_repository::LocalObjectRepository;
use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields};
use crate::pane_group::PaneEvent;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::ui_components::icons::Icon;
use crate::util::bindings::{
keybinding_name_to_display_string, trigger_to_keystroke, CustomAction,
};
use crate::{AppContext, CloudModel, FeatureFlag};
use crate::{AppContext, CloudModel};
const PANE_MENU_WIDTH: f32 = 200.;
@@ -360,10 +360,7 @@ impl EnvVarCollectionView {
let mut menu_items = Vec::new();
let active_collection_data = self.active_env_var_collection_data.as_ref(ctx);
let access_level = active_collection_data.access_level(ctx);
let space = active_collection_data.space(ctx);
if !active_collection_data.is_on_server()
if active_collection_data.id().is_none()
|| active_collection_data.trash_status(ctx) != TrashStatus::Active
{
return menu_items;
@@ -380,26 +377,20 @@ impl EnvVarCollectionView {
}
// Add "Duplicate" to menu
if space != Some(Space::Shared) {
menu_items.push(
MenuItemFields::new("Duplicate")
.with_on_select_action(EnvVarCollectionAction::Duplicate)
.with_icon(Icon::Duplicate)
.into_item(),
);
}
menu_items.push(
MenuItemFields::new("Duplicate")
.with_on_select_action(EnvVarCollectionAction::Duplicate)
.with_icon(Icon::Duplicate)
.into_item(),
);
// Add "Trash" to menu
if self.is_online(ctx)
&& (!FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash())
{
menu_items.push(
MenuItemFields::new("Trash")
.with_on_select_action(EnvVarCollectionAction::Trash)
.with_icon(Icon::Trash)
.into_item(),
);
}
menu_items.push(
MenuItemFields::new("Trash")
.with_on_select_action(EnvVarCollectionAction::Trash)
.with_icon(Icon::Trash)
.into_item(),
);
#[cfg(feature = "local_fs")]
menu_items.push(
@@ -424,16 +415,8 @@ impl EnvVarCollectionView {
return;
}
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
update_manager.untrash_object(
CloudObjectTypeAndId::GenericStringObject {
object_type: GenericStringObjectFormat::Json(
crate::cloud_object::JsonObjectType::EnvVarCollection,
),
id: env_var_collection_id,
},
ctx,
);
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
repository.set_env_var_collection_trashed(env_var_collection_id, false, ctx);
});
}
ctx.notify();
@@ -443,16 +426,8 @@ impl EnvVarCollectionView {
if let Some(env_var_collection_id) = self.env_var_collection_id(ctx) {
self.close_env_var_collection(ctx);
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
update_manager.trash_object(
CloudObjectTypeAndId::from_generic_string_object(
GenericStringObjectFormat::Json(
crate::cloud_object::JsonObjectType::EnvVarCollection,
),
env_var_collection_id,
),
ctx,
);
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
repository.set_env_var_collection_trashed(env_var_collection_id, true, ctx);
});
ctx.notify();
}
@@ -460,16 +435,8 @@ impl EnvVarCollectionView {
pub(super) fn duplicate_env_var_collection(&self, ctx: &mut ViewContext<Self>) {
if let Some(env_var_collection_id) = self.env_var_collection_id(ctx) {
UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| {
update_manager.duplicate_object(
&CloudObjectTypeAndId::from_generic_string_object(
GenericStringObjectFormat::Json(
crate::cloud_object::JsonObjectType::EnvVarCollection,
),
env_var_collection_id,
),
ctx,
);
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
repository.duplicate_env_var_collection(env_var_collection_id, ctx);
});
ctx.notify();
}