Wire elyspace import export UI
This commit is contained in:
@@ -2,6 +2,14 @@ use ely_domain::CommandIntent;
|
|||||||
use gpui::{Context, Window};
|
use gpui::{Context, Window};
|
||||||
|
|
||||||
use super::ElyShell;
|
use super::ElyShell;
|
||||||
|
use ely_browser_core::SpaceImportProfileMapping;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum SpaceFileCommand {
|
||||||
|
ExportActiveSpace,
|
||||||
|
ImportToActiveProfile,
|
||||||
|
ImportPreservingProfiles,
|
||||||
|
}
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub(super) fn handle_shell_command_intent(
|
pub(super) fn handle_shell_command_intent(
|
||||||
@@ -17,6 +25,17 @@ impl ElyShell {
|
|||||||
if install_plugin_from_file_command(command) {
|
if install_plugin_from_file_command(command) {
|
||||||
self.choose_plugin_package(window, cx);
|
self.choose_plugin_package(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match space_file_command(command) {
|
||||||
|
Some(SpaceFileCommand::ExportActiveSpace) => self.export_active_space(window, cx),
|
||||||
|
Some(SpaceFileCommand::ImportToActiveProfile) => {
|
||||||
|
self.choose_space_import(SpaceImportProfileMapping::UseActiveProfile, window, cx);
|
||||||
|
}
|
||||||
|
Some(SpaceFileCommand::ImportPreservingProfiles) => {
|
||||||
|
self.choose_space_import(SpaceImportProfileMapping::PreserveExisting, window, cx);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +49,26 @@ fn install_plugin_from_file_command(command: &str) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn space_file_command(command: &str) -> Option<SpaceFileCommand> {
|
||||||
|
match command.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"export-space" | "export space" | "export-active-space" | "export active space" => {
|
||||||
|
Some(SpaceFileCommand::ExportActiveSpace)
|
||||||
|
}
|
||||||
|
"import-space"
|
||||||
|
| "import space"
|
||||||
|
| "import-space-active-profile"
|
||||||
|
| "import space active profile" => Some(SpaceFileCommand::ImportToActiveProfile),
|
||||||
|
"import-space-with-profiles"
|
||||||
|
| "import space with profiles"
|
||||||
|
| "import-space-preserve-profiles"
|
||||||
|
| "import space preserve profiles" => Some(SpaceFileCommand::ImportPreservingProfiles),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::install_plugin_from_file_command;
|
use super::{SpaceFileCommand, install_plugin_from_file_command, space_file_command};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn install_plugin_from_file_command_matches_prd_aliases() {
|
fn install_plugin_from_file_command_matches_prd_aliases() {
|
||||||
@@ -46,4 +82,23 @@ mod tests {
|
|||||||
assert!(!install_plugin_from_file_command("plugins"));
|
assert!(!install_plugin_from_file_command("plugins"));
|
||||||
assert!(!install_plugin_from_file_command("open plugins"));
|
assert!(!install_plugin_from_file_command("open plugins"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_file_command_matches_export_and_import_aliases() {
|
||||||
|
assert_eq!(space_file_command("export-space"), Some(SpaceFileCommand::ExportActiveSpace));
|
||||||
|
assert_eq!(
|
||||||
|
space_file_command("import space"),
|
||||||
|
Some(SpaceFileCommand::ImportToActiveProfile)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
space_file_command("import-space-with-profiles"),
|
||||||
|
Some(SpaceFileCommand::ImportPreservingProfiles)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_file_command_rejects_other_space_commands() {
|
||||||
|
assert_eq!(space_file_command("new-space Research"), None);
|
||||||
|
assert_eq!(space_file_command("spaces"), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ mod sidebar_tabs;
|
|||||||
mod site_permissions_settings;
|
mod site_permissions_settings;
|
||||||
mod site_settings;
|
mod site_settings;
|
||||||
mod sleep;
|
mod sleep;
|
||||||
|
mod space_actions;
|
||||||
mod spaces;
|
mod spaces;
|
||||||
mod sync;
|
mod sync;
|
||||||
mod tab_context;
|
mod tab_context;
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
use ely_design_system::colors;
|
||||||
|
use ely_domain::SpaceId;
|
||||||
|
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, rgb};
|
||||||
|
use gpui_component::{
|
||||||
|
Disableable, IconName, Sizable, StyledExt,
|
||||||
|
button::{Button, ButtonVariants},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::ElyShell;
|
||||||
|
|
||||||
|
pub(super) fn render_space_actions(
|
||||||
|
index: usize,
|
||||||
|
space_count: usize,
|
||||||
|
space_id: SpaceId,
|
||||||
|
active: bool,
|
||||||
|
confirming_trash: bool,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
if confirming_trash {
|
||||||
|
return render_trash_confirmation(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let can_move_up = index > 0;
|
||||||
|
let can_move_down = index + 1 < space_count;
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(render_space_order_button(
|
||||||
|
("move-space-up", index),
|
||||||
|
space_id.clone(),
|
||||||
|
IconName::ArrowUp,
|
||||||
|
"Move Space Up",
|
||||||
|
can_move_up,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.child(render_space_order_button(
|
||||||
|
("move-space-down", index),
|
||||||
|
space_id.clone(),
|
||||||
|
IconName::ArrowDown,
|
||||||
|
"Move Space Down",
|
||||||
|
can_move_down,
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.child(render_space_export_button(index, space_id.clone(), 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_space_order_button(
|
||||||
|
id: (&'static str, usize),
|
||||||
|
space_id: SpaceId,
|
||||||
|
icon: IconName,
|
||||||
|
tooltip: &'static str,
|
||||||
|
enabled: bool,
|
||||||
|
moves_up: bool,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
Button::new(id)
|
||||||
|
.small()
|
||||||
|
.ghost()
|
||||||
|
.icon(icon)
|
||||||
|
.tooltip(tooltip)
|
||||||
|
.disabled(!enabled)
|
||||||
|
.on_click(cx.listener(move |shell, _, _, cx| {
|
||||||
|
if moves_up {
|
||||||
|
shell.move_space_up(&space_id, cx);
|
||||||
|
} else {
|
||||||
|
shell.move_space_down(&space_id, cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_space_export_button(
|
||||||
|
index: usize,
|
||||||
|
space_id: SpaceId,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
Button::new(("export-space", index))
|
||||||
|
.small()
|
||||||
|
.ghost()
|
||||||
|
.icon(IconName::File)
|
||||||
|
.label("Export")
|
||||||
|
.tooltip("Export .elyspace")
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.export_space(&space_id, window, cx);
|
||||||
|
}))
|
||||||
|
.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(
|
||||||
|
index: usize,
|
||||||
|
space_id: SpaceId,
|
||||||
|
active: bool,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
if active {
|
||||||
|
return div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::SUCCESS))
|
||||||
|
.child("Active")
|
||||||
|
.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
Button::new(("switch-space", index))
|
||||||
|
.small()
|
||||||
|
.primary()
|
||||||
|
.icon(IconName::Check)
|
||||||
|
.label("Switch")
|
||||||
|
.tooltip("Switch Space")
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.select_space(&space_id, window, cx);
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use ely_browser_core::{BrowserSnapshot, TrashedSpace};
|
use ely_browser_core::{BrowserSnapshot, SpaceImportProfileMapping, 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::{
|
||||||
@@ -6,12 +6,12 @@ use gpui::{
|
|||||||
px, rgb,
|
px, rgb,
|
||||||
};
|
};
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
Disableable, IconName, Sizable, StyledExt,
|
IconName, Sizable, StyledExt,
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
scroll::ScrollableElement,
|
scroll::ScrollableElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ElyShell, render_canvas_surface};
|
use super::{ElyShell, render_canvas_surface, space_actions::render_space_actions};
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub(super) fn render_spaces_page(
|
pub(super) fn render_spaces_page(
|
||||||
@@ -26,7 +26,11 @@ impl ElyShell {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_5()
|
.gap_5()
|
||||||
.child(render_spaces_header(snapshot))
|
.child(render_spaces_header(snapshot, cx))
|
||||||
|
.child(render_space_file_message(
|
||||||
|
self.space_file_notice.as_deref(),
|
||||||
|
self.space_file_error.as_deref(),
|
||||||
|
))
|
||||||
.child(render_active_space_summary(snapshot))
|
.child(render_active_space_summary(snapshot))
|
||||||
.child(render_spaces_list(snapshot, self.pending_space_trash.as_ref(), cx))
|
.child(render_spaces_list(snapshot, self.pending_space_trash.as_ref(), cx))
|
||||||
.child(render_trashed_spaces_list(snapshot, cx)),
|
.child(render_trashed_spaces_list(snapshot, cx)),
|
||||||
@@ -34,7 +38,7 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_spaces_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
fn render_spaces_header(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_end()
|
.items_end()
|
||||||
@@ -55,6 +59,41 @@ fn render_spaces_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
|||||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Button::new("import-space-active-profile")
|
||||||
|
.small()
|
||||||
|
.primary()
|
||||||
|
.icon(IconName::FolderOpen)
|
||||||
|
.label("Import")
|
||||||
|
.tooltip("Import .elyspace to Active Profile")
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.choose_space_import(
|
||||||
|
SpaceImportProfileMapping::UseActiveProfile,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("import-space-preserve-profiles")
|
||||||
|
.small()
|
||||||
|
.ghost()
|
||||||
|
.icon(IconName::User)
|
||||||
|
.label("Import Profiles")
|
||||||
|
.tooltip("Import .elyspace with Existing Profiles")
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.choose_space_import(
|
||||||
|
SpaceImportProfileMapping::PreserveExisting,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -65,10 +104,36 @@ fn render_spaces_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
|||||||
.text_color(rgb(colors::MUTED))
|
.text_color(rgb(colors::MUTED))
|
||||||
.child(IconName::GalleryVerticalEnd)
|
.child(IconName::GalleryVerticalEnd)
|
||||||
.child(format!("{} spaces", snapshot.spaces.len())),
|
.child(format!("{} spaces", snapshot.spaces.len())),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_space_file_message(notice: Option<&str>, error: Option<&str>) -> AnyElement {
|
||||||
|
let (message, color, icon) = if let Some(error) = error {
|
||||||
|
(error, colors::ERROR, IconName::TriangleAlert)
|
||||||
|
} else if let Some(notice) = notice {
|
||||||
|
(notice, colors::SUCCESS, IconName::CircleCheck)
|
||||||
|
} else {
|
||||||
|
return div().into_any_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(color))
|
||||||
|
.px_4()
|
||||||
|
.py_3()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(rgb(color))
|
||||||
|
.child(icon)
|
||||||
|
.child(message.to_string())
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_active_space_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
fn render_active_space_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
let Some(active_space) =
|
let Some(active_space) =
|
||||||
snapshot.spaces.iter().find(|space| space.id() == &snapshot.active_space_id)
|
snapshot.spaces.iter().find(|space| space.id() == &snapshot.active_space_id)
|
||||||
@@ -198,142 +263,6 @@ fn render_space_row(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_space_actions(
|
|
||||||
index: usize,
|
|
||||||
space_count: usize,
|
|
||||||
space_id: SpaceId,
|
|
||||||
active: bool,
|
|
||||||
confirming_trash: bool,
|
|
||||||
cx: &mut Context<ElyShell>,
|
|
||||||
) -> AnyElement {
|
|
||||||
if confirming_trash {
|
|
||||||
return render_trash_confirmation(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let can_move_up = index > 0;
|
|
||||||
let can_move_down = index + 1 < space_count;
|
|
||||||
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.child(render_space_order_button(
|
|
||||||
("move-space-up", index),
|
|
||||||
space_id.clone(),
|
|
||||||
IconName::ArrowUp,
|
|
||||||
"Move Space Up",
|
|
||||||
can_move_up,
|
|
||||||
true,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
.child(render_space_order_button(
|
|
||||||
("move-space-down", index),
|
|
||||||
space_id.clone(),
|
|
||||||
IconName::ArrowDown,
|
|
||||||
"Move Space Down",
|
|
||||||
can_move_down,
|
|
||||||
false,
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_space_order_button(
|
|
||||||
id: (&'static str, usize),
|
|
||||||
space_id: SpaceId,
|
|
||||||
icon: IconName,
|
|
||||||
tooltip: &'static str,
|
|
||||||
enabled: bool,
|
|
||||||
moves_up: bool,
|
|
||||||
cx: &mut Context<ElyShell>,
|
|
||||||
) -> AnyElement {
|
|
||||||
Button::new(id)
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.icon(icon)
|
|
||||||
.tooltip(tooltip)
|
|
||||||
.disabled(!enabled)
|
|
||||||
.on_click(cx.listener(move |shell, _, _, cx| {
|
|
||||||
if moves_up {
|
|
||||||
shell.move_space_up(&space_id, cx);
|
|
||||||
} else {
|
|
||||||
shell.move_space_down(&space_id, cx);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.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(
|
|
||||||
index: usize,
|
|
||||||
space_id: SpaceId,
|
|
||||||
active: bool,
|
|
||||||
cx: &mut Context<ElyShell>,
|
|
||||||
) -> AnyElement {
|
|
||||||
if active {
|
|
||||||
return div()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(rgb(colors::SUCCESS))
|
|
||||||
.child("Active")
|
|
||||||
.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
Button::new(("switch-space", index))
|
|
||||||
.small()
|
|
||||||
.primary()
|
|
||||||
.icon(IconName::Check)
|
|
||||||
.label("Switch")
|
|
||||||
.tooltip("Switch Space")
|
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
||||||
shell.select_space(&space_id, window, cx);
|
|
||||||
}))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_trashed_spaces_list(
|
fn render_trashed_spaces_list(
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ mod downloads;
|
|||||||
mod focus;
|
mod focus;
|
||||||
mod history;
|
mod history;
|
||||||
mod internal_pages;
|
mod internal_pages;
|
||||||
|
mod navigation;
|
||||||
mod notes;
|
mod notes;
|
||||||
mod plugins;
|
mod plugins;
|
||||||
mod reading_list;
|
mod reading_list;
|
||||||
mod render;
|
mod render;
|
||||||
mod sidebar;
|
mod sidebar;
|
||||||
mod site_permissions;
|
mod site_permissions;
|
||||||
|
mod space_files;
|
||||||
mod spaces;
|
mod spaces;
|
||||||
mod splits;
|
mod splits;
|
||||||
mod tab_groups;
|
mod tab_groups;
|
||||||
@@ -28,10 +30,9 @@ use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
|||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
ArchivePolicy, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, NewTabDestination,
|
ArchivePolicy, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, NewTabDestination,
|
||||||
ProfileId, ProfileSyncPolicy, SearchEngine, SpaceId, SyncObjectKind, SyncObjectPolicy, TabId,
|
ProfileId, ProfileSyncPolicy, SearchEngine, SpaceId, SyncObjectKind, SyncObjectPolicy, TabId,
|
||||||
UrlText,
|
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Window};
|
use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Window};
|
||||||
use gpui_component::input::{InputEvent, InputState, SelectAll};
|
use gpui_component::input::{InputEvent, InputState};
|
||||||
|
|
||||||
use bookmarks::PendingBookmarkEdit;
|
use bookmarks::PendingBookmarkEdit;
|
||||||
use downloads::PendingDownloadFileAction;
|
use downloads::PendingDownloadFileAction;
|
||||||
@@ -62,6 +63,8 @@ pub struct ElyShell {
|
|||||||
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_space_trash: Option<SpaceId>,
|
||||||
|
space_file_error: Option<String>,
|
||||||
|
space_file_notice: Option<String>,
|
||||||
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>,
|
||||||
@@ -130,6 +133,8 @@ impl ElyShell {
|
|||||||
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_space_trash: None,
|
||||||
|
space_file_error: None,
|
||||||
|
space_file_notice: 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,
|
||||||
@@ -140,54 +145,6 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_new_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state
|
|
||||||
&& core.open_new_tab().is_ok()
|
|
||||||
{
|
|
||||||
self.sync_address_input(window, cx);
|
|
||||||
self.focus_address_bar(window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_downloads(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_internal_tab("ely://downloads", window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_internal_tab("ely://history", window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_internal_tab("ely://settings", window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_task_manager(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_internal_tab("ely://task-manager", window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_internal_tab(&mut self, url_text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if let Ok(url) = UrlText::parse(url_text) {
|
|
||||||
self.open_url(url, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_url(&mut self, url: UrlText, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state {
|
|
||||||
core.open_tab(url);
|
|
||||||
self.sync_address_input(window, cx);
|
|
||||||
self.focus_address_bar(window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_address_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.command_input.update(cx, |input, cx| {
|
|
||||||
input.focus(window, cx);
|
|
||||||
});
|
|
||||||
window.dispatch_action(Box::new(SelectAll), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_command_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn focus_command_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let ShellState::Ready(core) = &mut self.state {
|
if let ShellState::Ready(core) = &mut self.state {
|
||||||
core.set_command_query(">");
|
core.set_command_query(">");
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use ely_domain::UrlText;
|
||||||
|
use gpui::{Context, Window};
|
||||||
|
use gpui_component::input::SelectAll;
|
||||||
|
|
||||||
|
use super::{ElyShell, ShellState};
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn active_tab_matches_url(&self, url: &str) -> bool {
|
||||||
|
match &self.state {
|
||||||
|
ShellState::Ready(core) => core.active_tab().is_ok_and(|tab| tab.url().as_str() == url),
|
||||||
|
ShellState::StartupError(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_new_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.open_new_tab().is_ok()
|
||||||
|
{
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
self.focus_address_bar(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_downloads(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.open_internal_tab("ely://downloads", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.open_internal_tab("ely://history", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.open_internal_tab("ely://settings", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_task_manager(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.open_internal_tab("ely://task-manager", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_internal_tab(
|
||||||
|
&mut self,
|
||||||
|
url_text: &str,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if let Ok(url) = UrlText::parse(url_text) {
|
||||||
|
self.open_url(url, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_url(&mut self, url: UrlText, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state {
|
||||||
|
core.open_tab(url);
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
self.focus_address_bar(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn focus_address_bar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.command_input.update(cx, |input, cx| {
|
||||||
|
input.focus(window, cx);
|
||||||
|
});
|
||||||
|
window.dispatch_action(Box::new(SelectAll), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,13 +108,6 @@ impl ElyShell {
|
|||||||
self.open_internal_tab(PLUGIN_SETTINGS_URL, window, cx);
|
self.open_internal_tab(PLUGIN_SETTINGS_URL, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_tab_matches_url(&self, url: &str) -> bool {
|
|
||||||
match &self.state {
|
|
||||||
ShellState::Ready(core) => core.active_tab().is_ok_and(|tab| tab.url().as_str() == url),
|
|
||||||
ShellState::StartupError(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn confirm_plugin_install(&mut self, cx: &mut Context<Self>) {
|
pub(super) fn confirm_plugin_install(&mut self, cx: &mut Context<Self>) {
|
||||||
let Some(pending) = self.pending_plugin_install.take() else {
|
let Some(pending) = self.pending_plugin_install.take() else {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use directories::UserDirs;
|
||||||
|
use ely_browser_core::{ELYSPACE_FILE_EXTENSION, SpaceImportProfileMapping};
|
||||||
|
use ely_domain::SpaceId;
|
||||||
|
use gpui::{Context, PathPromptOptions, Window};
|
||||||
|
|
||||||
|
use super::{ElyShell, ShellState};
|
||||||
|
|
||||||
|
const SPACE_SETTINGS_URL: &str = "ely://settings/spaces";
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn export_active_space(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let space_id = match &self.state {
|
||||||
|
ShellState::Ready(core) => match core.snapshot() {
|
||||||
|
Ok(snapshot) => snapshot.active_space_id,
|
||||||
|
Err(error) => {
|
||||||
|
self.set_space_file_error(error.to_string(), cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ShellState::StartupError(message) => {
|
||||||
|
self.set_space_file_error(message.clone(), cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.export_space(&space_id, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn export_space(
|
||||||
|
&mut self,
|
||||||
|
space_id: &SpaceId,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.ensure_space_settings_surface(window, cx);
|
||||||
|
self.clear_space_file_message();
|
||||||
|
|
||||||
|
let export = match &mut self.state {
|
||||||
|
ShellState::Ready(core) => {
|
||||||
|
let package_json = match core.export_space_package_json(space_id) {
|
||||||
|
Ok(package_json) => package_json,
|
||||||
|
Err(error) => {
|
||||||
|
self.set_space_file_error(error.to_string(), cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let package = match core.export_space_package(space_id) {
|
||||||
|
Ok(package) => package,
|
||||||
|
Err(error) => {
|
||||||
|
self.set_space_file_error(error.to_string(), cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok((package.space_name().to_string(), package_json))
|
||||||
|
}
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (space_name, package_json) = match export {
|
||||||
|
Ok(export) => export,
|
||||||
|
Err(error) => {
|
||||||
|
self.set_space_file_error(error, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let directory = match default_export_directory() {
|
||||||
|
Ok(directory) => directory,
|
||||||
|
Err(error) => {
|
||||||
|
self.set_space_file_error(error, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let suggested_name = space_export_filename(&space_name);
|
||||||
|
let prompt = cx.prompt_for_new_path(&directory, Some(&suggested_name));
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |shell, window| {
|
||||||
|
let selected_path = match prompt.await {
|
||||||
|
Ok(Ok(path)) => path,
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.set_space_file_error(error.to_string(), cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.set_space_file_error(error.to_string(), cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(path) = selected_path else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = window
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move { write_space_package(path, package_json) })
|
||||||
|
.await;
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.handle_space_export_result(result, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn choose_space_import(
|
||||||
|
&mut self,
|
||||||
|
profile_mapping: SpaceImportProfileMapping,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.ensure_space_settings_surface(window, cx);
|
||||||
|
self.clear_space_file_message();
|
||||||
|
|
||||||
|
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: false,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Select .elyspace file".into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |shell, window| {
|
||||||
|
let selected_path = match prompt.await {
|
||||||
|
Ok(Ok(Some(paths))) => paths.into_iter().next(),
|
||||||
|
Ok(Ok(None)) => None,
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.set_space_file_error(error.to_string(), cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.set_space_file_error(error.to_string(), cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(path) = selected_path else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
window.background_executor().spawn(async move { read_space_package(path) }).await;
|
||||||
|
_ = shell.update_in(window, |shell, window, cx| {
|
||||||
|
shell.handle_space_import_result(result, profile_mapping, window, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_space_settings_surface(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.active_tab_matches_url(SPACE_SETTINGS_URL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.open_internal_tab(SPACE_SETTINGS_URL, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_space_file_message(&mut self) {
|
||||||
|
self.space_file_error = None;
|
||||||
|
self.space_file_notice = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_space_file_error(&mut self, message: String, cx: &mut Context<Self>) {
|
||||||
|
self.space_file_error = Some(message);
|
||||||
|
self.space_file_notice = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_space_file_notice(&mut self, message: String, cx: &mut Context<Self>) {
|
||||||
|
self.space_file_notice = Some(message);
|
||||||
|
self.space_file_error = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_space_export_result(
|
||||||
|
&mut self,
|
||||||
|
result: Result<PathBuf, String>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match result {
|
||||||
|
Ok(path) => self.set_space_file_notice(format!("Exported {}", path.display()), cx),
|
||||||
|
Err(error) => self.set_space_file_error(error, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_space_import_result(
|
||||||
|
&mut self,
|
||||||
|
result: Result<String, String>,
|
||||||
|
profile_mapping: SpaceImportProfileMapping,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let package_json = match result {
|
||||||
|
Ok(package_json) => package_json,
|
||||||
|
Err(error) => {
|
||||||
|
self.set_space_file_error(error, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let import_result = match &mut self.state {
|
||||||
|
ShellState::Ready(core) => core
|
||||||
|
.import_space_package_json(&package_json, profile_mapping)
|
||||||
|
.and_then(|space_id| {
|
||||||
|
core.snapshot().map(|snapshot| {
|
||||||
|
snapshot.spaces.iter().find(|space| space.id() == &space_id).map_or_else(
|
||||||
|
|| "Imported Space".to_string(),
|
||||||
|
|space| format!("Imported {}", space.name()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string()),
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match import_result {
|
||||||
|
Ok(message) => {
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
self.set_space_file_notice(message, cx);
|
||||||
|
}
|
||||||
|
Err(error) => self.set_space_file_error(error, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_export_directory() -> Result<PathBuf, String> {
|
||||||
|
UserDirs::new()
|
||||||
|
.and_then(|dirs| dirs.document_dir().map(Path::to_path_buf))
|
||||||
|
.ok_or_else(|| "Documents directory is unavailable.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_space_package(path: PathBuf, package_json: String) -> Result<PathBuf, String> {
|
||||||
|
let path = normalize_export_path(path)?;
|
||||||
|
fs::write(&path, package_json)
|
||||||
|
.map_err(|error| format!("Unable to write {}: {error}", path.display()))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_space_package(path: PathBuf) -> Result<String, String> {
|
||||||
|
if !path_has_elyspace_extension(&path) {
|
||||||
|
return Err("Selected file must use .elyspace extension.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::read_to_string(&path).map_err(|error| format!("Unable to read {}: {error}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_export_path(mut path: PathBuf) -> Result<PathBuf, String> {
|
||||||
|
if path.extension().is_none() {
|
||||||
|
path.set_extension(ELYSPACE_FILE_EXTENSION);
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if path_has_elyspace_extension(&path) {
|
||||||
|
Ok(path)
|
||||||
|
} else {
|
||||||
|
Err("Export path must use .elyspace extension.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_has_elyspace_extension(path: &Path) -> bool {
|
||||||
|
path.extension().is_some_and(|extension| {
|
||||||
|
extension.to_string_lossy().eq_ignore_ascii_case(ELYSPACE_FILE_EXTENSION)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn space_export_filename(space_name: &str) -> String {
|
||||||
|
let mut stem = String::new();
|
||||||
|
let mut previous_separator = false;
|
||||||
|
|
||||||
|
for ch in space_name.chars() {
|
||||||
|
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') {
|
||||||
|
stem.push(ch);
|
||||||
|
previous_separator = false;
|
||||||
|
} else if !previous_separator {
|
||||||
|
stem.push('-');
|
||||||
|
previous_separator = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stem = stem.trim_matches('-');
|
||||||
|
let stem = if stem.is_empty() { "space" } else { stem };
|
||||||
|
format!("{stem}.{ELYSPACE_FILE_EXTENSION}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::{normalize_export_path, space_export_filename};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_export_filename_sanitizes_names() {
|
||||||
|
assert_eq!(space_export_filename("Work"), "Work.elyspace");
|
||||||
|
assert_eq!(space_export_filename("Client / Research"), "Client-Research.elyspace");
|
||||||
|
assert_eq!(space_export_filename(" "), "space.elyspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_export_path_adds_missing_extension() -> Result<(), String> {
|
||||||
|
let path = normalize_export_path(PathBuf::from("Work"))?;
|
||||||
|
|
||||||
|
assert_eq!(path, PathBuf::from("Work.elyspace"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_export_path_rejects_other_extensions() {
|
||||||
|
let error = normalize_export_path(PathBuf::from("Work.json"));
|
||||||
|
|
||||||
|
assert_eq!(error, Err("Export path must use .elyspace extension.".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,6 +206,10 @@ pub(crate) fn plugin_settings_url() -> Result<UrlText, CoreError> {
|
|||||||
internal_page_url("ely://settings/plugins")
|
internal_page_url("ely://settings/plugins")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn space_settings_url() -> Result<UrlText, CoreError> {
|
||||||
|
internal_page_url("ely://settings/spaces")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn plugin_detail_url(plugin_id: &PluginId) -> Result<UrlText, CoreError> {
|
pub(crate) fn plugin_detail_url(plugin_id: &PluginId) -> Result<UrlText, CoreError> {
|
||||||
let route = format!("ely://plugin/{}", plugin_id.as_str());
|
let route = format!("ely://plugin/{}", plugin_id.as_str());
|
||||||
internal_page_url(&route)
|
internal_page_url(&route)
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ use crate::{
|
|||||||
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name, note_body,
|
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name, note_body,
|
||||||
notes_url, plugin_detail_url, plugin_settings_url, plugins_url, reading_list_url,
|
notes_url, plugin_detail_url, plugin_settings_url, plugins_url, reading_list_url,
|
||||||
reading_progress_percent, rename_tab_group_name, search_url, settings_page_url,
|
reading_progress_percent, rename_tab_group_name, search_url, settings_page_url,
|
||||||
settings_url, shortcut_settings_url, space_icon, split_group_name, switch_profile_name,
|
settings_url, shortcut_settings_url, space_icon, space_settings_url, split_group_name,
|
||||||
sync_status_url, tab_group_color_hex, tab_group_name, tab_note_body, task_manager_url,
|
switch_profile_name, sync_status_url, tab_group_color_hex, tab_group_name, tab_note_body,
|
||||||
|
task_manager_url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,6 +279,21 @@ impl BrowserCore {
|
|||||||
self.open_tab(plugin_settings_url()?);
|
self.open_tab(plugin_settings_url()?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
"export-space" | "export space" | "export-active-space" | "export active space" => {
|
||||||
|
self.open_tab(space_settings_url()?);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"import-space"
|
||||||
|
| "import space"
|
||||||
|
| "import-space-active-profile"
|
||||||
|
| "import space active profile"
|
||||||
|
| "import-space-with-profiles"
|
||||||
|
| "import space with profiles"
|
||||||
|
| "import-space-preserve-profiles"
|
||||||
|
| "import space preserve profiles" => {
|
||||||
|
self.open_tab(space_settings_url()?);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
"site-settings" | "open-site-settings" | "open site settings" => {
|
"site-settings" | "open-site-settings" | "open site settings" => {
|
||||||
let Some(url) = self.active_tab_site_settings_url()? else {
|
let Some(url) = self.active_tab_site_settings_url()? else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
|
use ely_domain::CommandIntent;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_space_command_opens_space_settings_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query(">export-space");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let active_tab = core.active_tab()?;
|
||||||
|
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("export-space".to_string())));
|
||||||
|
assert_eq!(active_tab.title(), "Space Settings");
|
||||||
|
assert_eq!(active_tab.url().as_str(), "ely://settings/spaces");
|
||||||
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_space_command_opens_space_settings_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query(">import-space-with-profiles");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let active_tab = core.active_tab()?;
|
||||||
|
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("import-space-with-profiles".to_string())));
|
||||||
|
assert_eq!(active_tab.title(), "Space Settings");
|
||||||
|
assert_eq!(active_tab.url().as_str(), "ely://settings/spaces");
|
||||||
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user