Add space trash recovery
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::{BrowserSnapshot, TrashedSpace};
|
||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use ely_domain::{ArchivePolicy, Profile, Space, SpaceId};
|
use ely_domain::{ArchivePolicy, Profile, Space, SpaceId};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -28,7 +28,8 @@ impl ElyShell {
|
|||||||
.gap_5()
|
.gap_5()
|
||||||
.child(render_spaces_header(snapshot))
|
.child(render_spaces_header(snapshot))
|
||||||
.child(render_active_space_summary(snapshot))
|
.child(render_active_space_summary(snapshot))
|
||||||
.child(render_spaces_list(snapshot, cx)),
|
.child(render_spaces_list(snapshot, self.pending_space_trash.as_ref(), cx))
|
||||||
|
.child(render_trashed_spaces_list(snapshot, cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,11 @@ fn render_active_space_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_spaces_list(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
fn render_spaces_list(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
pending_space_trash: Option<&SpaceId>,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
@@ -138,6 +143,7 @@ fn render_spaces_list(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) ->
|
|||||||
space,
|
space,
|
||||||
snapshot,
|
snapshot,
|
||||||
space.id() == &snapshot.active_space_id,
|
space.id() == &snapshot.active_space_id,
|
||||||
|
pending_space_trash == Some(space.id()),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
@@ -150,6 +156,7 @@ fn render_space_row(
|
|||||||
space: &Space,
|
space: &Space,
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
confirming_trash: bool,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let space_id = space.id().clone();
|
let space_id = space.id().clone();
|
||||||
@@ -187,7 +194,7 @@ fn render_space_row(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(render_space_actions(index, space_count, space_id, active, cx))
|
.child(render_space_actions(index, space_count, space_id, active, confirming_trash, cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +203,13 @@ fn render_space_actions(
|
|||||||
space_count: usize,
|
space_count: usize,
|
||||||
space_id: SpaceId,
|
space_id: SpaceId,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
confirming_trash: bool,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
if confirming_trash {
|
||||||
|
return render_trash_confirmation(cx);
|
||||||
|
}
|
||||||
|
|
||||||
let can_move_up = index > 0;
|
let can_move_up = index > 0;
|
||||||
let can_move_down = index + 1 < space_count;
|
let can_move_down = index + 1 < space_count;
|
||||||
|
|
||||||
@@ -223,7 +235,8 @@ fn render_space_actions(
|
|||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(render_space_switch_action(index, space_id, active, cx))
|
.child(render_space_switch_action(index, space_id.clone(), active, cx))
|
||||||
|
.child(render_request_trash_button(index, space_id, space_count, cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +265,48 @@ fn render_space_order_button(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_request_trash_button(
|
||||||
|
index: usize,
|
||||||
|
space_id: SpaceId,
|
||||||
|
space_count: usize,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
Button::new(("trash-space", index))
|
||||||
|
.small()
|
||||||
|
.ghost()
|
||||||
|
.icon(IconName::Delete)
|
||||||
|
.tooltip("Move Space to Trash")
|
||||||
|
.disabled(space_count <= 1)
|
||||||
|
.on_click(cx.listener(move |shell, _, _, cx| {
|
||||||
|
shell.request_space_trash(space_id.clone(), cx);
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_trash_confirmation(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(Button::new("cancel-space-trash").small().ghost().label("Cancel").on_click(
|
||||||
|
cx.listener(|shell, _, _, cx| {
|
||||||
|
shell.cancel_space_trash(cx);
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
Button::new("confirm-space-trash")
|
||||||
|
.small()
|
||||||
|
.danger()
|
||||||
|
.icon(IconName::Delete)
|
||||||
|
.label("Trash")
|
||||||
|
.tooltip("Move Space to Trash")
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.trash_pending_space(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_space_switch_action(
|
fn render_space_switch_action(
|
||||||
index: usize,
|
index: usize,
|
||||||
space_id: SpaceId,
|
space_id: SpaceId,
|
||||||
@@ -279,6 +334,94 @@ fn render_space_switch_action(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_trashed_spaces_list(
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
if snapshot.trashed_spaces.is_empty() {
|
||||||
|
return div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.pt_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child("Recently Trashed"),
|
||||||
|
)
|
||||||
|
.children(
|
||||||
|
snapshot
|
||||||
|
.trashed_spaces
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, trashed_space)| render_trashed_space_row(index, trashed_space, cx)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_trashed_space_row(
|
||||||
|
index: usize,
|
||||||
|
trashed_space: &TrashedSpace,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let space_id = trashed_space.space().id().clone();
|
||||||
|
div()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.child(space_avatar(trashed_space.space()))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(trashed_space.space().name().to_string()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(trashed_space_detail_label(trashed_space)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new(("restore-space", index))
|
||||||
|
.small()
|
||||||
|
.primary()
|
||||||
|
.icon(IconName::Undo)
|
||||||
|
.label("Restore")
|
||||||
|
.tooltip("Restore Space")
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.restore_trashed_space(&space_id, window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn space_avatar(space: &Space) -> AnyElement {
|
fn space_avatar(space: &Space) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.w(px(28.0))
|
.w(px(28.0))
|
||||||
@@ -297,6 +440,14 @@ fn space_avatar(space: &Space) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn trashed_space_detail_label(trashed_space: &TrashedSpace) -> String {
|
||||||
|
format!(
|
||||||
|
"{} open tabs - {} archived tabs - retained 30 days",
|
||||||
|
trashed_space.tabs().len(),
|
||||||
|
trashed_space.archived_tabs().len()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn space_detail_label(space: &Space, profiles: &[Profile]) -> String {
|
fn space_detail_label(space: &Space, profiles: &[Profile]) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{} - {} - {}",
|
"{} - {} - {}",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub struct ElyShell {
|
|||||||
pending_history_domain_clear: Option<PendingHistoryDomainClear>,
|
pending_history_domain_clear: Option<PendingHistoryDomainClear>,
|
||||||
pending_history_time_clear: Option<PendingHistoryTimeClear>,
|
pending_history_time_clear: Option<PendingHistoryTimeClear>,
|
||||||
site_permissions_clear_confirmation: Option<ProfileId>,
|
site_permissions_clear_confirmation: Option<ProfileId>,
|
||||||
|
pending_space_trash: Option<SpaceId>,
|
||||||
pending_bookmark_edit: Option<PendingBookmarkEdit>,
|
pending_bookmark_edit: Option<PendingBookmarkEdit>,
|
||||||
bookmark_edit_error: Option<String>,
|
bookmark_edit_error: Option<String>,
|
||||||
plugin_install_error: Option<String>,
|
plugin_install_error: Option<String>,
|
||||||
@@ -116,6 +117,7 @@ impl ElyShell {
|
|||||||
pending_history_domain_clear: None,
|
pending_history_domain_clear: None,
|
||||||
pending_history_time_clear: None,
|
pending_history_time_clear: None,
|
||||||
site_permissions_clear_confirmation: None,
|
site_permissions_clear_confirmation: None,
|
||||||
|
pending_space_trash: None,
|
||||||
pending_bookmark_edit: None,
|
pending_bookmark_edit: None,
|
||||||
bookmark_edit_error: None,
|
bookmark_edit_error: None,
|
||||||
plugin_install_error: None,
|
plugin_install_error: None,
|
||||||
|
|||||||
@@ -21,6 +21,44 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn request_space_trash(&mut self, space_id: SpaceId, cx: &mut Context<Self>) {
|
||||||
|
self.pending_space_trash = Some(space_id);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_space_trash(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.pending_space_trash = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn trash_pending_space(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(space_id) = self.pending_space_trash.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.trash_space(&space_id, std::time::SystemTime::now()).is_ok()
|
||||||
|
{
|
||||||
|
self.pending_space_trash = None;
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn restore_trashed_space(
|
||||||
|
&mut self,
|
||||||
|
space_id: &SpaceId,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.restore_trashed_space(space_id).is_ok()
|
||||||
|
{
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_select_next_space(
|
pub(super) fn on_select_next_space(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &SelectNextSpace,
|
_: &SelectNextSpace,
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ pub enum CoreError {
|
|||||||
#[error("space not found: {id}")]
|
#[error("space not found: {id}")]
|
||||||
SpaceNotFound { id: SpaceId },
|
SpaceNotFound { id: SpaceId },
|
||||||
|
|
||||||
|
#[error("trashed space not found: {id}")]
|
||||||
|
TrashedSpaceNotFound { id: SpaceId },
|
||||||
|
|
||||||
|
#[error("last space cannot be moved to trash")]
|
||||||
|
LastSpaceCannotBeTrashed,
|
||||||
|
|
||||||
#[error("split not found: {id}")]
|
#[error("split not found: {id}")]
|
||||||
SplitNotFound { id: SplitId },
|
SplitNotFound { id: SplitId },
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ mod state;
|
|||||||
pub use error::CoreError;
|
pub use error::CoreError;
|
||||||
pub use state::{
|
pub use state::{
|
||||||
BrowserCore, BrowserSnapshot, InitialBrowserConfig, InstalledPlugin, PluginAuditAction,
|
BrowserCore, BrowserSnapshot, InitialBrowserConfig, InstalledPlugin, PluginAuditAction,
|
||||||
PluginAuditEvent,
|
PluginAuditEvent, TrashedSpace,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ mod tab_order;
|
|||||||
mod tabs;
|
mod tabs;
|
||||||
|
|
||||||
pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent};
|
pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent};
|
||||||
|
pub use spaces::TrashedSpace;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct InitialBrowserConfig {
|
pub struct InitialBrowserConfig {
|
||||||
@@ -67,6 +68,7 @@ pub struct BrowserSnapshot {
|
|||||||
pub installed_plugins: Vec<InstalledPlugin>,
|
pub installed_plugins: Vec<InstalledPlugin>,
|
||||||
pub plugin_audit_events: Vec<PluginAuditEvent>,
|
pub plugin_audit_events: Vec<PluginAuditEvent>,
|
||||||
pub spaces: Vec<Space>,
|
pub spaces: Vec<Space>,
|
||||||
|
pub trashed_spaces: Vec<TrashedSpace>,
|
||||||
pub profiles: Vec<Profile>,
|
pub profiles: Vec<Profile>,
|
||||||
pub sync_status: SyncStatus,
|
pub sync_status: SyncStatus,
|
||||||
pub active_tab_id: TabId,
|
pub active_tab_id: TabId,
|
||||||
@@ -98,6 +100,7 @@ pub struct BrowserCore {
|
|||||||
tab_groups: Vec<TabGroup>,
|
tab_groups: Vec<TabGroup>,
|
||||||
split_layouts: Vec<SplitLayout>,
|
split_layouts: Vec<SplitLayout>,
|
||||||
archived_split_layouts: Vec<SplitLayout>,
|
archived_split_layouts: Vec<SplitLayout>,
|
||||||
|
trashed_spaces: Vec<TrashedSpace>,
|
||||||
installed_plugins: Vec<InstalledPlugin>,
|
installed_plugins: Vec<InstalledPlugin>,
|
||||||
plugin_audit_events: Vec<PluginAuditEvent>,
|
plugin_audit_events: Vec<PluginAuditEvent>,
|
||||||
active_space_id: SpaceId,
|
active_space_id: SpaceId,
|
||||||
@@ -168,6 +171,7 @@ impl BrowserCore {
|
|||||||
tab_groups: Vec::new(),
|
tab_groups: Vec::new(),
|
||||||
split_layouts: Vec::new(),
|
split_layouts: Vec::new(),
|
||||||
archived_split_layouts: Vec::new(),
|
archived_split_layouts: Vec::new(),
|
||||||
|
trashed_spaces: Vec::new(),
|
||||||
installed_plugins: Vec::new(),
|
installed_plugins: Vec::new(),
|
||||||
plugin_audit_events: Vec::new(),
|
plugin_audit_events: Vec::new(),
|
||||||
command_query: String::new(),
|
command_query: String::new(),
|
||||||
@@ -390,6 +394,7 @@ impl BrowserCore {
|
|||||||
installed_plugins: self.installed_plugins.clone(),
|
installed_plugins: self.installed_plugins.clone(),
|
||||||
plugin_audit_events: self.plugin_audit_events.clone(),
|
plugin_audit_events: self.plugin_audit_events.clone(),
|
||||||
spaces: self.sorted_spaces(),
|
spaces: self.sorted_spaces(),
|
||||||
|
trashed_spaces: self.trashed_spaces.clone(),
|
||||||
profiles: self.profiles.clone(),
|
profiles: self.profiles.clone(),
|
||||||
sync_status: self.sync_status(),
|
sync_status: self.sync_status(),
|
||||||
tabs: self.visible_tabs(),
|
tabs: self.visible_tabs(),
|
||||||
|
|||||||
@@ -1,9 +1,108 @@
|
|||||||
use ely_domain::SpaceId;
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use ely_domain::{ArchivedTab, BrowserTab, Space, SpaceId};
|
||||||
|
|
||||||
use super::BrowserCore;
|
use super::BrowserCore;
|
||||||
use crate::CoreError;
|
use crate::CoreError;
|
||||||
|
|
||||||
|
pub const SPACE_TRASH_RETENTION_DAYS: u64 = 30;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrashedSpace {
|
||||||
|
space: Space,
|
||||||
|
tabs: Vec<BrowserTab>,
|
||||||
|
archived_tabs: Vec<ArchivedTab>,
|
||||||
|
trashed_at: SystemTime,
|
||||||
|
purge_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrashedSpace {
|
||||||
|
fn new(
|
||||||
|
space: Space,
|
||||||
|
tabs: Vec<BrowserTab>,
|
||||||
|
archived_tabs: Vec<ArchivedTab>,
|
||||||
|
trashed_at: SystemTime,
|
||||||
|
) -> Self {
|
||||||
|
let purge_at = trashed_at + Duration::from_secs(SPACE_TRASH_RETENTION_DAYS * 86_400);
|
||||||
|
Self { space, tabs, archived_tabs, trashed_at, purge_at }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn space(&self) -> &Space {
|
||||||
|
&self.space
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn tabs(&self) -> &[BrowserTab] {
|
||||||
|
&self.tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn archived_tabs(&self) -> &[ArchivedTab] {
|
||||||
|
&self.archived_tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn trashed_at(&self) -> SystemTime {
|
||||||
|
self.trashed_at
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn purge_at(&self) -> SystemTime {
|
||||||
|
self.purge_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BrowserCore {
|
impl BrowserCore {
|
||||||
|
pub fn trash_space(
|
||||||
|
&mut self,
|
||||||
|
space_id: &SpaceId,
|
||||||
|
trashed_at: SystemTime,
|
||||||
|
) -> Result<bool, CoreError> {
|
||||||
|
if self.spaces.len() <= 1 {
|
||||||
|
return Err(CoreError::LastSpaceCannotBeTrashed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(space_index) = self.spaces.iter().position(|space| space.id() == space_id) else {
|
||||||
|
return Err(CoreError::SpaceNotFound { id: space_id.clone() });
|
||||||
|
};
|
||||||
|
|
||||||
|
let trashed_space = self.spaces.remove(space_index);
|
||||||
|
let tabs = self.remove_space_tabs(space_id);
|
||||||
|
let archived_tabs = self.remove_space_archived_tabs(space_id);
|
||||||
|
self.active_tabs_by_space.remove(space_id);
|
||||||
|
self.active_tabs_by_space_profile
|
||||||
|
.retain(|(mapped_space_id, _), _| mapped_space_id != space_id);
|
||||||
|
self.trashed_spaces.push(TrashedSpace::new(trashed_space, tabs, archived_tabs, trashed_at));
|
||||||
|
|
||||||
|
if &self.active_space_id == space_id {
|
||||||
|
let next_space_id = self
|
||||||
|
.sorted_spaces()
|
||||||
|
.first()
|
||||||
|
.map(|space| space.id().clone())
|
||||||
|
.ok_or(CoreError::MissingActiveTab)?;
|
||||||
|
self.select_space(&next_space_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_trashed_space(&mut self, space_id: &SpaceId) -> Result<bool, CoreError> {
|
||||||
|
let Some(trash_index) =
|
||||||
|
self.trashed_spaces.iter().position(|entry| entry.space().id() == space_id)
|
||||||
|
else {
|
||||||
|
return Err(CoreError::TrashedSpaceNotFound { id: space_id.clone() });
|
||||||
|
};
|
||||||
|
|
||||||
|
let trashed_space = self.trashed_spaces.remove(trash_index);
|
||||||
|
let restored_space_id = trashed_space.space().id().clone();
|
||||||
|
self.spaces.push(trashed_space.space);
|
||||||
|
self.tabs.extend(trashed_space.tabs);
|
||||||
|
self.archived_tabs.extend(trashed_space.archived_tabs);
|
||||||
|
self.select_space(&restored_space_id)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_space_up(&mut self, space_id: &SpaceId) -> Result<bool, CoreError> {
|
pub fn move_space_up(&mut self, space_id: &SpaceId) -> Result<bool, CoreError> {
|
||||||
let mut ordered_ids = self.sorted_space_ids();
|
let mut ordered_ids = self.sorted_space_ids();
|
||||||
let Some(index) = ordered_ids.iter().position(|id| id == space_id) else {
|
let Some(index) = ordered_ids.iter().position(|id| id == space_id) else {
|
||||||
@@ -52,4 +151,30 @@ impl BrowserCore {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_space_tabs(&mut self, space_id: &SpaceId) -> Vec<BrowserTab> {
|
||||||
|
let mut removed_tabs = Vec::new();
|
||||||
|
self.tabs.retain(|tab| {
|
||||||
|
if tab.space_id() == space_id {
|
||||||
|
removed_tabs.push(tab.clone());
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
removed_tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_space_archived_tabs(&mut self, space_id: &SpaceId) -> Vec<ArchivedTab> {
|
||||||
|
let mut removed_tabs = Vec::new();
|
||||||
|
self.archived_tabs.retain(|archived| {
|
||||||
|
if archived.tab().space_id() == space_id {
|
||||||
|
removed_tabs.push(archived.clone());
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
removed_tabs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
use std::error::Error;
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
||||||
use ely_domain::{ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, ProfileId, ProfileKind, SpaceId};
|
use ely_domain::{
|
||||||
|
ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, ProfileId, ProfileKind, SpaceId, UrlText,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn created_space_binds_current_profile_as_default() -> Result<(), Box<dyn Error>> {
|
fn created_space_binds_current_profile_as_default() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -130,6 +135,80 @@ fn moving_boundary_or_missing_space_is_safe() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trashing_space_removes_it_and_restores_with_tabs() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let work_space_id = core.snapshot()?.active_space_id;
|
||||||
|
let research_space_id = core.create_space("Research", "R", 0x9fc9a2)?;
|
||||||
|
let research_tab_id = core.open_tab(UrlText::parse("https://servo.org")?);
|
||||||
|
|
||||||
|
core.select_space(&work_space_id)?;
|
||||||
|
assert!(core.trash_space(&research_space_id, SystemTime::UNIX_EPOCH)?);
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
assert_eq!(snapshot.active_space_id, work_space_id);
|
||||||
|
assert!(snapshot.spaces.iter().all(|space| space.id() != &research_space_id));
|
||||||
|
assert_eq!(snapshot.trashed_spaces[0].space().id(), &research_space_id);
|
||||||
|
assert!(snapshot.trashed_spaces[0].tabs().iter().any(|tab| tab.id() == &research_tab_id));
|
||||||
|
|
||||||
|
assert!(core.restore_trashed_space(&research_space_id)?);
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
assert_eq!(snapshot.active_space_id, research_space_id);
|
||||||
|
assert!(snapshot.tabs.iter().any(|tab| tab.id() == &research_tab_id));
|
||||||
|
assert!(snapshot.trashed_spaces.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trashing_active_space_selects_remaining_space() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let work_space_id = core.snapshot()?.active_space_id;
|
||||||
|
let research_space_id = core.create_space("Research", "R", 0x9fc9a2)?;
|
||||||
|
|
||||||
|
assert!(core.trash_space(&research_space_id, SystemTime::UNIX_EPOCH)?);
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(snapshot.active_space_id, work_space_id);
|
||||||
|
assert_eq!(snapshot.active_space_name, "Work");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trashed_space_keeps_thirty_day_retention() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let research_space_id = core.create_space("Research", "R", 0x9fc9a2)?;
|
||||||
|
let trashed_at = SystemTime::UNIX_EPOCH + Duration::from_secs(900);
|
||||||
|
|
||||||
|
core.trash_space(&research_space_id, trashed_at)?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
let trashed_space = &snapshot.trashed_spaces[0];
|
||||||
|
|
||||||
|
assert_eq!(trashed_space.trashed_at(), trashed_at);
|
||||||
|
assert_eq!(trashed_space.purge_at(), trashed_at + Duration::from_secs(30 * 86_400));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trashing_last_or_missing_space_is_rejected() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let work_space_id = core.snapshot()?.active_space_id;
|
||||||
|
let error = match core.trash_space(&work_space_id, SystemTime::UNIX_EPOCH) {
|
||||||
|
Err(error) => error,
|
||||||
|
Ok(_) => return Err("trashing the last space should fail".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(error, CoreError::LastSpaceCannotBeTrashed);
|
||||||
|
|
||||||
|
let missing_space_id = SpaceId::new();
|
||||||
|
core.create_space("Research", "R", 0x9fc9a2)?;
|
||||||
|
let error = match core.trash_space(&missing_space_id, SystemTime::UNIX_EPOCH) {
|
||||||
|
Err(error) => error,
|
||||||
|
Ok(_) => return Err("trashing a missing space should fail".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(error, CoreError::SpaceNotFound { id: missing_space_id });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn space_default_profile_updates_with_profile_validation() -> Result<(), Box<dyn Error>> {
|
fn space_default_profile_updates_with_profile_validation() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user