Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
use warp_core::ui::appearance::Appearance;
|
||||
use warp_editor::editor::NavigationKey;
|
||||
use warpui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, Flex, MouseStateHandle, ParentElement,
|
||||
Radius, Shrinkable,
|
||||
},
|
||||
ui_components::{
|
||||
button::ButtonVariant,
|
||||
components::{UiComponent, UiComponentStyles},
|
||||
},
|
||||
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
|
||||
use crate::editor::{
|
||||
EditorOptions, EditorView, Event, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
|
||||
TextOptions,
|
||||
};
|
||||
|
||||
use super::EnvVarSecretCommand;
|
||||
|
||||
const COMMAND_EDITOR_MIN_LINES: f32 = 6.;
|
||||
const SPAN_FONT_SIZE: f32 = 16.;
|
||||
const BUTTON_FONT_SIZE: f32 = 14.;
|
||||
const CORE_WIDTH: f32 = 400.;
|
||||
const CORE_HEIGHT: f32 = 250.;
|
||||
const EDITOR_FONT_SIZE: f32 = 14.;
|
||||
const CONTAINER_PADDING: f32 = 25.;
|
||||
const ELEMENT_SPACING: f32 = 10.;
|
||||
const EDITOR_DIVIDE: f32 = 6.;
|
||||
|
||||
const SECRET_SPAN: &str = "Secret command";
|
||||
const SAVE_BUTTON_LABEL: &str = "Save";
|
||||
const CANCEL_BUTTON_LABEL: &str = "Cancel";
|
||||
const NAME_PLACEHOLDER_TEXT: &str = "Name";
|
||||
const COMMAND_PLACEHOLDER_TEXT: &str = "Command";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EnvVarCommandDialogAction {
|
||||
Close,
|
||||
SaveCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EnvVarCommandDialogEvent {
|
||||
Close,
|
||||
SaveCommand(EnvVarSecretCommand),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseStateHandles {
|
||||
cancel_button_mouse_state_handle: MouseStateHandle,
|
||||
save_button_mouse_state_handle: MouseStateHandle,
|
||||
}
|
||||
|
||||
pub struct EnvVarCommandDialog {
|
||||
mouse_state_handles: MouseStateHandles,
|
||||
name_editor: ViewHandle<EditorView>,
|
||||
command_editor: ViewHandle<EditorView>,
|
||||
}
|
||||
|
||||
impl EnvVarCommandDialog {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let name_editor = {
|
||||
ctx.add_typed_action_view(|ctx| {
|
||||
let appearance = Appearance::as_ref(ctx);
|
||||
let options = SingleLineEditorOptions {
|
||||
text: TextOptions::ui_text(Some(EDITOR_FONT_SIZE), appearance),
|
||||
propagate_and_no_op_vertical_navigation_keys:
|
||||
PropagateAndNoOpNavigationKeys::Always,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut editor = EditorView::single_line(options, ctx);
|
||||
editor.set_placeholder_text(NAME_PLACEHOLDER_TEXT, ctx);
|
||||
editor
|
||||
})
|
||||
};
|
||||
|
||||
ctx.subscribe_to_view(&name_editor, |me, _, event, ctx| {
|
||||
me.handle_name_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
let command_editor = {
|
||||
ctx.add_typed_action_view(|ctx| {
|
||||
let appearance = Appearance::as_ref(ctx);
|
||||
let options = EditorOptions {
|
||||
text: TextOptions {
|
||||
font_size_override: Some(EDITOR_FONT_SIZE),
|
||||
font_family_override: Some(appearance.monospace_font_family()),
|
||||
..Default::default()
|
||||
},
|
||||
soft_wrap: true,
|
||||
autogrow: true,
|
||||
propagate_and_no_op_vertical_navigation_keys:
|
||||
PropagateAndNoOpNavigationKeys::Always,
|
||||
supports_vim_mode: false,
|
||||
single_line: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut editor = EditorView::new(options, ctx);
|
||||
editor.set_placeholder_text(COMMAND_PLACEHOLDER_TEXT, ctx);
|
||||
editor
|
||||
})
|
||||
};
|
||||
|
||||
ctx.subscribe_to_view(&command_editor, |me, _, event, ctx| {
|
||||
me.handle_command_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
Self {
|
||||
mouse_state_handles: Default::default(),
|
||||
name_editor,
|
||||
command_editor,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_name_editor_event(&mut self, event: &Event, ctx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
Event::Navigate(NavigationKey::Tab) | Event::Navigate(NavigationKey::ShiftTab) => {
|
||||
ctx.focus(&self.command_editor)
|
||||
}
|
||||
Event::Enter => {
|
||||
if !self.should_disable_save(ctx) {
|
||||
self.save_command_and_close(ctx)
|
||||
}
|
||||
}
|
||||
Event::Escape => self.close(ctx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_command_editor_event(&mut self, event: &Event, ctx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
Event::Navigate(NavigationKey::Tab) | Event::Navigate(NavigationKey::ShiftTab) => {
|
||||
ctx.focus(&self.name_editor)
|
||||
}
|
||||
Event::Enter => {
|
||||
if !self.should_disable_save(ctx) {
|
||||
self.save_command_and_close(ctx)
|
||||
}
|
||||
}
|
||||
Event::Escape => self.close(ctx),
|
||||
Event::Edited(_) => ctx.notify(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, secret_command: &EnvVarSecretCommand, ctx: &mut ViewContext<Self>) {
|
||||
self.name_editor.update(ctx, |buffer, ctx| {
|
||||
buffer.set_buffer_text(&secret_command.name, ctx)
|
||||
});
|
||||
|
||||
self.command_editor.update(ctx, |buffer, ctx| {
|
||||
buffer.set_buffer_text(&secret_command.command, ctx)
|
||||
});
|
||||
}
|
||||
|
||||
fn save_command_and_close(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.emit(EnvVarCommandDialogEvent::SaveCommand(EnvVarSecretCommand {
|
||||
name: self.name_editor.as_ref(ctx).buffer_text(ctx),
|
||||
command: self.command_editor.as_ref(ctx).buffer_text(ctx),
|
||||
}));
|
||||
self.close(ctx);
|
||||
}
|
||||
|
||||
fn close(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.name_editor
|
||||
.update(ctx, |buffer, ctx| buffer.clear_buffer(ctx));
|
||||
|
||||
self.command_editor
|
||||
.update(ctx, |buffer, ctx| buffer.clear_buffer(ctx));
|
||||
ctx.emit(EnvVarCommandDialogEvent::Close)
|
||||
}
|
||||
|
||||
fn should_disable_save(&self, app: &AppContext) -> bool {
|
||||
self.command_editor.as_ref(app).is_empty(app)
|
||||
}
|
||||
|
||||
fn render_button(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
button_mouse_state: MouseStateHandle,
|
||||
action: EnvVarCommandDialogAction,
|
||||
label_text: &str,
|
||||
is_save: bool,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let mut button = appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
if is_save {
|
||||
ButtonVariant::Accent
|
||||
} else {
|
||||
ButtonVariant::Secondary
|
||||
},
|
||||
button_mouse_state,
|
||||
)
|
||||
.with_centered_text_label(label_text.to_owned())
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(BUTTON_FONT_SIZE),
|
||||
font_weight: Some(warpui::fonts::Weight::Normal),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if is_save && self.should_disable_save(app) {
|
||||
button = button.disabled();
|
||||
};
|
||||
|
||||
button
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(action.clone()))
|
||||
.with_cursor(warpui::platform::Cursor::PointingHand)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_command_editor(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let line_height = self
|
||||
.command_editor
|
||||
.as_ref(app)
|
||||
.line_height(app.font_cache(), appearance);
|
||||
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(self.command_editor.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_height(COMMAND_EDITOR_MIN_LINES * line_height)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_bottom(ELEMENT_SPACING)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_name_editor(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(self.name_editor.clone())
|
||||
.with_style(UiComponentStyles::default())
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_bottom(EDITOR_DIVIDE)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_dialog_span(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.span(SECRET_SPAN)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(SPAN_FONT_SIZE),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_bottom(ELEMENT_SPACING)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for EnvVarCommandDialog {
|
||||
type Event = EnvVarCommandDialogEvent;
|
||||
}
|
||||
|
||||
impl View for EnvVarCommandDialog {
|
||||
fn ui_name() -> &'static str {
|
||||
"EnvVarCommandDialog"
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
|
||||
if focus_ctx.is_self_focused() {
|
||||
ctx.focus(&self.name_editor);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
|
||||
ConstrainedBox::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_child(self.render_dialog_span(appearance))
|
||||
.with_child(self.render_name_editor(appearance))
|
||||
.with_child(self.render_command_editor(appearance, app))
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(
|
||||
self.render_button(
|
||||
appearance,
|
||||
self.mouse_state_handles
|
||||
.cancel_button_mouse_state_handle
|
||||
.clone(),
|
||||
EnvVarCommandDialogAction::Close,
|
||||
CANCEL_BUTTON_LABEL,
|
||||
false,
|
||||
app,
|
||||
),
|
||||
)
|
||||
.with_margin_right(ELEMENT_SPACING)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.render_button(
|
||||
appearance,
|
||||
self.mouse_state_handles
|
||||
.save_button_mouse_state_handle
|
||||
.clone(),
|
||||
EnvVarCommandDialogAction::SaveCommand,
|
||||
SAVE_BUTTON_LABEL,
|
||||
true,
|
||||
app,
|
||||
),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
||||
.with_uniform_padding(CONTAINER_PADDING)
|
||||
.with_border(Border::all(2.).with_border_fill(appearance.theme().surface_2()))
|
||||
.with_background(appearance.theme().surface_1())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(CORE_WIDTH)
|
||||
.with_height(CORE_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for EnvVarCommandDialog {
|
||||
type Action = EnvVarCommandDialogAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
EnvVarCommandDialogAction::Close => self.close(ctx),
|
||||
EnvVarCommandDialogAction::SaveCommand => self.save_command_and_close(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use warpui::ViewContext;
|
||||
|
||||
use super::env_var_collection::{EnvVarCollectionView, VariableRowIndex};
|
||||
use crate::env_vars::{active_env_var_collection_data::SavingStatus, EnvVarValue};
|
||||
|
||||
mod command_dialog_view;
|
||||
pub(super) use command_dialog_view::{EnvVarCommandDialog, EnvVarCommandDialogEvent};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EnvVarSecretCommand {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
impl EnvVarCollectionView {
|
||||
pub(super) fn display_command_dialog(
|
||||
&mut self,
|
||||
index: Option<VariableRowIndex>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(VariableRowIndex(index)) = index {
|
||||
if let EnvVarValue::Command(cmd) = &self.variable_rows[index].value {
|
||||
self.env_var_command_dialog
|
||||
.update(ctx, |dialog, ctx| dialog.load(cmd, ctx))
|
||||
}
|
||||
}
|
||||
self.dialog_open_states.env_var_command_dialog_open = true;
|
||||
self.update_open_modal_state(ctx);
|
||||
ctx.focus(&self.env_var_command_dialog);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn handle_command_dialog_event(
|
||||
&mut self,
|
||||
event: &EnvVarCommandDialogEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EnvVarCommandDialogEvent::Close => {
|
||||
self.dialog_open_states.env_var_command_dialog_open = false;
|
||||
self.update_open_modal_state(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
EnvVarCommandDialogEvent::SaveCommand(command) => {
|
||||
self.save_command(command.clone(), ctx);
|
||||
self.set_saving_status(SavingStatus::Unsaved, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_command(&mut self, command: EnvVarSecretCommand, ctx: &mut ViewContext<Self>) {
|
||||
let row_index = self.pending_variable_row_index.take();
|
||||
|
||||
if let Some(VariableRowIndex(index)) = row_index {
|
||||
self.variable_rows[index].value = EnvVarValue::Command(command);
|
||||
}
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
use warp_editor::editor::NavigationKey;
|
||||
use warpui::{
|
||||
elements::{
|
||||
Align, ConstrainedBox, Container, Flex, ParentElement, SavePosition, Shrinkable, Stack,
|
||||
},
|
||||
fonts::FamilyId,
|
||||
ui_components::components::{Coords, UiComponent, UiComponentStyles},
|
||||
AppContext, Element, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::{
|
||||
EditOrigin, EditorOptions, EditorView, Event as EditorEvent, InteractionState,
|
||||
PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions,
|
||||
},
|
||||
env_vars::{
|
||||
active_env_var_collection_data::SavingStatus,
|
||||
view::env_var_collection::{
|
||||
EditorType, EnvVarCollectionView, DESCRIPTION_EDITOR_POSITION, ROW_SPACING,
|
||||
},
|
||||
EnvVarValue,
|
||||
},
|
||||
Appearance,
|
||||
};
|
||||
|
||||
// Metadata labels (name and description)
|
||||
const LABEL_FONT_SIZE: f32 = 12.;
|
||||
const METADATA_SPACING: f32 = 8.;
|
||||
const LAST_ROW_ELEMENT_SPACING: f32 = 2.;
|
||||
const TITLE_LABEL_TEXT: &str = "Title";
|
||||
const DESCRIPTION_LABEL_TEXT: &str = "Description";
|
||||
|
||||
const VERTICAL_TEXT_INPUT_PADDING: f32 = 5.;
|
||||
const HORIZONTAL_TEXT_INPUT_PADDING: f32 = 10.;
|
||||
const SECRET_ICON_BUTTON_MARGIN: f32 = 2.;
|
||||
|
||||
impl EnvVarCollectionView {
|
||||
pub(super) fn create_editor_handle(
|
||||
ctx: &mut ViewContext<Self>,
|
||||
font_size_override: Option<f32>,
|
||||
font_family_override: Option<FamilyId>,
|
||||
placeholder_text: Option<&str>,
|
||||
single_line: bool,
|
||||
) -> ViewHandle<EditorView> {
|
||||
let text = TextOptions {
|
||||
font_size_override,
|
||||
font_family_override,
|
||||
..Default::default()
|
||||
};
|
||||
ctx.add_typed_action_view(|ctx| {
|
||||
let mut editor = if single_line {
|
||||
EditorView::single_line(
|
||||
SingleLineEditorOptions {
|
||||
text,
|
||||
propagate_and_no_op_vertical_navigation_keys:
|
||||
PropagateAndNoOpNavigationKeys::Always,
|
||||
..Default::default()
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
} else {
|
||||
EditorView::new(
|
||||
EditorOptions {
|
||||
text,
|
||||
soft_wrap: true,
|
||||
autogrow: true,
|
||||
propagate_and_no_op_vertical_navigation_keys:
|
||||
PropagateAndNoOpNavigationKeys::Always,
|
||||
supports_vim_mode: false,
|
||||
single_line: false,
|
||||
..Default::default()
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(text) = placeholder_text {
|
||||
editor.set_placeholder_text(text, ctx);
|
||||
}
|
||||
|
||||
editor
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn editors_are_empty(&self, app: &AppContext) -> bool {
|
||||
self.variable_rows.iter().any(|row| {
|
||||
row.variable_name_editor.as_ref(app).is_empty(app)
|
||||
|| (row.variable_value_editor.as_ref(app).is_empty(app)
|
||||
&& matches!(row.value, EnvVarValue::Constant(_)))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn handle_title_editor_event(
|
||||
&mut self,
|
||||
event: &EditorEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Navigate(NavigationKey::ShiftTab) => {
|
||||
if self.variable_rows.is_empty() {
|
||||
ctx.focus(&self.description_editor);
|
||||
} else if let Some(variable_row) = self.variable_rows.last() {
|
||||
ctx.focus(&variable_row.variable_description_editor);
|
||||
}
|
||||
}
|
||||
EditorEvent::Navigate(NavigationKey::Tab) => {
|
||||
ctx.focus(&self.description_editor);
|
||||
}
|
||||
EditorEvent::ClearParentSelections => {
|
||||
self.clear_parent_selections(self.title_editor.clone(), ctx)
|
||||
}
|
||||
EditorEvent::Edited(EditOrigin::UserInitiated)
|
||||
| EditorEvent::Edited(EditOrigin::UserTyped) => {
|
||||
self.set_saving_status(SavingStatus::Unsaved, ctx);
|
||||
|
||||
let current_text = self.title_editor.as_ref(ctx).buffer_text(ctx);
|
||||
self.update_title_validation(¤t_text, ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_description_editor_event(
|
||||
&mut self,
|
||||
event: &EditorEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Navigate(NavigationKey::ShiftTab) => {
|
||||
ctx.focus(&self.title_editor);
|
||||
}
|
||||
EditorEvent::Navigate(NavigationKey::Tab) => {
|
||||
if self.variable_rows.is_empty() {
|
||||
ctx.focus(&self.title_editor);
|
||||
} else if let Some(variable_row) = self.variable_rows.first() {
|
||||
ctx.focus(&variable_row.variable_name_editor);
|
||||
}
|
||||
}
|
||||
EditorEvent::ClearParentSelections => {
|
||||
self.clear_parent_selections(self.description_editor.clone(), ctx)
|
||||
}
|
||||
EditorEvent::Edited(EditOrigin::UserInitiated)
|
||||
| EditorEvent::Edited(EditOrigin::UserTyped) => {
|
||||
self.set_saving_status(SavingStatus::Unsaved, ctx);
|
||||
|
||||
let current_text = self.description_editor.as_ref(ctx).buffer_text(ctx);
|
||||
self.update_description_validation(¤t_text, ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_variable_event(
|
||||
&mut self,
|
||||
handle: ViewHandle<EditorView>,
|
||||
event: &EditorEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Navigate(NavigationKey::ShiftTab) => {
|
||||
self.focus_prev_variable_editor(handle, ctx)
|
||||
}
|
||||
EditorEvent::Navigate(NavigationKey::Tab) => {
|
||||
self.focus_next_variable_editor(handle, ctx)
|
||||
}
|
||||
EditorEvent::ClearParentSelections => self.clear_parent_selections(handle.clone(), ctx),
|
||||
EditorEvent::Edited(EditOrigin::UserInitiated)
|
||||
| EditorEvent::Edited(EditOrigin::UserTyped) => {
|
||||
self.set_saving_status(SavingStatus::Unsaved, ctx);
|
||||
|
||||
if let Some((row_index, field_type)) = self.find_editor_info(&handle) {
|
||||
let current_text = handle.as_ref(ctx).buffer_text(ctx);
|
||||
self.update_field_validation(row_index, field_type, ¤t_text, ctx);
|
||||
}
|
||||
|
||||
ctx.notify()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_parent_selections(
|
||||
&mut self,
|
||||
editor: ViewHandle<EditorView>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if editor != self.title_editor {
|
||||
self.title_editor.update(ctx, |editor, ctx| {
|
||||
editor.clear_selections(ctx);
|
||||
})
|
||||
}
|
||||
|
||||
if editor != self.description_editor {
|
||||
self.description_editor.update(ctx, |editor, ctx| {
|
||||
editor.clear_selections(ctx);
|
||||
})
|
||||
}
|
||||
|
||||
self.variable_rows.iter().for_each(|var_editor| {
|
||||
if var_editor.variable_name_editor != editor {
|
||||
var_editor
|
||||
.variable_name_editor
|
||||
.update(ctx, |var_editor, ctx| {
|
||||
var_editor.clear_selections(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
if var_editor.variable_value_editor != editor {
|
||||
var_editor
|
||||
.variable_value_editor
|
||||
.update(ctx, |var_editor, ctx| {
|
||||
var_editor.clear_selections(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
if var_editor.variable_description_editor != editor {
|
||||
var_editor
|
||||
.variable_description_editor
|
||||
.update(ctx, |var_editor, ctx| {
|
||||
var_editor.clear_selections(ctx);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn focus_next_variable_editor(
|
||||
&self,
|
||||
handle: ViewHandle<EditorView>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let editor = self
|
||||
.variable_rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(index, editor)| {
|
||||
if editor.variable_name_editor == handle {
|
||||
Some((index, EditorType::Name))
|
||||
} else if editor.variable_value_editor == handle {
|
||||
Some((index, EditorType::Value))
|
||||
} else if editor.variable_description_editor == handle {
|
||||
Some((index, EditorType::Description))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match editor {
|
||||
Some((index, EditorType::Name)) => {
|
||||
if let Some(variable_row) = self.variable_rows.get(index) {
|
||||
if let EnvVarValue::Constant(_) = variable_row.value {
|
||||
ctx.focus(&variable_row.variable_value_editor);
|
||||
} else {
|
||||
ctx.focus(&variable_row.variable_description_editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((index, EditorType::Value)) => {
|
||||
if let Some(variable_row) = self.variable_rows.get(index) {
|
||||
ctx.focus(&variable_row.variable_description_editor);
|
||||
}
|
||||
}
|
||||
Some((index, EditorType::Description)) => {
|
||||
if index == self.variable_rows.len() - 1 {
|
||||
ctx.focus(&self.title_editor)
|
||||
} else if let Some(next_variable_row) = self.variable_rows.get(index + 1) {
|
||||
ctx.focus(&next_variable_row.variable_name_editor)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn focus_prev_variable_editor(
|
||||
&self,
|
||||
handle: ViewHandle<EditorView>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let editor = self
|
||||
.variable_rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(index, editor)| {
|
||||
if editor.variable_name_editor == handle {
|
||||
Some((index, EditorType::Name))
|
||||
} else if editor.variable_value_editor == handle {
|
||||
Some((index, EditorType::Value))
|
||||
} else if editor.variable_description_editor == handle {
|
||||
Some((index, EditorType::Description))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match editor {
|
||||
Some((index, EditorType::Name)) => {
|
||||
if index == 0 {
|
||||
ctx.focus(&self.description_editor)
|
||||
} else if let Some(prev_variable_row) = self.variable_rows.get(index - 1) {
|
||||
ctx.focus(&prev_variable_row.variable_description_editor);
|
||||
}
|
||||
}
|
||||
Some((index, EditorType::Value)) => {
|
||||
if let Some(variable_row) = self.variable_rows.get(index) {
|
||||
ctx.focus(&variable_row.variable_name_editor);
|
||||
}
|
||||
}
|
||||
Some((index, EditorType::Description)) => {
|
||||
if let Some(variable_row) = self.variable_rows.get(index) {
|
||||
if let EnvVarValue::Constant(_) = variable_row.value {
|
||||
ctx.focus(&variable_row.variable_value_editor);
|
||||
} else {
|
||||
ctx.focus(&variable_row.variable_name_editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn render_metadata_label<S>(&self, text: S, appearance: &Appearance) -> Box<dyn Element>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
appearance
|
||||
.ui_builder()
|
||||
.span(text.into())
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(LABEL_FONT_SIZE),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_metadata_editor(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
editor: ViewHandle<EditorView>,
|
||||
has_error: bool,
|
||||
) -> Box<dyn Element> {
|
||||
let mut style = UiComponentStyles {
|
||||
padding: Some(Coords {
|
||||
left: HORIZONTAL_TEXT_INPUT_PADDING,
|
||||
right: HORIZONTAL_TEXT_INPUT_PADDING,
|
||||
top: VERTICAL_TEXT_INPUT_PADDING,
|
||||
bottom: VERTICAL_TEXT_INPUT_PADDING,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if has_error {
|
||||
let error_color = appearance.theme().ui_error_color();
|
||||
style.border_color = Some(error_color.into());
|
||||
style.border_width = Some(super::env_var_collection::ERROR_BORDER_WIDTH);
|
||||
}
|
||||
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(editor.clone())
|
||||
.with_style(style)
|
||||
.build()
|
||||
.finish()
|
||||
}
|
||||
|
||||
// "Metadata" references the object level title and description fields
|
||||
pub(super) fn render_metadata(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let title_has_error = self.form_validation_state.title_error.is_some();
|
||||
let description_has_error = self.form_validation_state.description_error.is_some();
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Container::new(self.render_metadata_label(TITLE_LABEL_TEXT, appearance))
|
||||
.with_margin_bottom(METADATA_SPACING)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(self.render_metadata_editor(
|
||||
appearance,
|
||||
self.title_editor.clone(),
|
||||
title_has_error,
|
||||
))
|
||||
.with_margin_bottom(METADATA_SPACING)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
SavePosition::new(
|
||||
Container::new(self.render_metadata_label(DESCRIPTION_LABEL_TEXT, appearance))
|
||||
.with_margin_bottom(METADATA_SPACING)
|
||||
.finish(),
|
||||
DESCRIPTION_EDITOR_POSITION,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(self.render_metadata_editor(
|
||||
appearance,
|
||||
self.description_editor.clone(),
|
||||
description_has_error,
|
||||
))
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub(super) fn render_variable_editor(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
editor: ViewHandle<EditorView>,
|
||||
editor_type: EditorType,
|
||||
inline_secret_button: Option<Box<dyn Element>>,
|
||||
row_index: Option<usize>,
|
||||
) -> Box<dyn Element> {
|
||||
let margin_right = if editor_type != EditorType::Description {
|
||||
ROW_SPACING
|
||||
} else {
|
||||
LAST_ROW_ELEMENT_SPACING
|
||||
};
|
||||
|
||||
let validation_error = if let Some(index) = row_index {
|
||||
self.variable_rows
|
||||
.get(index)
|
||||
.and_then(|row| row.validation_state.get_field_error(editor_type))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let text_input = {
|
||||
let mut style = UiComponentStyles {
|
||||
padding: Some(Coords {
|
||||
left: HORIZONTAL_TEXT_INPUT_PADDING,
|
||||
right: HORIZONTAL_TEXT_INPUT_PADDING,
|
||||
top: VERTICAL_TEXT_INPUT_PADDING,
|
||||
bottom: VERTICAL_TEXT_INPUT_PADDING,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if validation_error.is_some() {
|
||||
let error_color = appearance.theme().ui_error_color();
|
||||
style.border_color = Some(error_color.into());
|
||||
style.border_width = Some(super::env_var_collection::ERROR_BORDER_WIDTH);
|
||||
}
|
||||
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(editor.clone())
|
||||
.with_style(style)
|
||||
.build()
|
||||
.finish()
|
||||
};
|
||||
|
||||
let input_container = {
|
||||
let mut stack = Stack::new().with_child(text_input);
|
||||
|
||||
if let Some(element) = inline_secret_button {
|
||||
stack.add_child(
|
||||
Align::new(
|
||||
Container::new(element)
|
||||
.with_margin_right(SECRET_ICON_BUTTON_MARGIN)
|
||||
.with_margin_top(SECRET_ICON_BUTTON_MARGIN)
|
||||
.finish(),
|
||||
)
|
||||
.right()
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
stack.finish()
|
||||
};
|
||||
|
||||
let editor_column = input_container;
|
||||
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(ConstrainedBox::new(editor_column).finish())
|
||||
.with_margin_right(margin_right)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Sync all editors with the user's access level. If the env var collection is view-only, all
|
||||
/// editors are set to selection-only mode. Otherwise, all are enabled.
|
||||
pub(super) fn update_editor_interactivity(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let editability = self
|
||||
.active_env_var_collection_data
|
||||
.as_ref(ctx)
|
||||
.editability(ctx);
|
||||
let interaction_state = if editability.can_edit() {
|
||||
InteractionState::Editable
|
||||
} else {
|
||||
InteractionState::Selectable
|
||||
};
|
||||
|
||||
// Update metadata editors.
|
||||
self.title_editor.update(ctx, |editor, ctx| {
|
||||
editor.set_interaction_state(interaction_state, ctx)
|
||||
});
|
||||
self.description_editor.update(ctx, |editor, ctx| {
|
||||
editor.set_interaction_state(interaction_state, ctx)
|
||||
});
|
||||
|
||||
// Update individual variable editors.
|
||||
for variable_row in self.variable_rows.iter() {
|
||||
variable_row
|
||||
.variable_name_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.set_interaction_state(interaction_state, ctx)
|
||||
});
|
||||
variable_row
|
||||
.variable_description_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.set_interaction_state(interaction_state, ctx)
|
||||
});
|
||||
variable_row
|
||||
.variable_value_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.set_interaction_state(interaction_state, ctx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
use warp_core::ui::appearance::Appearance;
|
||||
use warpui::{platform::WindowStyle, App, ViewHandle};
|
||||
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::{
|
||||
cloud_object::model::{actions::ObjectActions, persistence::CloudModel, view::CloudViewModel},
|
||||
env_vars::{
|
||||
active_env_var_collection_data::SavingStatus,
|
||||
view::env_var_collection::EnvVarCollectionView,
|
||||
},
|
||||
network::NetworkStatus,
|
||||
server::{
|
||||
cloud_objects::update_manager::UpdateManager, server_api::ServerApiProvider,
|
||||
sync_queue::SyncQueue,
|
||||
},
|
||||
settings_view::keybindings::KeybindingChangedNotifier,
|
||||
test_util::settings::initialize_settings_for_tests,
|
||||
workspace::ActiveSession,
|
||||
workspaces::{
|
||||
team_tester::TeamTesterStatus, user_profiles::UserProfiles, user_workspaces::UserWorkspaces,
|
||||
},
|
||||
GlobalResourceHandles, GlobalResourceHandlesProvider,
|
||||
};
|
||||
|
||||
fn initialize_app(app: &mut App) {
|
||||
initialize_settings_for_tests(app);
|
||||
|
||||
let global_resources = GlobalResourceHandles::mock(app);
|
||||
app.add_singleton_model(|_| GlobalResourceHandlesProvider::new(global_resources));
|
||||
app.add_singleton_model(CloudModel::mock);
|
||||
app.add_singleton_model(|_| NetworkStatus::new());
|
||||
app.add_singleton_model(|_| Appearance::mock());
|
||||
|
||||
app.add_singleton_model(UserWorkspaces::default_mock);
|
||||
app.add_singleton_model(SyncQueue::mock);
|
||||
app.add_singleton_model(TeamTesterStatus::mock);
|
||||
app.add_singleton_model(UpdateManager::mock);
|
||||
app.add_singleton_model(CloudViewModel::mock);
|
||||
app.add_singleton_model(|_| UserProfiles::new(vec![]));
|
||||
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
|
||||
app.add_singleton_model(|_| ActiveSession::default());
|
||||
app.add_singleton_model(|_| ObjectActions::new(Vec::new()));
|
||||
app.add_singleton_model(|_| KeybindingChangedNotifier::mock());
|
||||
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
|
||||
|
||||
#[cfg(feature = "voice_input")]
|
||||
app.add_singleton_model(voice_input::VoiceInput::new);
|
||||
}
|
||||
|
||||
fn create_env_var_collection_view(app: &mut App) -> ViewHandle<EnvVarCollectionView> {
|
||||
initialize_app(app);
|
||||
let (_, env_var_collection_view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||||
EnvVarCollectionView::new(ctx)
|
||||
});
|
||||
|
||||
env_var_collection_view
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable_row_addition_and_removal() {
|
||||
App::test((), |mut app| async move {
|
||||
let env_var_collection_view = create_env_var_collection_view(&mut app);
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.open_new_env_var_collection(
|
||||
crate::cloud_object::Owner::mock_current_user(),
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
// New EVCs should open with a new row
|
||||
env_var_collection_view.read(&app, |view, _| {
|
||||
assert_eq!(view.variable_rows.len(), 1);
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.add_variable_row(ctx);
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.variable_rows[1]
|
||||
.variable_description_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text("description for foo_1", ctx);
|
||||
});
|
||||
|
||||
view.delete_row(0, ctx);
|
||||
});
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert_eq!(view.variable_rows.len(), 1);
|
||||
assert_eq!(
|
||||
view.variable_rows[0]
|
||||
.variable_description_editor
|
||||
.as_ref(ctx)
|
||||
.buffer_text(ctx),
|
||||
"description for foo_1".to_owned()
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_saving_status() {
|
||||
App::test((), |mut app| async move {
|
||||
let env_var_collection_view = create_env_var_collection_view(&mut app);
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert_eq!(
|
||||
view.active_env_var_collection_data
|
||||
.as_ref(ctx)
|
||||
.saving_status,
|
||||
SavingStatus::Saved
|
||||
);
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.add_variable_row(ctx);
|
||||
});
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert_eq!(
|
||||
view.active_env_var_collection_data
|
||||
.as_ref(ctx)
|
||||
.saving_status,
|
||||
SavingStatus::Unsaved
|
||||
);
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.active_env_var_collection_data.update(ctx, |data, _| {
|
||||
data.saving_status = SavingStatus::Saved;
|
||||
})
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.delete_row(0, ctx);
|
||||
});
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert_eq!(
|
||||
view.active_env_var_collection_data
|
||||
.as_ref(ctx)
|
||||
.saving_status,
|
||||
SavingStatus::Unsaved
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_disable_save() {
|
||||
App::test((), |mut app| async move {
|
||||
let env_var_collection_view = create_env_var_collection_view(&mut app);
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert!(view.should_disable_save(ctx));
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.add_variable_row(ctx);
|
||||
});
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert!(view.should_disable_save(ctx));
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.variable_rows[0]
|
||||
.variable_name_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text("Test", ctx);
|
||||
});
|
||||
|
||||
view.variable_rows[0]
|
||||
.variable_value_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text("Test", ctx);
|
||||
})
|
||||
});
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert!(!view.should_disable_save(ctx));
|
||||
});
|
||||
|
||||
env_var_collection_view.update(&mut app, |view, ctx| {
|
||||
view.variable_rows[0]
|
||||
.variable_value_editor
|
||||
.update(ctx, |editor, ctx| {
|
||||
editor.clear_buffer(ctx);
|
||||
})
|
||||
});
|
||||
|
||||
env_var_collection_view.read(&app, |view, ctx| {
|
||||
assert!(view.should_disable_save(ctx));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warpui::{
|
||||
elements::{
|
||||
Align, ConstrainedBox, Container, CrossAxisAlignment, Empty, Flex, MainAxisAlignment,
|
||||
MainAxisSize, ParentElement, Rect, Shrinkable, Stack,
|
||||
},
|
||||
fonts::Weight,
|
||||
ui_components::{
|
||||
button::{ButtonVariant, TextAndIcon, TextAndIconAlignment},
|
||||
components::{UiComponent, UiComponentStyles},
|
||||
},
|
||||
Element, ViewContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drive::sharing::{ContentEditability, SharingAccessLevel},
|
||||
env_vars::{
|
||||
active_env_var_collection_data::TrashStatus,
|
||||
view::env_var_collection::{EnvVarCollectionAction, EnvVarCollectionView},
|
||||
},
|
||||
ui_components::{breadcrumb::BreadcrumbState, buttons::icon_button, icons::Icon},
|
||||
AppContext, Appearance, SingletonEntity,
|
||||
};
|
||||
|
||||
const VARIABLE_DIVIDER_HEIGHT: f32 = 2.;
|
||||
const SECTION_FONT_SIZE: f32 = 16.;
|
||||
const BUTTON_HEIGHT: f32 = 32.;
|
||||
|
||||
const SAVE_BUTTON_TEXT: &str = "Save";
|
||||
const VARIABLES_LABEL_TEXT: &str = "Variables";
|
||||
|
||||
/// This file contains components that fixed in the view,
|
||||
/// i.e. the trash banner, breadcrumbs, and variables section header
|
||||
impl EnvVarCollectionView {
|
||||
pub(super) fn update_breadcrumbs(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.breadcrumbs = self
|
||||
.active_env_var_collection_data
|
||||
.update(ctx, |data, ctx| {
|
||||
data.breadcrumbs(ctx)
|
||||
.map(|breadcrumbs| breadcrumbs.into_iter().map(BreadcrumbState::new).collect())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn render_trash_banner(
|
||||
&self,
|
||||
access_level: SharingAccessLevel,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let deleted = match self
|
||||
.active_env_var_collection_data
|
||||
.as_ref(app)
|
||||
.trash_status(app)
|
||||
{
|
||||
TrashStatus::Active => return None,
|
||||
TrashStatus::Trashed => false,
|
||||
TrashStatus::Deleted => true,
|
||||
};
|
||||
let appearance = Appearance::as_ref(app);
|
||||
|
||||
let mut stack = Stack::new();
|
||||
|
||||
let text = if deleted {
|
||||
"You no longer have access to these environment variables"
|
||||
} else {
|
||||
"Environment variables were moved to trash"
|
||||
};
|
||||
stack.add_child(
|
||||
Align::new(
|
||||
Flex::row()
|
||||
.with_children([
|
||||
ConstrainedBox::new(
|
||||
Icon::Trash
|
||||
.to_warpui_icon(appearance.theme().foreground())
|
||||
.finish(),
|
||||
)
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish(),
|
||||
appearance
|
||||
.ui_builder()
|
||||
.span(text)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(appearance.ui_font_size() + 2.),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.with_padding_left(8.)
|
||||
.finish(),
|
||||
])
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
let action_row = if deleted {
|
||||
Shrinkable::new(1., Empty::new().finish()).finish()
|
||||
} else {
|
||||
let mut action_row = Flex::row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
if !FeatureFlag::SharedWithMe.is_enabled() || access_level.can_trash() {
|
||||
let ui_builder = appearance.ui_builder().clone();
|
||||
action_row.add_child(
|
||||
Align::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
ButtonVariant::Basic,
|
||||
self.button_mouse_states.restore_from_trash_button.clone(),
|
||||
)
|
||||
.with_tooltip(move || {
|
||||
ui_builder
|
||||
.tool_tip(
|
||||
"Restore environment variables from trash".to_string(),
|
||||
)
|
||||
.build()
|
||||
.finish()
|
||||
})
|
||||
.with_text_label("Restore".to_string())
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(EnvVarCollectionAction::Untrash)
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
action_row.finish()
|
||||
};
|
||||
|
||||
stack.add_child(Align::new(action_row).right().finish());
|
||||
|
||||
Some(
|
||||
Container::new(
|
||||
ConstrainedBox::new(stack.finish())
|
||||
.with_min_height(40.)
|
||||
.finish(),
|
||||
)
|
||||
.with_horizontal_padding(16.)
|
||||
.with_background(appearance.theme().surface_2())
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn render_variables_section_header(
|
||||
&self,
|
||||
editability: ContentEditability,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let mut variables_section_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
variables_section_row.add_child(
|
||||
Shrinkable::new(
|
||||
2.,
|
||||
appearance
|
||||
.ui_builder()
|
||||
.span(VARIABLES_LABEL_TEXT.to_string())
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(SECTION_FONT_SIZE),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if !FeatureFlag::SharedWithMe.is_enabled() || editability.can_edit() {
|
||||
variables_section_row.add_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Flex::row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
icon_button(
|
||||
appearance,
|
||||
Icon::Plus,
|
||||
false,
|
||||
self.button_mouse_states.add_variable_state.clone(),
|
||||
)
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(EnvVarCollectionAction::AddVariable)
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
variables_section_row.finish()
|
||||
}
|
||||
|
||||
pub(super) fn render_divider(&self, appearance: &Appearance, index: usize) -> Box<dyn Element> {
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(if index != self.variable_rows.len() - 1 {
|
||||
appearance.theme().surface_2().into()
|
||||
} else {
|
||||
ColorU::transparent_black()
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_height(VARIABLE_DIVIDER_HEIGHT)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub(super) fn render_invoke_button(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let mut button = appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
ButtonVariant::Secondary,
|
||||
self.button_mouse_states.invoke_mouse_state.clone(),
|
||||
)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
width: Some(80.),
|
||||
height: Some(BUTTON_HEIGHT),
|
||||
..Default::default()
|
||||
})
|
||||
.with_text_and_icon_label(
|
||||
TextAndIcon::new(
|
||||
TextAndIconAlignment::TextFirst,
|
||||
"Load",
|
||||
Icon::TerminalInput.to_warpui_icon(appearance.theme().active_ui_text_color()),
|
||||
MainAxisSize::Min,
|
||||
MainAxisAlignment::SpaceBetween,
|
||||
Vector2F::new(10., 10.),
|
||||
)
|
||||
.with_inner_padding(4.),
|
||||
);
|
||||
|
||||
if self.should_disable_invoke(app) {
|
||||
button = button.disabled();
|
||||
}
|
||||
|
||||
button
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(EnvVarCollectionAction::Invoke))
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub(super) fn render_save_button(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let is_save_disabled = self.should_disable_save(app);
|
||||
let mut button = appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
ButtonVariant::Accent,
|
||||
self.button_mouse_states.save_mouse_state.clone(),
|
||||
)
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: if is_save_disabled {
|
||||
Some(
|
||||
appearance
|
||||
.theme()
|
||||
.disabled_text_color(appearance.theme().background())
|
||||
.into_solid(),
|
||||
)
|
||||
} else {
|
||||
Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().accent())
|
||||
.into_solid(),
|
||||
)
|
||||
},
|
||||
font_weight: Some(Weight::Bold),
|
||||
width: Some(100.),
|
||||
height: Some(BUTTON_HEIGHT),
|
||||
font_size: Some(14.),
|
||||
..Default::default()
|
||||
})
|
||||
.with_centered_text_label(SAVE_BUTTON_TEXT.to_owned());
|
||||
|
||||
if is_save_disabled {
|
||||
button = button.disabled();
|
||||
}
|
||||
|
||||
button
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(EnvVarCollectionAction::SaveVariables)
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use warp_core::context_flag::ContextFlag;
|
||||
use warpui::{keymap::Trigger, SingletonEntity, ViewContext, ViewHandle};
|
||||
|
||||
use crate::{
|
||||
cloud_object::{CloudObject, GenericStringObjectFormat, Space},
|
||||
drive::{
|
||||
drive_helpers::has_feature_gated_anonymous_user_reached_env_var_limit,
|
||||
export::ExportManager, CloudObjectTypeAndId,
|
||||
},
|
||||
env_vars::active_env_var_collection_data::TrashStatus,
|
||||
external_secrets::SecretManager,
|
||||
menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields},
|
||||
pane_group::PaneEvent,
|
||||
server::cloud_objects::update_manager::UpdateManager,
|
||||
ui_components::icons::Icon,
|
||||
util::bindings::{keybinding_name_to_display_string, trigger_to_keystroke, CustomAction},
|
||||
AppContext, CloudModel, FeatureFlag,
|
||||
};
|
||||
|
||||
use super::env_var_collection::{EnvVarCollectionAction, EnvVarCollectionView, VariableRowIndex};
|
||||
|
||||
const PANE_MENU_WIDTH: f32 = 200.;
|
||||
|
||||
pub struct Menus {
|
||||
pub(super) secret_menu: ViewHandle<Menu<EnvVarCollectionAction>>,
|
||||
pub(super) rendered_secret_menu: ViewHandle<Menu<EnvVarCollectionAction>>,
|
||||
pub(super) rendered_command_menu: ViewHandle<Menu<EnvVarCollectionAction>>,
|
||||
pub(super) pane_context_menu: ViewHandle<Menu<EnvVarCollectionAction>>,
|
||||
}
|
||||
|
||||
impl EnvVarCollectionView {
|
||||
pub(super) fn initialize_menus(ctx: &mut ViewContext<Self>) -> Menus {
|
||||
let command_item = Self::item(
|
||||
"Command",
|
||||
EnvVarCollectionAction::DisplayCommandDialog,
|
||||
None,
|
||||
Some(Icon::Terminal),
|
||||
);
|
||||
|
||||
let one_password_item = Self::item(
|
||||
"1Password",
|
||||
EnvVarCollectionAction::SelectSecretManager(SecretManager::OnePassword),
|
||||
None,
|
||||
Some(Icon::OnePassword),
|
||||
);
|
||||
|
||||
let lastpass_item = Self::item(
|
||||
"LastPass",
|
||||
EnvVarCollectionAction::SelectSecretManager(SecretManager::LastPass),
|
||||
None,
|
||||
Some(Icon::LastPass),
|
||||
);
|
||||
|
||||
let edit_item = Self::item(
|
||||
"Edit",
|
||||
EnvVarCollectionAction::EditCommand,
|
||||
None,
|
||||
Some(Icon::Terminal),
|
||||
);
|
||||
|
||||
let clear_secret_item = Self::item(
|
||||
"Clear secret",
|
||||
EnvVarCollectionAction::ClearSecret,
|
||||
None,
|
||||
Some(Icon::Trash),
|
||||
);
|
||||
|
||||
let separator = MenuItem::Separator;
|
||||
|
||||
let secret_menu = Self::menu(
|
||||
vec![
|
||||
command_item.clone(),
|
||||
one_password_item.clone(),
|
||||
lastpass_item.clone(),
|
||||
],
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
|
||||
let rendered_secret_menu = Self::menu(
|
||||
vec![
|
||||
command_item.clone(),
|
||||
one_password_item.clone(),
|
||||
lastpass_item.clone(),
|
||||
separator.clone(),
|
||||
clear_secret_item.clone(),
|
||||
],
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
|
||||
let rendered_command_menu = Self::menu(
|
||||
vec![
|
||||
edit_item,
|
||||
one_password_item,
|
||||
lastpass_item,
|
||||
separator,
|
||||
clear_secret_item,
|
||||
],
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
|
||||
ctx.subscribe_to_view(&secret_menu, |me, _, event, ctx| {
|
||||
me.handle_secret_menu_event(event, ctx);
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&rendered_secret_menu, |me, _, event, ctx| {
|
||||
me.handle_rendered_secret_menu_event(event, ctx);
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&rendered_command_menu, |me, _, event, ctx| {
|
||||
me.handle_rendered_command_menu_event(event, ctx);
|
||||
});
|
||||
|
||||
let pane_context_menu = Self::menu(Vec::new(), Some(PANE_MENU_WIDTH), ctx);
|
||||
|
||||
Menus {
|
||||
secret_menu,
|
||||
rendered_secret_menu,
|
||||
rendered_command_menu,
|
||||
pane_context_menu,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn initialize_pane_context_menu(
|
||||
&self,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> ViewHandle<Menu<EnvVarCollectionAction>> {
|
||||
let split_pane_right = Self::item(
|
||||
"Split pane right",
|
||||
EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitRight(None)),
|
||||
keybinding_name_to_display_string("pane_group:add_right", ctx),
|
||||
None,
|
||||
);
|
||||
|
||||
let split_pane_left = Self::item(
|
||||
"Split pane left",
|
||||
EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitLeft(None)),
|
||||
keybinding_name_to_display_string("pane_group:add_left", ctx),
|
||||
None,
|
||||
);
|
||||
|
||||
let split_pane_down = Self::item(
|
||||
"Split pane down",
|
||||
EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitDown(None)),
|
||||
keybinding_name_to_display_string("pane_group:add_down", ctx),
|
||||
None,
|
||||
);
|
||||
|
||||
let split_pane_up = Self::item(
|
||||
"Split pane up",
|
||||
EnvVarCollectionAction::EmitPaneEvent(PaneEvent::SplitUp(None)),
|
||||
keybinding_name_to_display_string("pane_group:add_up", ctx),
|
||||
None,
|
||||
);
|
||||
|
||||
let is_maximized = self
|
||||
.focus_handle
|
||||
.as_ref()
|
||||
.is_some_and(|handle| handle.split_pane_state(ctx).is_maximized());
|
||||
let toggle_maximize_pane = Self::item(
|
||||
if is_maximized {
|
||||
"Minimize pane"
|
||||
} else {
|
||||
"Maximize pane"
|
||||
},
|
||||
EnvVarCollectionAction::EmitPaneEvent(PaneEvent::ToggleMaximized),
|
||||
keybinding_name_to_display_string("pane_group:toggle_maximize_pane", ctx),
|
||||
None,
|
||||
);
|
||||
|
||||
let close_pane = Self::item(
|
||||
"Close pane",
|
||||
EnvVarCollectionAction::EmitPaneEvent(PaneEvent::Close),
|
||||
trigger_to_keystroke(&Trigger::Custom(CustomAction::CloseCurrentSession.into()))
|
||||
.map(|keystroke| keystroke.displayed()),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
if ContextFlag::CreateNewSession.is_enabled() {
|
||||
items.extend(vec![
|
||||
split_pane_right,
|
||||
split_pane_left,
|
||||
split_pane_down,
|
||||
split_pane_up,
|
||||
]);
|
||||
}
|
||||
|
||||
if self
|
||||
.focus_handle
|
||||
.as_ref()
|
||||
.is_some_and(|handle| handle.is_in_split_pane(ctx))
|
||||
{
|
||||
items.extend(vec![toggle_maximize_pane, close_pane]);
|
||||
}
|
||||
|
||||
let pane_context_menu = Self::menu(items, Some(PANE_MENU_WIDTH), ctx);
|
||||
|
||||
ctx.subscribe_to_view(&pane_context_menu, |me, _, event, ctx| {
|
||||
me.handle_pane_context_menu_event(event, ctx);
|
||||
});
|
||||
|
||||
pane_context_menu
|
||||
}
|
||||
|
||||
pub(super) fn display_secret_menu(&mut self, index: usize) {
|
||||
let row = &mut self.variable_rows[index];
|
||||
row.secret_menu_is_focused = true;
|
||||
|
||||
self.pending_variable_row_index = Some(VariableRowIndex(index));
|
||||
}
|
||||
|
||||
pub(super) fn display_rendered_secret_menu(&mut self, index: usize) {
|
||||
let row = &mut self.variable_rows[index];
|
||||
row.rendered_secret_menu_is_focused = true;
|
||||
|
||||
self.pending_variable_row_index = Some(VariableRowIndex(index));
|
||||
}
|
||||
|
||||
pub(super) fn display_rendered_command_menu(&mut self, index: usize) {
|
||||
let row = &mut self.variable_rows[index];
|
||||
row.rendered_command_menu_is_focused = true;
|
||||
|
||||
self.pending_variable_row_index = Some(VariableRowIndex(index));
|
||||
}
|
||||
|
||||
pub(super) fn display_pane_context_menu(
|
||||
&mut self,
|
||||
position: &Vector2F,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(parent_bounds) = ctx.element_position_by_id(self.view_position_id.clone()) {
|
||||
self.menus.pane_context_menu = self.initialize_pane_context_menu(ctx);
|
||||
let offset = *position - parent_bounds.origin();
|
||||
self.pane_context_menu_offset = Some(offset);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_secret_menu_event(&mut self, event: &MenuEvent, ctx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
MenuEvent::Close { via_select_item: _ } => self.reset_secret_menu(ctx),
|
||||
MenuEvent::ItemSelected => self.reset_secret_menu(ctx),
|
||||
MenuEvent::ItemHovered => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rendered_secret_menu_event(
|
||||
&mut self,
|
||||
event: &MenuEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
MenuEvent::Close { via_select_item: _ } => self.reset_rendered_secret_menu(ctx),
|
||||
MenuEvent::ItemSelected => self.reset_rendered_secret_menu(ctx),
|
||||
MenuEvent::ItemHovered => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rendered_command_menu_event(
|
||||
&mut self,
|
||||
event: &MenuEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
MenuEvent::Close { via_select_item: _ } => self.reset_rendered_command_menu(ctx),
|
||||
MenuEvent::ItemSelected => self.reset_rendered_command_menu(ctx),
|
||||
MenuEvent::ItemHovered => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pane_context_menu_event(&mut self, event: &MenuEvent, ctx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
MenuEvent::Close { via_select_item: _ } | MenuEvent::ItemSelected => {
|
||||
self.pane_context_menu_offset = None;
|
||||
self.menus
|
||||
.pane_context_menu
|
||||
.update(ctx, |menu, ctx| menu.reset_selection(ctx));
|
||||
ctx.notify()
|
||||
}
|
||||
MenuEvent::ItemHovered => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_secret_menu(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.variable_rows.iter_mut().for_each(|row| {
|
||||
row.secret_menu_is_focused = false;
|
||||
});
|
||||
self.menus
|
||||
.secret_menu
|
||||
.update(ctx, |menu, ctx| menu.reset_selection(ctx));
|
||||
ctx.notify()
|
||||
}
|
||||
|
||||
fn reset_rendered_secret_menu(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.variable_rows.iter_mut().for_each(|row| {
|
||||
row.rendered_secret_menu_is_focused = false;
|
||||
});
|
||||
self.menus
|
||||
.rendered_secret_menu
|
||||
.update(ctx, |menu, ctx| menu.reset_selection(ctx));
|
||||
ctx.notify()
|
||||
}
|
||||
|
||||
fn reset_rendered_command_menu(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.variable_rows.iter_mut().for_each(|row| {
|
||||
row.rendered_command_menu_is_focused = false;
|
||||
});
|
||||
self.menus
|
||||
.rendered_command_menu
|
||||
.update(ctx, |menu, ctx| menu.reset_selection(ctx));
|
||||
ctx.notify()
|
||||
}
|
||||
|
||||
fn menu(
|
||||
items: Vec<MenuItem<EnvVarCollectionAction>>,
|
||||
width: Option<f32>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> ViewHandle<Menu<EnvVarCollectionAction>> {
|
||||
ctx.add_typed_action_view(|_| {
|
||||
let mut menu = Menu::new()
|
||||
.prevent_interaction_with_other_elements()
|
||||
.with_drop_shadow();
|
||||
|
||||
if let Some(width) = width {
|
||||
menu = menu.with_width(width);
|
||||
}
|
||||
|
||||
menu.add_items(items);
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
|
||||
fn item(
|
||||
name: &str,
|
||||
action: EnvVarCollectionAction,
|
||||
key_shortcut: Option<String>,
|
||||
icon: Option<Icon>,
|
||||
) -> MenuItem<EnvVarCollectionAction> {
|
||||
let mut field = MenuItemFields::new(name)
|
||||
.with_on_select_action(action)
|
||||
.with_key_shortcut_label(key_shortcut);
|
||||
|
||||
if let Some(icon) = icon {
|
||||
field = field.with_icon(icon);
|
||||
}
|
||||
|
||||
field.into_item()
|
||||
}
|
||||
|
||||
// Used for duplicate, copy link, trash etc
|
||||
pub(super) fn overflow_menu_items(
|
||||
&self,
|
||||
ctx: &AppContext,
|
||||
) -> Vec<MenuItem<EnvVarCollectionAction>> {
|
||||
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()
|
||||
|| active_collection_data.trash_status(ctx) != TrashStatus::Active
|
||||
{
|
||||
return menu_items;
|
||||
}
|
||||
|
||||
// Add "Copy Link" to menu
|
||||
if let Some(link) = self.env_var_collection_link(ctx) {
|
||||
menu_items.push(
|
||||
MenuItemFields::new("Copy link")
|
||||
.with_on_select_action(EnvVarCollectionAction::CopyLink(link))
|
||||
.with_icon(Icon::Link)
|
||||
.into_item(),
|
||||
);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
menu_items.push(
|
||||
MenuItemFields::new("Export")
|
||||
.with_on_select_action(EnvVarCollectionAction::Export)
|
||||
.with_icon(Icon::Download)
|
||||
.into_item(),
|
||||
);
|
||||
|
||||
menu_items
|
||||
}
|
||||
|
||||
pub(super) fn env_var_collection_link(&self, ctx: &AppContext) -> Option<String> {
|
||||
self.env_var_collection_id(ctx)
|
||||
.and_then(|id| CloudModel::as_ref(ctx).get_env_var_collection(&id))
|
||||
.map(|env_var_collection| env_var_collection.object_link())?
|
||||
}
|
||||
|
||||
pub(super) fn untrash_env_var_collection(&self, ctx: &mut ViewContext<Self>) {
|
||||
if let Some(env_var_collection_id) = self.active_env_var_collection_data.as_ref(ctx).id() {
|
||||
if has_feature_gated_anonymous_user_reached_env_var_limit(ctx) {
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn trash_env_var_collection(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
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,
|
||||
);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn export_env_var_collection(&self, ctx: &mut ViewContext<Self>) {
|
||||
if let Some(env_var_collection_id) = self.env_var_collection_id(ctx) {
|
||||
let window_id = ctx.window_id();
|
||||
ExportManager::handle(ctx).update(ctx, |export_manager, ctx| {
|
||||
export_manager.export(
|
||||
window_id,
|
||||
&[CloudObjectTypeAndId::from_generic_string_object(
|
||||
GenericStringObjectFormat::Json(
|
||||
crate::cloud_object::JsonObjectType::EnvVarCollection,
|
||||
),
|
||||
env_var_collection_id,
|
||||
)],
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod command_dialog;
|
||||
pub mod editors;
|
||||
pub mod env_var_collection;
|
||||
#[cfg(test)]
|
||||
mod env_var_collection_tests;
|
||||
pub mod fixed_view_components;
|
||||
pub mod menus;
|
||||
pub mod secrets;
|
||||
pub mod unsaved_changes_dialog;
|
||||
@@ -0,0 +1,245 @@
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use warp_core::{features::FeatureFlag, ui::appearance::Appearance};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use warpui::SingletonEntity;
|
||||
use warpui::{
|
||||
elements::{
|
||||
ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, Empty, Fill, MainAxisAlignment,
|
||||
MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, Shrinkable, Stack,
|
||||
},
|
||||
fonts::Weight,
|
||||
ui_components::{
|
||||
button::{ButtonVariant, TextAndIcon, TextAndIconAlignment},
|
||||
components::{UiComponent, UiComponentStyles},
|
||||
},
|
||||
Element, ViewContext,
|
||||
};
|
||||
|
||||
use super::env_var_collection::{
|
||||
EnvVarCollectionAction, EnvVarCollectionView, VariableRowIndex, CORE_MAX_WIDTH, ROW_SPACING,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drive::sharing::ContentEditability,
|
||||
env_vars::{active_env_var_collection_data::SavingStatus, EnvVarValue},
|
||||
external_secrets::{ExternalSecretManager, SecretManager},
|
||||
search::external_secrets::{
|
||||
searcher::ExternalSecretSearchItemAction, view::ExternalSecretsMenuEvent,
|
||||
},
|
||||
ui_components::icons::Icon,
|
||||
};
|
||||
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
|
||||
use crate::{
|
||||
terminal::local_shell::LocalShellState,
|
||||
view_components::{DismissibleToast, ToastLink},
|
||||
workspace::{ToastStack, WorkspaceAction},
|
||||
};
|
||||
|
||||
impl EnvVarCollectionView {
|
||||
pub(super) fn handle_external_secrets_dialog_event(
|
||||
&mut self,
|
||||
event: &ExternalSecretsMenuEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ExternalSecretsMenuEvent::ItemSelected { payload } => {
|
||||
let ExternalSecretSearchItemAction::AcceptSecret(secret) = payload.as_ref();
|
||||
let row_index = self.pending_variable_row_index.take();
|
||||
if let Some(VariableRowIndex(index)) = row_index {
|
||||
self.variable_rows[index].value = EnvVarValue::Secret(secret.clone());
|
||||
}
|
||||
|
||||
self.set_saving_status(SavingStatus::Unsaved, ctx)
|
||||
}
|
||||
ExternalSecretsMenuEvent::Close => {
|
||||
self.dialog_open_states.secrets_dialog_open = false;
|
||||
self.update_open_modal_state(ctx);
|
||||
ctx.focus_self();
|
||||
ctx.notify();
|
||||
}
|
||||
ExternalSecretsMenuEvent::Open => {
|
||||
self.dialog_open_states.secrets_dialog_open = true;
|
||||
ctx.focus(&self.secrets_dialog);
|
||||
self.update_open_modal_state(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear_secret(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if let Some(VariableRowIndex(index)) = self.pending_variable_row_index.take() {
|
||||
self.variable_rows[index].value = EnvVarValue::Constant(String::new());
|
||||
}
|
||||
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(unused_variables))]
|
||||
pub(super) fn fetch_secret(
|
||||
&mut self,
|
||||
secret_manager: SecretManager,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
|
||||
{
|
||||
let window_id = ctx.window_id();
|
||||
let local_shell = LocalShellState::as_ref(ctx);
|
||||
let secret_manager_clone = secret_manager.clone();
|
||||
|
||||
let Some(local_shell_state) = local_shell.local_shell_info() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let shell_type = local_shell_state.get_shell_type();
|
||||
let shell_path = local_shell_state.get_shell_path().clone();
|
||||
let path_env_var = local_shell_state.get_path_env_var().clone();
|
||||
|
||||
self.secrets_dialog.update(ctx, |_, dialog_ctx| {
|
||||
let _ = dialog_ctx.spawn(
|
||||
async move {
|
||||
secret_manager
|
||||
.verify_installed_and_fetch_secrets(
|
||||
shell_type,
|
||||
shell_path,
|
||||
path_env_var,
|
||||
)
|
||||
.await
|
||||
},
|
||||
move |view, result, dialog_ctx| match result {
|
||||
Ok(secrets) => {
|
||||
view.setup(secrets, dialog_ctx);
|
||||
}
|
||||
Err(e) => {
|
||||
let error_message_and_command =
|
||||
secret_manager_clone.get_toast_message_and_link(e);
|
||||
|
||||
let mut toast =
|
||||
DismissibleToast::error(error_message_and_command.message);
|
||||
|
||||
if let (Some(link), Some(link_message)) = (
|
||||
error_message_and_command.link,
|
||||
error_message_and_command.link_message,
|
||||
) {
|
||||
toast = toast.with_link(
|
||||
ToastLink::new(link_message)
|
||||
.with_onclick_action(WorkspaceAction::OpenLink(link)),
|
||||
);
|
||||
}
|
||||
|
||||
ToastStack::handle(dialog_ctx).update(dialog_ctx, |toast_stack, ctx| {
|
||||
toast_stack.add_persistent_toast(toast, window_id, ctx);
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn render_secret_or_command_button(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
secret: &EnvVarValue,
|
||||
menu_button_mouse_state: MouseStateHandle,
|
||||
row_index: usize,
|
||||
is_focused: bool,
|
||||
editability: ContentEditability,
|
||||
) -> Box<dyn Element> {
|
||||
let (display_name, action, menu, icon) = match secret {
|
||||
EnvVarValue::Secret(sec) => (
|
||||
sec.get_display_name(),
|
||||
EnvVarCollectionAction::DisplayRenderedSecretMenu(VariableRowIndex(row_index)),
|
||||
&self.menus.rendered_secret_menu,
|
||||
sec.icon(),
|
||||
),
|
||||
EnvVarValue::Command(cmd) => (
|
||||
if !cmd.name.is_empty() {
|
||||
cmd.name.clone()
|
||||
} else {
|
||||
cmd.command.clone()
|
||||
},
|
||||
EnvVarCollectionAction::DisplayRenderedCommandMenu(VariableRowIndex(row_index)),
|
||||
&self.menus.rendered_command_menu,
|
||||
Icon::Terminal,
|
||||
),
|
||||
_ => {
|
||||
log::warn!("Secret type not supported for button rendering");
|
||||
return Empty::new().finish();
|
||||
}
|
||||
};
|
||||
|
||||
let text_and_icon = TextAndIcon::new(
|
||||
TextAndIconAlignment::IconFirst,
|
||||
display_name,
|
||||
icon.to_warpui_icon(appearance.theme().active_ui_text_color()),
|
||||
MainAxisSize::Max,
|
||||
MainAxisAlignment::Center,
|
||||
vec2f(16., 16.),
|
||||
)
|
||||
.with_inner_padding(4.);
|
||||
|
||||
let default_button_styles = UiComponentStyles {
|
||||
font_size: Some(appearance.ui_font_size()),
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
border_width: Some(1.),
|
||||
border_color: Some(appearance.theme().surface_2().into()),
|
||||
font_weight: Some(Weight::Bold),
|
||||
background: Some(Fill::None),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let hovered_styles = UiComponentStyles {
|
||||
border_width: Some(1.),
|
||||
border_color: Some(appearance.theme().accent_button_color().into()),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let mut button = appearance
|
||||
.ui_builder()
|
||||
.button(ButtonVariant::Outlined, menu_button_mouse_state)
|
||||
.with_style(default_button_styles)
|
||||
.with_hovered_styles(hovered_styles)
|
||||
.with_text_and_icon_label(text_and_icon);
|
||||
|
||||
if FeatureFlag::SharedWithMe.is_enabled() && !editability.can_edit() {
|
||||
button = button.disabled();
|
||||
}
|
||||
|
||||
let button = button
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(action.clone()));
|
||||
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(
|
||||
ConstrainedBox::new({
|
||||
let mut stack = Stack::new().with_child(Clipped::new(button.finish()).finish());
|
||||
if is_focused {
|
||||
stack.add_positioned_overlay_child(
|
||||
ChildView::new(menu).finish(),
|
||||
OffsetPositioning::offset_from_parent(
|
||||
vec2f(0., 0.),
|
||||
ParentOffsetBounds::WindowByPosition,
|
||||
ParentAnchor::TopRight,
|
||||
ChildAnchor::TopLeft,
|
||||
),
|
||||
);
|
||||
}
|
||||
stack.finish()
|
||||
})
|
||||
.with_width(CORE_MAX_WIDTH)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(ROW_SPACING)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use warp_core::ui::appearance::Appearance;
|
||||
use warpui::{
|
||||
elements::{Container, MouseStateHandle},
|
||||
fonts::Weight,
|
||||
platform::Cursor,
|
||||
ui_components::{
|
||||
button::ButtonVariant,
|
||||
components::{Coords, UiComponent, UiComponentStyles},
|
||||
},
|
||||
Element,
|
||||
};
|
||||
|
||||
use crate::ui_components::dialog::{dialog_styles, Dialog};
|
||||
|
||||
use super::env_var_collection::{EnvVarCollectionAction, EnvVarCollectionView};
|
||||
|
||||
const UNSAVED_CHANGES_TEXT: &str = "You have unsaved changes.";
|
||||
const KEEP_EDITING_TEXT: &str = "Keep editing";
|
||||
const DISCARD_CHANGES_TEXT: &str = "Discard changes";
|
||||
const BUTTON_FONT_SIZE: f32 = 14.;
|
||||
const BUTTON_PADDING: f32 = 12.;
|
||||
const MODAL_HORIZONTAL_MARGIN: f32 = 28.;
|
||||
const DIALOG_WIDTH: f32 = 460.;
|
||||
|
||||
impl EnvVarCollectionView {
|
||||
pub fn render_unsaved_changes_dialog_button(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
button_mouse_state: MouseStateHandle,
|
||||
action: EnvVarCollectionAction,
|
||||
text: &str,
|
||||
) -> Box<dyn Element> {
|
||||
appearance
|
||||
.ui_builder()
|
||||
.button(ButtonVariant::Secondary, button_mouse_state)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(BUTTON_FONT_SIZE),
|
||||
font_weight: Some(Weight::Bold),
|
||||
padding: Some(Coords::uniform(BUTTON_PADDING)),
|
||||
..Default::default()
|
||||
})
|
||||
.with_text_label(text.into())
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(action.clone()))
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn render_unsaved_changes_dialog(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let keep_editing_button = self.render_unsaved_changes_dialog_button(
|
||||
appearance,
|
||||
self.button_mouse_states.keep_editing_state.clone(),
|
||||
EnvVarCollectionAction::CloseUnsavedChangesDialog,
|
||||
KEEP_EDITING_TEXT,
|
||||
);
|
||||
|
||||
let discard_changes_button = self.render_unsaved_changes_dialog_button(
|
||||
appearance,
|
||||
self.button_mouse_states.discard_changes_state.clone(),
|
||||
EnvVarCollectionAction::ForceClose,
|
||||
DISCARD_CHANGES_TEXT,
|
||||
);
|
||||
|
||||
Container::new(
|
||||
Dialog::new(
|
||||
UNSAVED_CHANGES_TEXT.to_string(),
|
||||
None,
|
||||
dialog_styles(appearance),
|
||||
)
|
||||
.with_bottom_row_child(keep_editing_button)
|
||||
.with_bottom_row_child(discard_changes_button)
|
||||
.with_width(DIALOG_WIDTH)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(MODAL_HORIZONTAL_MARGIN)
|
||||
.with_margin_right(MODAL_HORIZONTAL_MARGIN)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user