Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -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();
}
}
+521
View File
@@ -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(&current_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(&current_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, &current_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()
}
}
+496
View File
@@ -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,
)
});
}
}
}
+9
View File
@@ -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;
+245
View File
@@ -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()
}
}