mirror of
https://github.com/enso-org/enso.git
synced 2025-01-03 12:34:06 +03:00
New notification system (#7339)
This PR consists of two primary changes: 1. I've replaced `react-hot-toast` with `react-toastify` library. Both serve the same purpose — sending popup notifications (so-called "toasts"). However, the latter comes with a richer feature set that matches our requirements much better. 2. I've exposed the relevant API surface to the Rust. Now Rust code can easily send notifications. ### Important Notes At this point, no attempt at customizing style of notifications was made (other than selecting the "light" theme). Likely we should consider this soon after integration as a separate task.
This commit is contained in:
parent
1d2371f986
commit
7211c8317d
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -3766,6 +3766,19 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-utils"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphql-introspection-query"
|
name = "graphql-introspection-query"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -4267,6 +4280,7 @@ dependencies = [
|
|||||||
"ensogl-hardcoded-theme",
|
"ensogl-hardcoded-theme",
|
||||||
"ensogl-text",
|
"ensogl-text",
|
||||||
"ensogl-text-msdf",
|
"ensogl-text-msdf",
|
||||||
|
"gloo-utils",
|
||||||
"ide-view-component-browser",
|
"ide-view-component-browser",
|
||||||
"ide-view-documentation",
|
"ide-view-documentation",
|
||||||
"ide-view-execution-environment-selector",
|
"ide-view-execution-environment-selector",
|
||||||
@ -4280,8 +4294,10 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"span-tree",
|
"span-tree",
|
||||||
|
"strum",
|
||||||
"uuid 0.8.2",
|
"uuid 0.8.2",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"welcome-screen",
|
"welcome-screen",
|
||||||
]
|
]
|
||||||
@ -7386,8 +7402,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
|
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ debug-assertions = true
|
|||||||
console-subscriber = "0.1.8"
|
console-subscriber = "0.1.8"
|
||||||
nix = "0.26.1"
|
nix = "0.26.1"
|
||||||
octocrab = { git = "https://github.com/enso-org/octocrab", default-features = false, features = [
|
octocrab = { git = "https://github.com/enso-org/octocrab", default-features = false, features = [
|
||||||
"rustls"
|
"rustls",
|
||||||
] }
|
] }
|
||||||
regex = { version = "1.6.0" }
|
regex = { version = "1.6.0" }
|
||||||
serde_yaml = { version = "0.9.16" }
|
serde_yaml = { version = "0.9.16" }
|
||||||
@ -93,7 +93,7 @@ serde-wasm-bindgen = { version = "0.4.5" }
|
|||||||
tokio = { version = "1.23.0", features = ["full", "tracing"] }
|
tokio = { version = "1.23.0", features = ["full", "tracing"] }
|
||||||
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||||
tokio-util = { version = "0.7.4", features = ["full"] }
|
tokio-util = { version = "0.7.4", features = ["full"] }
|
||||||
wasm-bindgen = { version = "0.2.84", features = ["serde-serialize"] }
|
wasm-bindgen = { version = "0.2.84", features = [] }
|
||||||
wasm-bindgen-test = { version = "0.3.34" }
|
wasm-bindgen-test = { version = "0.3.34" }
|
||||||
anyhow = { version = "1.0.66" }
|
anyhow = { version = "1.0.66" }
|
||||||
failure = { version = "0.1.8" }
|
failure = { version = "0.1.8" }
|
||||||
@ -135,7 +135,11 @@ syn = { version = "1.0", features = [
|
|||||||
] }
|
] }
|
||||||
quote = { version = "1.0.23" }
|
quote = { version = "1.0.23" }
|
||||||
semver = { version = "1.0.0", features = ["serde"] }
|
semver = { version = "1.0.0", features = ["serde"] }
|
||||||
|
strum = { version = "0.24.0", features = ["derive"] }
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||||
bitflags = { version = "2.2.1" }
|
bitflags = { version = "2.2.1" }
|
||||||
superslice = { version = "1.0.0" }
|
superslice = { version = "1.0.0" }
|
||||||
|
# We don't use serde-wasm-bindgen in some cases, because it cannot deal properly with flattened fields, see:
|
||||||
|
# https://github.com/cloudflare/serde-wasm-bindgen/issues/9
|
||||||
|
gloo-utils = { version = "0.1.7" }
|
||||||
|
@ -22,7 +22,7 @@ mockall = { version = "0.7.1", features = ["nightly"] }
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sha3 = { version = "0.8.2" }
|
sha3 = { version = "0.8.2" }
|
||||||
strum = "0.24.0"
|
strum = { workspace = true }
|
||||||
strum_macros = "0.24.0"
|
strum_macros = "0.24.0"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
||||||
|
|
||||||
|
@ -28,14 +28,17 @@ ide-view-documentation = { path = "documentation" }
|
|||||||
ide-view-graph-editor = { path = "graph-editor" }
|
ide-view-graph-editor = { path = "graph-editor" }
|
||||||
ide-view-project-view-top-bar = { path = "project-view-top-bar" }
|
ide-view-project-view-top-bar = { path = "project-view-top-bar" }
|
||||||
span-tree = { path = "../language/span-tree" }
|
span-tree = { path = "../language/span-tree" }
|
||||||
|
gloo-utils = { workspace = true }
|
||||||
js-sys = { workspace = true }
|
js-sys = { workspace = true }
|
||||||
multi-map = { workspace = true }
|
multi-map = { workspace = true }
|
||||||
nalgebra = { workspace = true }
|
nalgebra = { workspace = true }
|
||||||
ordered-float = { workspace = true }
|
ordered-float = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
strum = { workspace = true }
|
||||||
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
||||||
wasm-bindgen = { workspace = true }
|
wasm-bindgen = { workspace = true }
|
||||||
|
wasm-bindgen-test = { workspace = true }
|
||||||
welcome-screen = { path = "welcome-screen" }
|
welcome-screen = { path = "welcome-screen" }
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
#[allow(clippy::option_map_unit_fn)]
|
#[allow(clippy::option_map_unit_fn)]
|
||||||
pub mod code_editor;
|
pub mod code_editor;
|
||||||
pub mod debug_mode_popup;
|
pub mod debug_mode_popup;
|
||||||
|
pub mod notification;
|
||||||
pub mod popup;
|
pub mod popup;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod project_list;
|
pub mod project_list;
|
||||||
|
391
app/gui/view/src/notification.rs
Normal file
391
app/gui/view/src/notification.rs
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
//! This is Rust wrapper for the [`react-toastify`](https://fkhadra.github.io/react-toastify) library.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use ide_view::notification;
|
||||||
|
//! # fn main() -> Result<(), wasm_bindgen::JsValue> {
|
||||||
|
//! use ide_view::notification::UpdateOptions;
|
||||||
|
//! let handle = notification::info(
|
||||||
|
//! "Undo triggered in UI.",
|
||||||
|
//! &Some(notification::Options {
|
||||||
|
//! theme: Some(notification::Theme::Dark),
|
||||||
|
//! auto_close: Some(notification::AutoClose::Never()),
|
||||||
|
//! draggable: Some(false),
|
||||||
|
//! close_on_click: Some(false),
|
||||||
|
//! ..Default::default()
|
||||||
|
//! }),
|
||||||
|
//! )?;
|
||||||
|
//! handle.update(&UpdateOptions::default().render_string("Undo done."))?;
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use gloo_utils::format::JsValueSerdeExt;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
|
||||||
|
// ==============
|
||||||
|
// === Export ===
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
pub mod js;
|
||||||
|
|
||||||
|
pub use js::Id;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// === Primary API ===
|
||||||
|
// ===================
|
||||||
|
|
||||||
|
/// Send any kind of notification.
|
||||||
|
pub fn send_any(message: &str, r#type: Type, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||||
|
let options = match options {
|
||||||
|
Some(options) => options.try_into()?,
|
||||||
|
None => JsValue::UNDEFINED,
|
||||||
|
};
|
||||||
|
js::toast(message, r#type, &options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send an info notification.
|
||||||
|
pub fn info(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||||
|
send_any(message, Type::Info, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a warning notification.
|
||||||
|
pub fn warning(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||||
|
send_any(message, Type::Warning, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a error notification.
|
||||||
|
pub fn error(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||||
|
send_any(message, Type::Error, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a success notification.
|
||||||
|
pub fn success(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||||
|
send_any(message, Type::Success, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// === JS-conversion helpers ===
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
|
||||||
|
// === Rust->JS error ===
|
||||||
|
|
||||||
|
/// Convert arbitrary Rust error value into JsValue-based error.
|
||||||
|
pub fn to_js_error(error: impl std::error::Error) -> JsValue {
|
||||||
|
js_sys::Error::new(&error.to_string()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// === Rust->JS conversion ===
|
||||||
|
|
||||||
|
/// Macro that implements TryFrom for given `type` to/from JsValue using
|
||||||
|
/// [`gloo-utils::format::JsValueSerdeExt`].
|
||||||
|
///
|
||||||
|
/// Implements:
|
||||||
|
/// - `TryFrom<&type> for JsValue`
|
||||||
|
/// - `TryFrom<JsValue> for type`
|
||||||
|
macro_rules! impl_try_from_jsvalue {
|
||||||
|
($type:ty) => {
|
||||||
|
impl TryFrom<&$type> for JsValue {
|
||||||
|
type Error = JsValue;
|
||||||
|
|
||||||
|
fn try_from(value: &$type) -> Result<Self, Self::Error> {
|
||||||
|
JsValue::from_serde(value).map_err($crate::notification::to_js_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<JsValue> for $type {
|
||||||
|
type Error = JsValue;
|
||||||
|
|
||||||
|
fn try_from(js_value: JsValue) -> Result<Self, Self::Error> {
|
||||||
|
js_value.into_serde().map_err($crate::notification::to_js_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// === SerializableJsValue ===
|
||||||
|
|
||||||
|
/// A wrapper around a `JsValue` that implements the `Serialize` and `Deserialize` traits.
|
||||||
|
#[derive(Clone, Debug, Deref, DerefMut, AsRef, From)]
|
||||||
|
pub struct SerializableJsValue(pub JsValue);
|
||||||
|
|
||||||
|
impl Serialize for SerializableJsValue {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let value = js_sys::JSON::stringify(self).map(String::from).map_err(|e| {
|
||||||
|
serde::ser::Error::custom(format!("Failed to stringify JsValue: {e:?}"))
|
||||||
|
})?;
|
||||||
|
let value = serde_json::from_str::<serde_json::Value>(&value)
|
||||||
|
.map_err(|e| serde::ser::Error::custom(format!("Failed to parse JSON: {e:?}")))?;
|
||||||
|
value.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for SerializableJsValue {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
JsValue::from_serde(&value).map(SerializableJsValue).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===============
|
||||||
|
// === Options ===
|
||||||
|
// ===============
|
||||||
|
|
||||||
|
// === AutoClose ===
|
||||||
|
|
||||||
|
/// Helper structure for [`AutoClose`] so it serializes in a way compatible with the JS library.
|
||||||
|
///
|
||||||
|
/// Users of this module should not deal directly with this type. Instead, rely on
|
||||||
|
/// [`AutoClose::After`] or [`AutoClose::Never`]
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum AutoCloseInner {
|
||||||
|
/// Auto close after a delay expressed in milliseconds.
|
||||||
|
Delay(u32),
|
||||||
|
/// Do not auto close. The boolean value must be `false`.
|
||||||
|
ShouldEver(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the auto-close delay of a notification.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Deref)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct AutoClose(AutoCloseInner);
|
||||||
|
|
||||||
|
impl AutoClose {
|
||||||
|
/// Auto close after a delay expressed in milliseconds.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn After(delay_ms: u32) -> Self {
|
||||||
|
Self(AutoCloseInner::Delay(delay_ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do not auto close.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Never() -> Self {
|
||||||
|
Self(AutoCloseInner::ShouldEver(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// === Type ===
|
||||||
|
|
||||||
|
/// Represents the type of a notification.
|
||||||
|
///
|
||||||
|
/// Affects styling and icon.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::AsRefStr)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum Type {
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// === Position ===
|
||||||
|
|
||||||
|
/// Represents the position of a notification on the screen.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum Position {
|
||||||
|
TopRight,
|
||||||
|
TopCenter,
|
||||||
|
TopLeft,
|
||||||
|
BottomRight,
|
||||||
|
BottomCenter,
|
||||||
|
BottomLeft,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// === DraggableDirection ===
|
||||||
|
|
||||||
|
/// Direction that the notification can be dragged (swiped) to dismiss it.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum DraggableDirection {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// === Theme ===
|
||||||
|
|
||||||
|
/// Themes supported by the library.
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum Theme {
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
Colored,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// === Options ===
|
||||||
|
|
||||||
|
/// Customization options for the notification.
|
||||||
|
///
|
||||||
|
/// Note that it is not necessary to set any of these. All options marked as `None` will be
|
||||||
|
/// auto-filled with default values.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub struct Options {
|
||||||
|
/// Unique notification identifier.
|
||||||
|
pub toast_id: Option<String>,
|
||||||
|
/// Type of the notification, affecting styling and icon. Default: `Type::Default`.
|
||||||
|
pub r#type: Option<Type>,
|
||||||
|
/// The position where the toast should appear. Default: `bottom-right`.
|
||||||
|
pub position: Option<Position>,
|
||||||
|
/// Time in milliseconds after the toast is removed. Default: `5000`.
|
||||||
|
pub auto_close: Option<AutoClose>,
|
||||||
|
/// Whether to show a close button. Default: `true`.
|
||||||
|
pub close_button: Option<bool>,
|
||||||
|
// pub transition: Option<ToastTransition>,
|
||||||
|
/// Hide or show the progress bar. `Default: false`
|
||||||
|
pub hide_progress_bar: Option<bool>,
|
||||||
|
/// Pause the timer when the mouse hover the toast. Default: `true`.
|
||||||
|
pub pause_on_hover: Option<bool>,
|
||||||
|
/// Pause the timer when the window loses focus. Default: `true`.
|
||||||
|
pub pause_on_focus_loss: Option<bool>,
|
||||||
|
/// Remove the toast when clicked. Default: `true`.
|
||||||
|
pub close_on_click: Option<bool>,
|
||||||
|
/// An optional css class to set.
|
||||||
|
pub class_name: Option<String>,
|
||||||
|
/// An optional css class to set for the toast content.
|
||||||
|
pub body_class_name: Option<String>,
|
||||||
|
/// An optional inline style to apply. This should be a JS object with CSS properties.
|
||||||
|
pub style: Option<SerializableJsValue>,
|
||||||
|
/// An optional inline style to apply for the toast content. This should be a JS object with
|
||||||
|
/// CSS properties.
|
||||||
|
pub body_style: Option<SerializableJsValue>,
|
||||||
|
/// An optional css class to set for the progress bar.
|
||||||
|
pub progress_class_name: Option<String>,
|
||||||
|
/// An optional inline style to apply for the progress bar. This should be a JS object with
|
||||||
|
/// CSS properties.
|
||||||
|
pub progress_style: Option<SerializableJsValue>,
|
||||||
|
/// Allow toast to be draggable. `Default: true`.
|
||||||
|
pub draggable: Option<bool>,
|
||||||
|
/// The percentage of the toast's width it takes for a drag to dismiss a toast. `Default: 80`.
|
||||||
|
pub draggable_percent: Option<f32>,
|
||||||
|
/// Specify in which direction should you swipe to dismiss the toast. `Default: "x"`.
|
||||||
|
pub draggable_direction: Option<DraggableDirection>,
|
||||||
|
/// Set id to handle multiple `ToastContainer` instances.
|
||||||
|
pub container_id: Option<String>,
|
||||||
|
/// Define the [ARIA role](https://www.w3.org/WAI/PF/aria/roles) for the notification. `Default: "alert"`.
|
||||||
|
pub role: Option<String>,
|
||||||
|
/// Add a delay in ms before the toast appear.
|
||||||
|
pub delay: Option<u32>,
|
||||||
|
/// Whether the ongoing action is still in progress.
|
||||||
|
///
|
||||||
|
/// The auto-close timer will not start if this is set to `true`.
|
||||||
|
pub is_loading: Option<bool>,
|
||||||
|
/// Support right to left display content. `Default: false`.
|
||||||
|
pub rtl: Option<bool>,
|
||||||
|
/// Theme to use.
|
||||||
|
pub theme: Option<Theme>,
|
||||||
|
/// Used to display a custom icon. Set it to `false` to prevent the icons from being displayed
|
||||||
|
pub icon: Option<bool>,
|
||||||
|
/// Any additional data to pass to the toast.
|
||||||
|
pub data: Option<SerializableJsValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_try_from_jsvalue! {Options}
|
||||||
|
|
||||||
|
|
||||||
|
// === Update ===
|
||||||
|
|
||||||
|
/// Update options for a toast.
|
||||||
|
///
|
||||||
|
/// Just like [`Options`], but also includes a `render` field that allows to update the
|
||||||
|
/// notification message.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Deref, DerefMut)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateOptions {
|
||||||
|
/// New state of the toast.
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub options: Options,
|
||||||
|
/// Used to update a toast. Pass any valid ReactNode(string, number, component).
|
||||||
|
pub render: Option<SerializableJsValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_try_from_jsvalue! { UpdateOptions }
|
||||||
|
|
||||||
|
impl UpdateOptions {
|
||||||
|
/// Allows set a new message in the notification.
|
||||||
|
///
|
||||||
|
/// Useful only for update a toast.
|
||||||
|
pub fn render_string(mut self, render: impl AsRef<str>) -> Self {
|
||||||
|
let message = render.as_ref();
|
||||||
|
let value = JsValue::from_str(message);
|
||||||
|
self.render = Some(value.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// =============
|
||||||
|
// === Tests ===
|
||||||
|
// =============
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use wasm_bindgen_test::wasm_bindgen_test;
|
||||||
|
use wasm_bindgen_test::wasm_bindgen_test_configure;
|
||||||
|
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_auto_close_delay() {
|
||||||
|
let auto_close = AutoClose::After(5000);
|
||||||
|
let serialized = serde_json::to_string(&auto_close).unwrap();
|
||||||
|
assert_eq!(serialized, "5000");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_auto_close_never() {
|
||||||
|
let auto_close = AutoClose::Never();
|
||||||
|
let serialized = serde_json::to_string(&auto_close).unwrap();
|
||||||
|
assert_eq!(serialized, "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_options_marshalling() {
|
||||||
|
let options = Options { theme: Some(Theme::Dark), ..default() };
|
||||||
|
let js_value = JsValue::try_from(&options).unwrap();
|
||||||
|
// Make sure that `js_value` is a valid JS object.
|
||||||
|
assert!(js_value.is_object());
|
||||||
|
let js_object = js_value.dyn_into::<js_sys::Object>().unwrap();
|
||||||
|
// Make sure that `js_object` has a `theme` property.
|
||||||
|
assert!(js_object.has_own_property(&"theme".into()));
|
||||||
|
// Make sure that `js_object.theme` is a string.
|
||||||
|
let theme = js_sys::Reflect::get(&js_object, &"theme".into()).unwrap();
|
||||||
|
assert!(theme.is_string());
|
||||||
|
// Make sure that `js_object.theme` is equal to "dark".
|
||||||
|
let theme = theme.as_string().unwrap();
|
||||||
|
assert_eq!(theme, "dark");
|
||||||
|
}
|
||||||
|
}
|
171
app/gui/view/src/notification/js.rs
Normal file
171
app/gui/view/src/notification/js.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
//! This module provides relatively low-level wrappers for the
|
||||||
|
//! [`react-toastify`](https://fkhadra.github.io/react-toastify/introduction) library API.
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::notification::Type;
|
||||||
|
use crate::notification::UpdateOptions;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// === Constants ===
|
||||||
|
// =================
|
||||||
|
|
||||||
|
/// Toastify's [`toast API`](https://fkhadra.github.io/react-toastify/api/toast) field name within
|
||||||
|
/// our application JS object.
|
||||||
|
const TOAST_FIELD_NAME: &str = "toast";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// === Toast API Handle ===
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
/// Get the global (set in JS app object) toast API handle.
|
||||||
|
pub fn get_toast() -> Result<ToastAPI, JsValue> {
|
||||||
|
// let window = &ensogl::system::web::window; // JSValue wrapper
|
||||||
|
let window = ensogl::system::web::binding::wasm::get_window();
|
||||||
|
let app_field_name = enso_config::CONFIG.window_app_scope_name;
|
||||||
|
// Hopefully, window contains a field with the name, so we can access it.
|
||||||
|
let app = js_sys::Reflect::get(window.as_ref(), &app_field_name.into())?;
|
||||||
|
let toastify = js_sys::Reflect::get(app.as_ref(), &TOAST_FIELD_NAME.into())?;
|
||||||
|
Ok(toastify.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// === JS bindings ===
|
||||||
|
// ===================
|
||||||
|
|
||||||
|
// Wrappers for [`toast`](https://react-hot-toast.com/docs/toast) API.
|
||||||
|
#[wasm_bindgen(inline_js = r#"
|
||||||
|
export function sendToast(toast, message, method, options) {
|
||||||
|
const target = toast[method];
|
||||||
|
return target(message, options);
|
||||||
|
}
|
||||||
|
"#)]
|
||||||
|
extern "C" {
|
||||||
|
/// The wrapper for the toastify API.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub type ToastifyAPI;
|
||||||
|
|
||||||
|
/// The wrapper for the toast API.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub type ToastAPI;
|
||||||
|
|
||||||
|
/// The unique identifier of a toast.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub type Id;
|
||||||
|
|
||||||
|
/// The unique identifier of a toast.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub type ContainerId;
|
||||||
|
|
||||||
|
/// Generalized wrapper for calling any kind of toast.
|
||||||
|
#[wasm_bindgen(catch)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn sendToast(
|
||||||
|
this: &ToastAPI,
|
||||||
|
message: &str,
|
||||||
|
method: &str,
|
||||||
|
options: &JsValue,
|
||||||
|
) -> Result<Id, JsValue>;
|
||||||
|
|
||||||
|
/// Supply a promise or a function that return a promise and the notification will be
|
||||||
|
/// updated if it resolves or fails. When the promise is pending a spinner is displayed.
|
||||||
|
#[wasm_bindgen(catch, method, js_name = promise)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn promise(
|
||||||
|
this: &ToastAPI,
|
||||||
|
promise: &js_sys::Promise,
|
||||||
|
promise_params: &JsValue,
|
||||||
|
options: &JsValue,
|
||||||
|
) -> Result<Id, JsValue>;
|
||||||
|
|
||||||
|
/// Wrapper for dismissing a toast.
|
||||||
|
#[wasm_bindgen(catch, method)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn dismiss(this: &ToastAPI, id: &Id) -> Result<(), JsValue>;
|
||||||
|
|
||||||
|
/// Wrapper for dismissing a toast.
|
||||||
|
#[wasm_bindgen(catch, method, js_name = dismiss)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn dismiss_all(this: &ToastAPI) -> Result<(), JsValue>;
|
||||||
|
|
||||||
|
/// Check if a toast is displayed or not.
|
||||||
|
#[wasm_bindgen(catch, method, js_name = isActive)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn is_active(this: &ToastAPI, id: &Id) -> Result<bool, JsValue>;
|
||||||
|
|
||||||
|
/// Update a toast.
|
||||||
|
#[wasm_bindgen(catch, method)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn update(this: &ToastAPI, id: &Id, options: &JsValue) -> Result<(), JsValue>;
|
||||||
|
|
||||||
|
/// Clear waiting queue when working with limit in a specific container.
|
||||||
|
#[wasm_bindgen(catch, method, js_name = clearWaitingQueue)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn clear_waiting_queue_in(this: &ToastAPI, opts: &ContainerId) -> Result<(), JsValue>;
|
||||||
|
|
||||||
|
/// Clear waiting queue when working with limit in the default container.
|
||||||
|
#[wasm_bindgen(catch, method, js_name = clearWaitingQueue)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn clear_waiting_queue(this: &ToastAPI) -> Result<(), JsValue>;
|
||||||
|
|
||||||
|
/// Completes the controlled progress bar.
|
||||||
|
#[wasm_bindgen(catch, method)]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn done(this: &ToastAPI, id: &Id) -> Result<(), JsValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// === JS-types extensions ===
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
impl ToastAPI {
|
||||||
|
/// Send the toast notification.
|
||||||
|
pub fn send(&self, message: &str, method: &str, options: &JsValue) -> Result<Id, JsValue> {
|
||||||
|
sendToast(self, message, method, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Id {
|
||||||
|
/// Dismisses the toast.
|
||||||
|
pub fn dismiss(&self) -> Result<(), JsValue> {
|
||||||
|
get_toast()?.dismiss(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completes the controlled progress bar.
|
||||||
|
pub fn done(&self) -> Result<(), JsValue> {
|
||||||
|
get_toast()?.done(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a toast is displayed or not.
|
||||||
|
pub fn is_active(&self) -> Result<bool, JsValue> {
|
||||||
|
get_toast()?.is_active(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a toast.
|
||||||
|
pub fn update(&self, options: &UpdateOptions) -> Result<(), JsValue> {
|
||||||
|
get_toast()?.update(self, &options.try_into()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl ContainerId {
|
||||||
|
/// Clear queue of notifications for this container (relevant if limit is set).
|
||||||
|
pub fn clear_waiting_queue(&self) -> Result<(), JsValue> {
|
||||||
|
get_toast()?.clear_waiting_queue_in(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for sending arbitrary kind of toast.
|
||||||
|
pub fn toast(message: &str, r#type: Type, options: &JsValue) -> Result<Id, JsValue> {
|
||||||
|
let method: &str = r#type.as_ref();
|
||||||
|
get_toast()?.send(message, method, options)
|
||||||
|
}
|
@ -23,7 +23,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"enso-content-config": "^1.0.0"
|
"enso-content-config": "^1.0.0",
|
||||||
|
"react-toastify": "^9.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
* allowing to choose a debug rendering test from. */
|
* allowing to choose a debug rendering test from. */
|
||||||
|
|
||||||
import * as semver from 'semver'
|
import * as semver from 'semver'
|
||||||
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import * as common from 'enso-common'
|
import * as common from 'enso-common'
|
||||||
import * as contentConfig from 'enso-content-config'
|
import * as contentConfig from 'enso-content-config'
|
||||||
@ -152,6 +153,7 @@ interface AuthenticationConfig {
|
|||||||
/** Contains the entrypoint into the IDE. */
|
/** Contains the entrypoint into the IDE. */
|
||||||
class Main implements AppRunner {
|
class Main implements AppRunner {
|
||||||
app: app.App | null = null
|
app: app.App | null = null
|
||||||
|
toast = toastify.toast
|
||||||
|
|
||||||
/** Stop an app instance, if one is running. */
|
/** Stop an app instance, if one is running. */
|
||||||
stopApp() {
|
stopApp() {
|
||||||
|
@ -75,7 +75,7 @@ function esbuildPluginGenerateTailwind(): esbuild.Plugin {
|
|||||||
tailwindConfigLastModified !== tailwindConfigNewLastModified
|
tailwindConfigLastModified !== tailwindConfigNewLastModified
|
||||||
tailwindConfigLastModified = tailwindConfigNewLastModified
|
tailwindConfigLastModified = tailwindConfigNewLastModified
|
||||||
})
|
})
|
||||||
build.onLoad({ filter: /\.css$/ }, async loadArgs => {
|
build.onLoad({ filter: /tailwind\.css$/ }, async loadArgs => {
|
||||||
const lastModified = (await fs.stat(loadArgs.path)).mtimeMs
|
const lastModified = (await fs.stat(loadArgs.path)).mtimeMs
|
||||||
let output = cachedOutput[loadArgs.path]
|
let output = cachedOutput[loadArgs.path]
|
||||||
if (!output || output.lastModified !== lastModified || tailwindConfigWasModified) {
|
if (!output || output.lastModified !== lastModified || tailwindConfigWasModified) {
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"eslint": "^8.32.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-plugin-jsdoc": "^39.6.8",
|
"eslint-plugin-jsdoc": "^39.6.8",
|
||||||
"eslint-plugin-react": "^7.32.1",
|
"eslint-plugin-react": "^7.32.1",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"enso-common": "^1.0.0",
|
"enso-common": "^1.0.0",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-toastify": "^9.1.3",
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"ts-results": "^3.3.0"
|
"ts-results": "^3.3.0"
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* email address. */
|
* email address. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import * as app from '../../components/app'
|
import * as app from '../../components/app'
|
||||||
import * as authModule from '../providers/auth'
|
import * as authModule from '../providers/auth'
|
||||||
@ -41,7 +41,7 @@ function ConfirmRegistration() {
|
|||||||
navigate(app.LOGIN_PATH + location.search.toString())
|
navigate(app.LOGIN_PATH + location.search.toString())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error while confirming sign-up', error)
|
logger.error('Error while confirming sign-up', error)
|
||||||
toast.error(
|
toastify.toast.error(
|
||||||
'Something went wrong! Please try again or contact the administrators.'
|
'Something went wrong! Please try again or contact the administrators.'
|
||||||
)
|
)
|
||||||
navigate(app.LOGIN_PATH)
|
navigate(app.LOGIN_PATH)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Registration container responsible for rendering and interactions in sign up flow. */
|
/** @file Registration container responsible for rendering and interactions in sign up flow. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import AtIcon from 'enso-assets/at.svg'
|
import AtIcon from 'enso-assets/at.svg'
|
||||||
import CreateAccountIcon from 'enso-assets/create_account.svg'
|
import CreateAccountIcon from 'enso-assets/create_account.svg'
|
||||||
@ -41,7 +41,7 @@ function Registration() {
|
|||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
/** The password & confirm password fields must match. */
|
/** The password & confirm password fields must match. */
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
toast.error('Passwords do not match.')
|
toastify.toast.error('Passwords do not match.')
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} else {
|
} else {
|
||||||
return auth.signUp(email, password, organizationId)
|
return auth.signUp(email, password, organizationId)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* flow. */
|
* flow. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||||
import AtIcon from 'enso-assets/at.svg'
|
import AtIcon from 'enso-assets/at.svg'
|
||||||
@ -44,7 +44,7 @@ function ResetPassword() {
|
|||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (newPassword !== newPasswordConfirm) {
|
if (newPassword !== newPasswordConfirm) {
|
||||||
toast.error('Passwords do not match')
|
toastify.toast.error('Passwords do not match')
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} else {
|
} else {
|
||||||
return resetPassword(email, code, newPassword)
|
return resetPassword(email, code, newPassword)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
* hook also provides methods for registering a user, logging in, logging out, etc. */
|
* hook also provides methods for registering a user, logging in, logging out, etc. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import * as app from '../../components/app'
|
import * as app from '../../components/app'
|
||||||
import * as authServiceModule from '../service'
|
import * as authServiceModule from '../service'
|
||||||
@ -211,7 +211,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const goOffline = React.useCallback(
|
const goOffline = React.useCallback(
|
||||||
(shouldShowToast = true) => {
|
(shouldShowToast = true) => {
|
||||||
if (shouldShowToast) {
|
if (shouldShowToast) {
|
||||||
toast.error('You are offline, switching to offline mode.')
|
toastify.toast.error('You are offline, switching to offline mode.')
|
||||||
}
|
}
|
||||||
goOfflineInternal()
|
goOfflineInternal()
|
||||||
navigate(app.DASHBOARD_PATH)
|
navigate(app.DASHBOARD_PATH)
|
||||||
@ -311,7 +311,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
|
|
||||||
fetchSession().catch(error => {
|
fetchSession().catch(error => {
|
||||||
if (isUserFacingError(error)) {
|
if (isUserFacingError(error)) {
|
||||||
toast.error(error.message)
|
toastify.toast.error(error.message)
|
||||||
logger.error(error.message)
|
logger.error(error.message)
|
||||||
} else {
|
} else {
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
@ -336,12 +336,12 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const withLoadingToast =
|
const withLoadingToast =
|
||||||
<T extends unknown[], R>(action: (...args: T) => Promise<R>) =>
|
<T extends unknown[], R>(action: (...args: T) => Promise<R>) =>
|
||||||
async (...args: T) => {
|
async (...args: T) => {
|
||||||
const loadingToast = toast.loading(MESSAGES.pleaseWait)
|
const loadingToast = toastify.toast.loading(MESSAGES.pleaseWait)
|
||||||
let result
|
let result
|
||||||
try {
|
try {
|
||||||
result = await action(...args)
|
result = await action(...args)
|
||||||
} finally {
|
} finally {
|
||||||
toast.dismiss(loadingToast)
|
toastify.toast.dismiss(loadingToast)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -349,10 +349,10 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const signUp = async (username: string, password: string, organizationId: string | null) => {
|
const signUp = async (username: string, password: string, organizationId: string | null) => {
|
||||||
const result = await cognito.signUp(username, password, organizationId)
|
const result = await cognito.signUp(username, password, organizationId)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
toast.success(MESSAGES.signUpSuccess)
|
toastify.toast.success(MESSAGES.signUpSuccess)
|
||||||
navigate(app.LOGIN_PATH)
|
navigate(app.LOGIN_PATH)
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.val.message)
|
toastify.toast.error(result.val.message)
|
||||||
}
|
}
|
||||||
return result.ok
|
return result.ok
|
||||||
}
|
}
|
||||||
@ -368,7 +368,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(MESSAGES.confirmSignUpSuccess)
|
toastify.toast.success(MESSAGES.confirmSignUpSuccess)
|
||||||
navigate(app.LOGIN_PATH)
|
navigate(app.LOGIN_PATH)
|
||||||
return result.ok
|
return result.ok
|
||||||
}
|
}
|
||||||
@ -376,13 +376,13 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const signInWithPassword = async (email: string, password: string) => {
|
const signInWithPassword = async (email: string, password: string) => {
|
||||||
const result = await cognito.signInWithPassword(email, password)
|
const result = await cognito.signInWithPassword(email, password)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
toast.success(MESSAGES.signInWithPasswordSuccess)
|
toastify.toast.success(MESSAGES.signInWithPasswordSuccess)
|
||||||
} else {
|
} else {
|
||||||
if (result.val.kind === 'UserNotFound') {
|
if (result.val.kind === 'UserNotFound') {
|
||||||
navigate(app.REGISTRATION_PATH)
|
navigate(app.REGISTRATION_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(result.val.message)
|
toastify.toast.error(result.val.message)
|
||||||
}
|
}
|
||||||
return result.ok
|
return result.ok
|
||||||
}
|
}
|
||||||
@ -393,14 +393,14 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
email: string
|
email: string
|
||||||
) => {
|
) => {
|
||||||
if (backend.type === backendModule.BackendType.local) {
|
if (backend.type === backendModule.BackendType.local) {
|
||||||
toast.error('You cannot set your username on the local backend.')
|
toastify.toast.error('You cannot set your username on the local backend.')
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const organizationId = await authService.cognito.organizationId()
|
const organizationId = await authService.cognito.organizationId()
|
||||||
// This should not omit success and error toasts as it is not possible
|
// This should not omit success and error toasts as it is not possible
|
||||||
// to render this optimistically.
|
// to render this optimistically.
|
||||||
await toast.promise(
|
await toastify.toast.promise(
|
||||||
backend.createUser({
|
backend.createUser({
|
||||||
userName: username,
|
userName: username,
|
||||||
userEmail: backendModule.EmailAddress(email),
|
userEmail: backendModule.EmailAddress(email),
|
||||||
@ -412,7 +412,7 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
{
|
{
|
||||||
success: MESSAGES.setUsernameSuccess,
|
success: MESSAGES.setUsernameSuccess,
|
||||||
error: MESSAGES.setUsernameFailure,
|
error: MESSAGES.setUsernameFailure,
|
||||||
loading: MESSAGES.setUsernameLoading,
|
pending: MESSAGES.setUsernameLoading,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
navigate(app.DASHBOARD_PATH)
|
navigate(app.DASHBOARD_PATH)
|
||||||
@ -426,10 +426,10 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const forgotPassword = async (email: string) => {
|
const forgotPassword = async (email: string) => {
|
||||||
const result = await cognito.forgotPassword(email)
|
const result = await cognito.forgotPassword(email)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
toast.success(MESSAGES.forgotPasswordSuccess)
|
toastify.toast.success(MESSAGES.forgotPasswordSuccess)
|
||||||
navigate(app.RESET_PASSWORD_PATH)
|
navigate(app.RESET_PASSWORD_PATH)
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.val.message)
|
toastify.toast.error(result.val.message)
|
||||||
}
|
}
|
||||||
return result.ok
|
return result.ok
|
||||||
}
|
}
|
||||||
@ -437,10 +437,10 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const resetPassword = async (email: string, code: string, password: string) => {
|
const resetPassword = async (email: string, code: string, password: string) => {
|
||||||
const result = await cognito.forgotPasswordSubmit(email, code, password)
|
const result = await cognito.forgotPasswordSubmit(email, code, password)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
toast.success(MESSAGES.resetPasswordSuccess)
|
toastify.toast.success(MESSAGES.resetPasswordSuccess)
|
||||||
navigate(app.LOGIN_PATH)
|
navigate(app.LOGIN_PATH)
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.val.message)
|
toastify.toast.error(result.val.message)
|
||||||
}
|
}
|
||||||
return result.ok
|
return result.ok
|
||||||
}
|
}
|
||||||
@ -448,9 +448,9 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
const changePassword = async (oldPassword: string, newPassword: string) => {
|
const changePassword = async (oldPassword: string, newPassword: string) => {
|
||||||
const result = await cognito.changePassword(oldPassword, newPassword)
|
const result = await cognito.changePassword(oldPassword, newPassword)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
toast.success(MESSAGES.changePasswordSuccess)
|
toastify.toast.success(MESSAGES.changePasswordSuccess)
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.val.message)
|
toastify.toast.error(result.val.message)
|
||||||
}
|
}
|
||||||
return result.ok
|
return result.ok
|
||||||
}
|
}
|
||||||
@ -461,10 +461,10 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
setUserSession(null)
|
setUserSession(null)
|
||||||
// This should not omit success and error toasts as it is not possible
|
// This should not omit success and error toasts as it is not possible
|
||||||
// to render this optimistically.
|
// to render this optimistically.
|
||||||
await toast.promise(cognito.signOut(), {
|
await toastify.toast.promise(cognito.signOut(), {
|
||||||
success: MESSAGES.signOutSuccess,
|
success: MESSAGES.signOutSuccess,
|
||||||
error: MESSAGES.signOutError,
|
error: MESSAGES.signOutError,
|
||||||
loading: MESSAGES.signOutLoading,
|
pending: MESSAGES.signOutLoading,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import * as toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import * as detect from 'enso-common/src/detect'
|
import * as detect from 'enso-common/src/detect'
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ function App(props: AppProps) {
|
|||||||
* will redirect the user between the login/register pages and the dashboard. */
|
* will redirect the user between the login/register pages and the dashboard. */
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<toast.Toaster toastOptions={{ style: { maxWidth: '100%' } }} position="top-center" />
|
<toastify.ToastContainer position="top-center" theme="light" closeOnClick={false} />
|
||||||
<Router basename={getMainPageUrl().pathname}>
|
<Router basename={getMainPageUrl().pathname}>
|
||||||
<AppRouter {...props} />
|
<AppRouter {...props} />
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Managing the logic and displaying the UI for the password change function. */
|
/** @file Managing the logic and displaying the UI for the password change function. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||||
import LockIcon from 'enso-assets/lock.svg'
|
import LockIcon from 'enso-assets/lock.svg'
|
||||||
@ -29,7 +29,7 @@ function ChangePasswordModal() {
|
|||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (newPassword !== confirmNewPassword) {
|
if (newPassword !== confirmNewPassword) {
|
||||||
toast.error('Passwords do not match.')
|
toastify.toast.error('Passwords do not match.')
|
||||||
} else {
|
} else {
|
||||||
const success = await changePassword(oldPassword, newPassword)
|
const success = await changePassword(oldPassword, newPassword)
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/** @file A WebSocket-based chat directly to official support on the official Discord server. */
|
/** @file A WebSocket-based chat directly to official support on the official Discord server. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as reactDom from 'react-dom'
|
import * as reactDom from 'react-dom'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import CloseLargeIcon from 'enso-assets/close_large.svg'
|
import CloseLargeIcon from 'enso-assets/close_large.svg'
|
||||||
import DefaultUserIcon from 'enso-assets/default_user.svg'
|
import DefaultUserIcon from 'enso-assets/default_user.svg'
|
||||||
@ -611,7 +611,7 @@ function Chat(props: ChatProps) {
|
|||||||
const threadData = threads.find(thread => thread.id === newThreadId)
|
const threadData = threads.find(thread => thread.id === newThreadId)
|
||||||
if (threadData == null) {
|
if (threadData == null) {
|
||||||
const message = `Unknown thread id '${newThreadId}'.`
|
const message = `Unknown thread id '${newThreadId}'.`
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Modal for confirming delete of any type of asset. */
|
/** @file Modal for confirming delete of any type of asset. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import CloseIcon from 'enso-assets/close.svg'
|
import CloseIcon from 'enso-assets/close.svg'
|
||||||
|
|
||||||
@ -32,10 +32,8 @@ function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
|
|||||||
try {
|
try {
|
||||||
doDelete()
|
doDelete()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Could not delete ${description}: ${
|
const message = errorModule.getMessageOrToString(error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
toastify.toast.error(message)
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Table displaying a list of directories. */
|
/** @file Table displaying a list of directories. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import DirectoryIcon from 'enso-assets/directory.svg'
|
import DirectoryIcon from 'enso-assets/directory.svg'
|
||||||
import PlusIcon from 'enso-assets/plus.svg'
|
import PlusIcon from 'enso-assets/plus.svg'
|
||||||
@ -111,7 +111,6 @@ function DirectoryName(props: InternalDirectoryNameProps) {
|
|||||||
rowState,
|
rowState,
|
||||||
setRowState,
|
setRowState,
|
||||||
} = props
|
} = props
|
||||||
const logger = loggerProvider.useLogger()
|
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
|
|
||||||
const doRename = async (newName: string) => {
|
const doRename = async (newName: string) => {
|
||||||
@ -120,11 +119,7 @@ function DirectoryName(props: InternalDirectoryNameProps) {
|
|||||||
await backend.updateDirectory(item.id, { title: newName }, item.title)
|
await backend.updateDirectory(item.id, { title: newName }, item.title)
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Error renaming folder: ${
|
errorModule.toastAndLog('Error renaming folder', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,11 +269,7 @@ function DirectoryRow(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(presence.Presence.present)
|
setStatus(presence.Presence.present)
|
||||||
markItemAsVisible(key)
|
markItemAsVisible(key)
|
||||||
const message = `Unable to delete directory: ${
|
errorModule.toastAndLog('Unable to delete directory', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,7 +280,7 @@ function DirectoryRow(
|
|||||||
if (key === event.placeholderId) {
|
if (key === event.placeholderId) {
|
||||||
if (backend.type !== backendModule.BackendType.remote) {
|
if (backend.type !== backendModule.BackendType.remote) {
|
||||||
const message = 'Folders cannot be created on the local backend.'
|
const message = 'Folders cannot be created on the local backend.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
setStatus(presence.Presence.inserting)
|
setStatus(presence.Presence.inserting)
|
||||||
@ -309,11 +300,7 @@ function DirectoryRow(
|
|||||||
type: directoryListEventModule.DirectoryListEventType.delete,
|
type: directoryListEventModule.DirectoryListEventType.delete,
|
||||||
directoryId: key,
|
directoryId: key,
|
||||||
})
|
})
|
||||||
const message = `Error creating new folder: ${
|
errorModule.toastAndLog('Error creating new folder', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file The directory header bar and directory item listing. */
|
/** @file The directory header bar and directory item listing. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import * as common from 'enso-common'
|
import * as common from 'enso-common'
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ function DirectoryView(props: DirectoryViewProps) {
|
|||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
if (!newProjectAssets.some(asset => asset.title === initialProjectName)) {
|
if (!newProjectAssets.some(asset => asset.title === initialProjectName)) {
|
||||||
const errorMessage = `No project named '${initialProjectName}' was found.`
|
const errorMessage = `No project named '${initialProjectName}' was found.`
|
||||||
toast.error(errorMessage)
|
toastify.toast.error(errorMessage)
|
||||||
logger.error(`Error opening project on startup: ${errorMessage}`)
|
logger.error(`Error opening project on startup: ${errorMessage}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Header menubar for the directory listing, containing information about
|
/** @file Header menubar for the directory listing, containing information about
|
||||||
* the current directory and some configuration options. */
|
* the current directory and some configuration options. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import ArrowRightSmallIcon from 'enso-assets/arrow_right_small.svg'
|
import ArrowRightSmallIcon from 'enso-assets/arrow_right_small.svg'
|
||||||
import DownloadIcon from 'enso-assets/download.svg'
|
import DownloadIcon from 'enso-assets/download.svg'
|
||||||
@ -52,18 +52,18 @@ function DriveBar(props: DriveBarProps) {
|
|||||||
// TODO[sb]: Allow uploading `.enso-project`s
|
// TODO[sb]: Allow uploading `.enso-project`s
|
||||||
// https://github.com/enso-org/cloud-v2/issues/510
|
// https://github.com/enso-org/cloud-v2/issues/510
|
||||||
const message = 'Files cannot be uploaded to the local backend.'
|
const message = 'Files cannot be uploaded to the local backend.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else if (
|
} else if (
|
||||||
event.currentTarget.files == null ||
|
event.currentTarget.files == null ||
|
||||||
event.currentTarget.files.length === 0
|
event.currentTarget.files.length === 0
|
||||||
) {
|
) {
|
||||||
toast.success('No files selected to upload.')
|
toastify.toast.success('No files selected to upload.')
|
||||||
} else if (directoryId == null) {
|
} else if (directoryId == null) {
|
||||||
// This should never happen, however display a nice error message in case
|
// This should never happen, however display a nice error message in case
|
||||||
// it somehow does.
|
// it somehow does.
|
||||||
const message = 'Files cannot be uploaded while offline.'
|
const message = 'Files cannot be uploaded while offline.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
dispatchFileListEvent({
|
dispatchFileListEvent({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Table displaying a list of files. */
|
/** @file Table displaying a list of files. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import PlusIcon from 'enso-assets/plus.svg'
|
import PlusIcon from 'enso-assets/plus.svg'
|
||||||
|
|
||||||
@ -76,18 +76,18 @@ function FileNameHeading(props: InternalFileNameHeadingProps) {
|
|||||||
// TODO[sb]: Allow uploading `.enso-project`s
|
// TODO[sb]: Allow uploading `.enso-project`s
|
||||||
// https://github.com/enso-org/cloud-v2/issues/510
|
// https://github.com/enso-org/cloud-v2/issues/510
|
||||||
const message = 'Files cannot be uploaded to the local backend.'
|
const message = 'Files cannot be uploaded to the local backend.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else if (
|
} else if (
|
||||||
event.currentTarget.files == null ||
|
event.currentTarget.files == null ||
|
||||||
event.currentTarget.files.length === 0
|
event.currentTarget.files.length === 0
|
||||||
) {
|
) {
|
||||||
toast.success('No files selected to upload.')
|
toastify.toast.success('No files selected to upload.')
|
||||||
} else if (directoryId == null) {
|
} else if (directoryId == null) {
|
||||||
// This should never happen, however display a nice error message in case
|
// This should never happen, however display a nice error message in case
|
||||||
// it somehow does.
|
// it somehow does.
|
||||||
const message = 'Files cannot be uploaded while offline.'
|
const message = 'Files cannot be uploaded while offline.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
dispatchFileListEvent({
|
dispatchFileListEvent({
|
||||||
@ -284,11 +284,7 @@ function FileRow(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(presence.Presence.present)
|
setStatus(presence.Presence.present)
|
||||||
markItemAsVisible(key)
|
markItemAsVisible(key)
|
||||||
const message = `Unable to delete file: ${
|
errorModule.toastAndLog('Unable to delete file', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -300,7 +296,7 @@ function FileRow(
|
|||||||
if (file != null) {
|
if (file != null) {
|
||||||
if (backend.type !== backendModule.BackendType.remote) {
|
if (backend.type !== backendModule.BackendType.remote) {
|
||||||
const message = 'Files cannot be uploaded on the local backend.'
|
const message = 'Files cannot be uploaded on the local backend.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
setStatus(presence.Presence.inserting)
|
setStatus(presence.Presence.inserting)
|
||||||
@ -324,11 +320,7 @@ function FileRow(
|
|||||||
type: fileListEventModule.FileListEventType.delete,
|
type: fileListEventModule.FileListEventType.delete,
|
||||||
fileId: key,
|
fileId: key,
|
||||||
})
|
})
|
||||||
const message = `Error creating new file: ${
|
errorModule.toastAndLog('Error creating new file', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file A modal with inputs for user email and permission level. */
|
/** @file A modal with inputs for user email and permission level. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import CloseIcon from 'enso-assets/close.svg'
|
import CloseIcon from 'enso-assets/close.svg'
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ export function ManagePermissionsModal(props: ManagePermissionsModalProps) {
|
|||||||
userEmail: backendModule.EmailAddress(firstEmail),
|
userEmail: backendModule.EmailAddress(firstEmail),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(errorModule.tryGetMessage(error) ?? 'Unknown error.')
|
toastify.toast.error(errorModule.tryGetMessage(error) ?? 'Unknown error.')
|
||||||
}
|
}
|
||||||
} else if (finalUsers.length !== 0) {
|
} else if (finalUsers.length !== 0) {
|
||||||
unsetModal()
|
unsetModal()
|
||||||
@ -250,7 +250,9 @@ export function ManagePermissionsModal(props: ManagePermissionsModalProps) {
|
|||||||
} catch {
|
} catch {
|
||||||
onFailure?.(finalUsers, permissionsArray)
|
onFailure?.(finalUsers, permissionsArray)
|
||||||
const finalUserEmails = finalUsers.map(finalUser => `'${finalUser.email}'`)
|
const finalUserEmails = finalUsers.map(finalUser => `'${finalUser.email}'`)
|
||||||
toast.error(`Unable to set permissions of ${finalUserEmails.join(', ')}.`)
|
toastify.toast.error(
|
||||||
|
`Unable to set permissions of ${finalUserEmails.join(', ')}.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Table displaying a list of projects. */
|
/** @file Table displaying a list of projects. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import ArrowUpIcon from 'enso-assets/arrow_up.svg'
|
import ArrowUpIcon from 'enso-assets/arrow_up.svg'
|
||||||
import PlayIcon from 'enso-assets/play.svg'
|
import PlayIcon from 'enso-assets/play.svg'
|
||||||
@ -14,7 +14,6 @@ import * as dateTime from '../dateTime'
|
|||||||
import * as errorModule from '../../error'
|
import * as errorModule from '../../error'
|
||||||
import * as eventModule from '../event'
|
import * as eventModule from '../event'
|
||||||
import * as hooks from '../../hooks'
|
import * as hooks from '../../hooks'
|
||||||
import * as loggerProvider from '../../providers/logger'
|
|
||||||
import * as modalProvider from '../../providers/modal'
|
import * as modalProvider from '../../providers/modal'
|
||||||
import * as permissions from '../permissions'
|
import * as permissions from '../permissions'
|
||||||
import * as presence from '../presence'
|
import * as presence from '../presence'
|
||||||
@ -157,14 +156,14 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
((state: spinner.SpinnerState | null) => void) | null
|
((state: spinner.SpinnerState | null) => void) | null
|
||||||
>(null)
|
>(null)
|
||||||
const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false)
|
const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false)
|
||||||
const [toastId, setToastId] = React.useState<string | null>(null)
|
const [toastId, setToastId] = React.useState<toastify.Id | null>(null)
|
||||||
|
|
||||||
const openProject = React.useCallback(async () => {
|
const openProject = React.useCallback(async () => {
|
||||||
setState(backendModule.ProjectState.openInProgress)
|
setState(backendModule.ProjectState.openInProgress)
|
||||||
try {
|
try {
|
||||||
switch (backend.type) {
|
switch (backend.type) {
|
||||||
case backendModule.BackendType.remote:
|
case backendModule.BackendType.remote:
|
||||||
setToastId(toast.loading(LOADING_MESSAGE))
|
setToastId(toastify.toast.loading(LOADING_MESSAGE))
|
||||||
setRowState({ ...rowState, isRunning: true })
|
setRowState({ ...rowState, isRunning: true })
|
||||||
await backend.openProject(project.id, null, project.title)
|
await backend.openProject(project.id, null, project.title)
|
||||||
setCheckState(CheckState.checkingStatus)
|
setCheckState(CheckState.checkingStatus)
|
||||||
@ -183,7 +182,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCheckState(CheckState.notChecking)
|
setCheckState(CheckState.notChecking)
|
||||||
toast.error(
|
toastify.toast.error(
|
||||||
`Error opening project '${project.title}': ${
|
`Error opening project '${project.title}': ${
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error'
|
errorModule.tryGetMessage(error) ?? 'unknown error'
|
||||||
}.`
|
}.`
|
||||||
@ -195,7 +194,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (toastId != null) {
|
if (toastId != null) {
|
||||||
return () => {
|
return () => {
|
||||||
toast.dismiss(toastId)
|
toastify.toast.dismiss(toastId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
@ -222,7 +221,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (toastId != null && state !== backendModule.ProjectState.openInProgress) {
|
if (toastId != null && state !== backendModule.ProjectState.openInProgress) {
|
||||||
toast.dismiss(toastId)
|
toastify.toast.dismiss(toastId)
|
||||||
}
|
}
|
||||||
}, [state, toastId])
|
}, [state, toastId])
|
||||||
|
|
||||||
@ -501,7 +500,6 @@ function ProjectName(props: InternalProjectNameProps) {
|
|||||||
doCloseIde,
|
doCloseIde,
|
||||||
},
|
},
|
||||||
} = props
|
} = props
|
||||||
const logger = loggerProvider.useLogger()
|
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
|
|
||||||
const doRename = async (newName: string) => {
|
const doRename = async (newName: string) => {
|
||||||
@ -517,11 +515,7 @@ function ProjectName(props: InternalProjectNameProps) {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Unable to rename project: ${
|
errorModule.toastAndLog('Unable to rename project', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -701,7 +695,6 @@ function ProjectRow(
|
|||||||
markItemAsVisible,
|
markItemAsVisible,
|
||||||
},
|
},
|
||||||
} = props
|
} = props
|
||||||
const logger = loggerProvider.useLogger()
|
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
const { setModal } = modalProvider.useSetModal()
|
const { setModal } = modalProvider.useSetModal()
|
||||||
const [item, setItem] = React.useState(rawItem)
|
const [item, setItem] = React.useState(rawItem)
|
||||||
@ -723,9 +716,7 @@ function ProjectRow(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(presence.Presence.present)
|
setStatus(presence.Presence.present)
|
||||||
markItemAsVisible(key)
|
markItemAsVisible(key)
|
||||||
const message = errorModule.tryGetMessage(error) ?? 'Unable to delete project.'
|
errorModule.toastAndLog('Unable to delete project', error)
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -764,11 +755,7 @@ function ProjectRow(
|
|||||||
type: projectListEventModule.ProjectListEventType.delete,
|
type: projectListEventModule.ProjectListEventType.delete,
|
||||||
projectId: key,
|
projectId: key,
|
||||||
})
|
})
|
||||||
const message = `Error creating new project: ${
|
errorModule.toastAndLog('Error creating new project', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Table displaying a list of secrets. */
|
/** @file Table displaying a list of secrets. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
import PlusIcon from 'enso-assets/plus.svg'
|
import PlusIcon from 'enso-assets/plus.svg'
|
||||||
import SecretIcon from 'enso-assets/secret.svg'
|
import SecretIcon from 'enso-assets/secret.svg'
|
||||||
@ -79,10 +79,10 @@ function SecretCreateForm(props: InternalSecretCreateFormProps) {
|
|||||||
const onSubmit = (event: React.FormEvent) => {
|
const onSubmit = (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
toast.error('Please provide a secret name.')
|
toastify.toast.error('Please provide a secret name.')
|
||||||
} else if (value == null) {
|
} else if (value == null) {
|
||||||
// Secret value explicitly can be empty.
|
// Secret value explicitly can be empty.
|
||||||
toast.error('Please provide a secret value.')
|
toastify.toast.error('Please provide a secret value.')
|
||||||
} else {
|
} else {
|
||||||
unsetModal()
|
unsetModal()
|
||||||
dispatchSecretListEvent({
|
dispatchSecretListEvent({
|
||||||
@ -322,11 +322,7 @@ function SecretRow(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(presence.Presence.present)
|
setStatus(presence.Presence.present)
|
||||||
markItemAsVisible(key)
|
markItemAsVisible(key)
|
||||||
const message = `Unable to delete secret: ${
|
errorModule.toastAndLog('Unable to delete secret', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -337,7 +333,7 @@ function SecretRow(
|
|||||||
if (key === event.placeholderId) {
|
if (key === event.placeholderId) {
|
||||||
if (backend.type !== backendModule.BackendType.remote) {
|
if (backend.type !== backendModule.BackendType.remote) {
|
||||||
const message = 'Secrets cannot be created on the local backend.'
|
const message = 'Secrets cannot be created on the local backend.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
setStatus(presence.Presence.inserting)
|
setStatus(presence.Presence.inserting)
|
||||||
@ -358,11 +354,7 @@ function SecretRow(
|
|||||||
type: secretListEventModule.SecretListEventType.delete,
|
type: secretListEventModule.SecretListEventType.delete,
|
||||||
secretId: key,
|
secretId: key,
|
||||||
})
|
})
|
||||||
const message = `Error creating new secret: ${
|
errorModule.toastAndLog('Error creating new secret', error)
|
||||||
errorModule.tryGetMessage(error) ?? 'unknown error.'
|
|
||||||
}`
|
|
||||||
toast.error(message)
|
|
||||||
logger.error(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -492,7 +484,7 @@ function SecretsTable(props: SecretsTableProps) {
|
|||||||
case secretListEventModule.SecretListEventType.create: {
|
case secretListEventModule.SecretListEventType.create: {
|
||||||
if (backend.type !== backendModule.BackendType.remote) {
|
if (backend.type !== backendModule.BackendType.remote) {
|
||||||
const message = 'Secrets cannot be created on the local backend.'
|
const message = 'Secrets cannot be created on the local backend.'
|
||||||
toast.error(message)
|
toastify.toast.error(message)
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
} else {
|
} else {
|
||||||
const placeholderItem: backendModule.SecretAsset = {
|
const placeholderItem: backendModule.SecretAsset = {
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
// === tryGetMessage ===
|
// === tryGetMessage ===
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
|
import * as loggerProvider from './providers/logger'
|
||||||
|
|
||||||
/** Evaluates the given type only if it the exact same type as {@link Expected}. */
|
/** Evaluates the given type only if it the exact same type as {@link Expected}. */
|
||||||
type MustBe<T, Expected> = (<U>() => U extends T ? 1 : 2) extends <U>() => U extends Expected
|
type MustBe<T, Expected> = (<U>() => U extends T ? 1 : 2) extends <U>() => U extends Expected
|
||||||
? 1
|
? 1
|
||||||
@ -29,6 +33,37 @@ export function tryGetMessage(error: unknown): string | null {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Like {@link tryGetMessage} but return the string representation of the value if it is not an
|
||||||
|
* {@link Error} */
|
||||||
|
export function getMessageOrToString<T>(error: unknown) {
|
||||||
|
return tryGetMessage(error) ?? String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return a toastify option object that renders an error message. */
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export function render(f: (message: string) => string): toastify.UpdateOptions {
|
||||||
|
return { render: ({ data }) => f(getMessageOrToString(data)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send a toast with rendered error message. Same message is logged as an error.
|
||||||
|
*
|
||||||
|
* @param messagePrefix - a prefix to add to the error message, should represent the immediate
|
||||||
|
* error context.
|
||||||
|
* @param error - the error to render, which will be appended to the message prefix.
|
||||||
|
* @param options - additional options to pass to the toast API.
|
||||||
|
* @returns - the toast ID. */
|
||||||
|
export function toastAndLog(
|
||||||
|
messagePrefix: string,
|
||||||
|
error?: unknown,
|
||||||
|
options?: toastify.ToastOptions
|
||||||
|
) {
|
||||||
|
const message =
|
||||||
|
error == null ? `${messagePrefix}.` : `${messagePrefix}: ${getMessageOrToString(error)}`
|
||||||
|
const id = toastify.toast.error(message, options)
|
||||||
|
loggerProvider.useLogger().error(message)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// === UnreachableCaseError ===
|
// === UnreachableCaseError ===
|
||||||
// ============================
|
// ============================
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;600;700&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=M+PLUS+1:wght@500;600;700&display=swap");
|
||||||
|
@import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
78
app/ide-desktop/package-lock.json
generated
78
app/ide-desktop/package-lock.json
generated
@ -387,7 +387,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"enso-content-config": "^1.0.0"
|
"enso-content-config": "^1.0.0",
|
||||||
|
"react-toastify": "^9.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
@ -443,11 +444,12 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||||
"@typescript-eslint/parser": "^5.49.0",
|
"@typescript-eslint/parser": "^5.49.0",
|
||||||
"enso-authentication": "^1.0.0",
|
"enso-authentication": "^1.0.0",
|
||||||
"enso-chat": "git+ssh://git@github.com/enso-org/enso-bot.git#3b76888ec6bd3579016e70ef83ba282714aec47d",
|
"enso-chat": "git://github.com/enso-org/enso-bot#wip/sb/initial-implementation",
|
||||||
"enso-content": "^1.0.0",
|
"enso-content": "^1.0.0",
|
||||||
"eslint": "^8.32.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-plugin-jsdoc": "^39.6.8",
|
"eslint-plugin-jsdoc": "^39.6.8",
|
||||||
"eslint-plugin-react": "^7.32.1",
|
"eslint-plugin-react": "^7.32.1",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
@ -525,8 +527,8 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"enso-common": "^1.0.0",
|
"enso-common": "^1.0.0",
|
||||||
"react-hot-toast": "^2.4.0",
|
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"ts-results": "^3.3.0"
|
"ts-results": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -6818,6 +6820,14 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/collection-visit": {
|
"node_modules/collection-visit": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -9283,13 +9293,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/goober": {
|
|
||||||
"version": "2.1.12",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"csstype": "^3.0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -13501,20 +13504,6 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hot-toast": {
|
|
||||||
"version": "2.4.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"goober": "^2.1.10"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16",
|
|
||||||
"react-dom": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -13689,6 +13678,18 @@
|
|||||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-toastify": {
|
||||||
|
"version": "9.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
|
||||||
|
"integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16",
|
||||||
|
"react-dom": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -20625,6 +20626,11 @@
|
|||||||
"mimic-response": "^1.0.0"
|
"mimic-response": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"clsx": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||||
|
},
|
||||||
"collection-visit": {
|
"collection-visit": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@ -21455,8 +21461,8 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"enso-common": "^1.0.0",
|
"enso-common": "^1.0.0",
|
||||||
"react-hot-toast": "^2.4.0",
|
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"ts-results": "^3.3.0",
|
"ts-results": "^3.3.0",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
@ -21503,6 +21509,7 @@
|
|||||||
"eslint-plugin-jsdoc": "^40.0.2",
|
"eslint-plugin-jsdoc": "^40.0.2",
|
||||||
"globals": "^13.20.0",
|
"globals": "^13.20.0",
|
||||||
"portfinder": "^1.0.32",
|
"portfinder": "^1.0.32",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"tsx": "^3.12.6",
|
"tsx": "^3.12.6",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
@ -21524,7 +21531,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||||
"@typescript-eslint/parser": "^5.49.0",
|
"@typescript-eslint/parser": "^5.49.0",
|
||||||
"enso-authentication": "^1.0.0",
|
"enso-authentication": "^1.0.0",
|
||||||
"enso-chat": "git+ssh://git@github.com/enso-org/enso-bot.git#3b76888ec6bd3579016e70ef83ba282714aec47d",
|
"enso-chat": "git://github.com/enso-org/enso-bot#wip/sb/initial-implementation",
|
||||||
"enso-content": "^1.0.0",
|
"enso-content": "^1.0.0",
|
||||||
"esbuild": "^0.17.15",
|
"esbuild": "^0.17.15",
|
||||||
"esbuild-plugin-time": "^1.0.0",
|
"esbuild-plugin-time": "^1.0.0",
|
||||||
@ -21534,6 +21541,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.7.0",
|
"react-router-dom": "^6.7.0",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
@ -22624,10 +22632,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"goober": {
|
|
||||||
"version": "2.1.12",
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"gopd": {
|
"gopd": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -25408,12 +25412,6 @@
|
|||||||
"scheduler": "^0.23.0"
|
"scheduler": "^0.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-hot-toast": {
|
|
||||||
"version": "2.4.0",
|
|
||||||
"requires": {
|
|
||||||
"goober": "^2.1.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"peer": true
|
"peer": true
|
||||||
@ -25540,6 +25538,14 @@
|
|||||||
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-toastify": {
|
||||||
|
"version": "9.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
|
||||||
|
"integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
|
||||||
|
"requires": {
|
||||||
|
"clsx": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"read-cache": {
|
"read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -63,7 +63,7 @@ serde = { version = "1.0.130", features = ["derive"] }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
scopeguard = "1.1.0"
|
scopeguard = "1.1.0"
|
||||||
strum = { version = "0.24.0", features = ["derive"] }
|
strum = { workspace = true }
|
||||||
sysinfo = "0.26.2"
|
sysinfo = "0.26.2"
|
||||||
tar = "0.4.37"
|
tar = "0.4.37"
|
||||||
tempfile = "3.2.0"
|
tempfile = "3.2.0"
|
||||||
|
@ -66,7 +66,7 @@ serde_json = { workspace = true }
|
|||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
scopeguard = "1.1.0"
|
scopeguard = "1.1.0"
|
||||||
sha2 = "0.10.2"
|
sha2 = "0.10.2"
|
||||||
strum = { version = "0.24.0", features = ["derive"] }
|
strum = { workspace = true }
|
||||||
symlink = "0.1.0"
|
symlink = "0.1.0"
|
||||||
syn = { workspace = true }
|
syn = { workspace = true }
|
||||||
sysinfo = "0.26.2"
|
sysinfo = "0.26.2"
|
||||||
|
@ -23,7 +23,7 @@ octocrab = { workspace = true }
|
|||||||
serde = { version = "1.0.130", features = ["derive"] }
|
serde = { version = "1.0.130", features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
strum = { version = "0.24.0", features = ["derive"] }
|
strum = { workspace = true }
|
||||||
tempfile = "3.2.0"
|
tempfile = "3.2.0"
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
|
Loading…
Reference in New Issue
Block a user