diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index c44f8fc842ba..f2000ebf09c0 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -55,7 +55,7 @@ jobs: run: ./run git-clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: ./run backend benchmark runtime enso + - run: ./run backend benchmark runtime env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: failure() && runner.os == 'Windows' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d15ffb8d1185..cc9bce064998 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -4,7 +4,7 @@ name: Nightly Release on: schedule: - - cron: 0 5 * * 2-6 + - cron: 0 2 * * 2-6 workflow_dispatch: {} jobs: promote-nightly: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2954f2490125..0c9ec402f119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,14 +135,22 @@ - [Added capability to create node widgets with complex UI][6347]. Node widgets such as dropdown can now be placed in the node and affect the code text flow. - [The IDE UI element for selecting the execution mode of the project is now - sending messages to the backend][6341]. + sending messages to the backend.][6341]. +- [Feedback when renaming a project][6366]. When the user tries to rename the + project to an invalid name, a helpful error message is shown and the text + field stays the same as to give the user the opportunity to fix the mistake. +- [Area selectionof nodes no longer takes into account the visualisation that + belongs to the node.][6487]. - [List Editor Widget][6470]. Now you can edit lists by clicking buttons on nodes or by dragging the elements. - [Fixed text visualisations which were being cut off at the last line.][6421] +- [Fixed a bug where, when scrolling or dragging on a full-screen visualization, + the view of the graph changed as well.][6530] - [Added a button to return from an opened project back to the project dashboard.][6474] [6421]: https://github.com/enso-org/enso/pull/6421 +[6530]: https://github.com/enso-org/enso/pull/6530 [6474]: https://github.com/enso-org/enso/pull/6474 #### EnsoGL (rendering engine) @@ -206,6 +214,9 @@ [5895]: https://github.com/enso-org/enso/pull/6130 [6035]: https://github.com/enso-org/enso/pull/6035 [6097]: https://github.com/enso-org/enso/pull/6097 +[6097]: https://github.com/enso-org/enso/pull/6341 +[6366]: https://github.com/enso-org/enso/pull/6366 +[6487]: https://github.com/enso-org/enso/pull/6487 [6341]: https://github.com/enso-org/enso/pull/6341 [6470]: https://github.com/enso-org/enso/pull/6470 @@ -411,6 +422,9 @@ `Text.write`.][6459] - [Implemented `create_database_table` allowing saving queries as database tables.][6467] +- [Implemented `Column.format` for in-memory `Column`s.][6538] +- [Added `at_least_one` flag to `Table.tokenize_to_rows`.][6539] +- [Moved `Redshift` connector into a separate `AWS` library.][6550] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -617,6 +631,9 @@ [6429]: https://github.com/enso-org/enso/pull/6429 [6459]: https://github.com/enso-org/enso/pull/6459 [6467]: https://github.com/enso-org/enso/pull/6467 +[6538]: https://github.com/enso-org/enso/pull/6538 +[6539]: https://github.com/enso-org/enso/pull/6539 +[6550]: https://github.com/enso-org/enso/pull/6550 #### Enso Compiler diff --git a/Cargo.lock b/Cargo.lock index 77f6d6fc1859..ce47542256e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,7 @@ dependencies = [ "enso-macro-utils", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -278,7 +278,7 @@ checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -295,7 +295,7 @@ checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -961,7 +961,7 @@ dependencies = [ "cached_proc_macro_types", "darling", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1055,7 +1055,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1399,7 +1399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1426,7 +1426,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.107", ] [[package]] @@ -1443,7 +1443,7 @@ checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1467,7 +1467,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.107", ] [[package]] @@ -1478,7 +1478,7 @@ checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1658,7 +1658,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1671,7 +1671,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.0", - "syn", + "syn 1.0.107", ] [[package]] @@ -1962,7 +1962,7 @@ dependencies = [ "enso-build-macros-lib", "itertools", "proc-macro2", - "syn", + "syn 1.0.107", ] [[package]] @@ -1978,7 +1978,7 @@ dependencies = [ "quote", "regex", "serde_yaml", - "syn", + "syn 1.0.107", ] [[package]] @@ -2019,6 +2019,7 @@ dependencies = [ "enso-prelude", "ensogl", "semver 1.0.16", + "thiserror", ] [[package]] @@ -2218,7 +2219,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2227,7 +2228,7 @@ version = "0.2.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2323,7 +2324,7 @@ dependencies = [ "enso-macro-utils", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2416,7 +2417,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2434,7 +2435,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2462,7 +2463,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-test", ] @@ -2634,7 +2635,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3267,7 +3268,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3328,7 +3329,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "synstructure", ] @@ -3427,7 +3428,7 @@ checksum = "236b4e4ae2b8be5f7a5652f6108c4a0f2627c569db4e7923333d31c7dbfed0fb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3559,7 +3560,7 @@ checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3731,7 +3732,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 1.0.107", ] [[package]] @@ -3742,7 +3743,7 @@ checksum = "a755cc59cda2641ea3037b4f9f7ef40471c329f55c1fa2db6fa0bb7ae6c1f7ce" dependencies = [ "graphql_client_codegen", "proc-macro2", - "syn", + "syn 1.0.107", ] [[package]] @@ -4150,7 +4151,7 @@ dependencies = [ "sha2", "strum", "symlink", - "syn", + "syn 1.0.107", "sysinfo", "tar", "tempfile", @@ -4343,6 +4344,7 @@ dependencies = [ "js-sys", "nalgebra", "ordered-float", + "parser", "serde", "serde-wasm-bindgen", "serde_json", @@ -4653,7 +4655,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -4783,7 +4785,7 @@ dependencies = [ "cfg-if 0.1.10", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5025,7 +5027,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5107,7 +5109,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5164,7 +5166,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5332,7 +5334,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5363,7 +5365,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5511,7 +5513,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "version_check", ] @@ -5534,9 +5536,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -5561,7 +5563,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -5603,9 +5605,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -6137,7 +6139,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -6343,7 +6345,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -6431,7 +6433,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.107", ] [[package]] @@ -6451,6 +6453,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.1" @@ -6465,7 +6478,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "unicode-xid", ] @@ -6554,22 +6567,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -6683,7 +6696,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -6889,7 +6902,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -7281,7 +7294,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -7315,7 +7328,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index b22bd852415d..e00b7ac0e0e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,3 +134,5 @@ syn = { version = "1.0", features = [ "visit-mut", ] } quote = { version = "1.0.23" } +semver = { version = "1.0.0", features = ["serde"] } +thiserror = "1.0.40" diff --git a/app/gui/Cargo.toml b/app/gui/Cargo.toml index 5a57c3454fcd..500fe57b4ad2 100644 --- a/app/gui/Cargo.toml +++ b/app/gui/Cargo.toml @@ -50,7 +50,7 @@ itertools = { workspace = true } js-sys = { workspace = true } mockall = { version = "0.7.1", features = ["nightly"] } nalgebra = { workspace = true } -semver = { version = "1.0.0", features = ["serde"] } +semver = { workspace = true } serde = { version = "1.0", features = ["derive"] } serde_json = { workspace = true } sha3 = { version = "0.8.2" } diff --git a/app/gui/config.yaml b/app/gui/config.yaml index e6ca0802fd42..08ece4c7e1b0 100644 --- a/app/gui/config.yaml +++ b/app/gui/config.yaml @@ -20,8 +20,8 @@ minimumSupportedVersion": "2.0.0-alpha.6" # The minimum engine version supported by the application. The projects opened with the older versions # will have the "Unsupported engine version" message displayed. -engineVersionSupported: "2023.1.1-nightly.2023.2.8" +engineVersionSupported: "2023.1.1" # The minimum language edition supported by the application. It will be displayed as edition user # should put in their package.yaml file to have project compatible with the IDE. -languageEditionSupported: "2023.1.1-nightly.2023.2.8" +languageEditionSupported: "2023.1.1" diff --git a/app/gui/config/Cargo.toml b/app/gui/config/Cargo.toml index 6b574e3c7dae..bb4bc0343e6c 100644 --- a/app/gui/config/Cargo.toml +++ b/app/gui/config/Cargo.toml @@ -8,7 +8,8 @@ edition = "2021" ensogl = { path = "../../../lib/rust/ensogl" } enso-prelude = { path = "../../../lib/rust/prelude" } enso-json-to-struct = { path = "../../../lib/rust/json-to-struct" } -semver = "1.0.0" +semver = { workspace = true } +thiserror = { workspace = true } [build-dependencies] config-reader = { path = "../../../lib/rust/config-reader" } diff --git a/app/gui/config/src/lib.rs b/app/gui/config/src/lib.rs index ca71959078b0..b01b31c494bb 100644 --- a/app/gui/config/src/lib.rs +++ b/app/gui/config/src/lib.rs @@ -20,15 +20,74 @@ use enso_json_to_struct::json_to_struct; // ============== -// === Config === +// === Errors === // ============== +///Error type with information that the Engine version does not meet the requirements. +#[derive(Clone, Debug, thiserror::Error)] +#[error("Unsupported Engine version: required {required} (or newer), found {found}.")] +pub struct UnsupportedEngineVersion { + /// The version of the Engine that is required. + pub required: semver::Version, + /// The version of the Engine that was found. + pub found: semver::Version, +} + + + +// =============== +// === Version === +// =============== + include!(concat!(env!("OUT_DIR"), "/config.rs")); pub use generated::*; -pub fn engine_version_requirement() -> semver::VersionReq { - semver::VersionReq::parse(&format!(">={engine_version_supported}")).unwrap() +/// The minimum supported engine version. +pub fn engine_version_required() -> semver::Version { + // Safe to unwrap, as `engine_version_supported` compile-time and is validated by the test. + semver::Version::parse(engine_version_supported).unwrap() +} + +/// Check if the given Engine version meets the requirements. +/// +/// Effectively, this checks if the given version is greater or equal to the minimum supported. +/// "Greater or equal" is defined by the [Semantic Versioning specification](https://semver.org/) +/// term of precedence. +pub fn check_engine_version_requirement( + required_version: &semver::Version, + tested_version: &semver::Version, +) -> Result<(), UnsupportedEngineVersion> { + // We don't want to rely on the `semver::VersionReq` semantics here. Unfortunately the + // [Semantic Versioning specification](https://semver.org/) does not define the semantics of + // the version requirement operators, so different implementations may behave differently. + // + // The `semver::VersionReq` implementation follows the Cargo's implementation, namely: + // ``` + // In particular, in order for any VersionReq to match a pre-release version, the VersionReq + // must contain at least one Comparator that has an explicit major, minor, and patch version + // identical to the pre-release being matched, and that has a nonempty pre-release component. + // ``` + // See: https://docs.rs/semver/latest/semver/struct.VersionReq.html#associatedconstant.STAR + // This leads to counter-intuitive behavior, where `2023.0.0-dev` does not fulfill the + // `>= 2022.0.0-dev` requirement. + if tested_version < required_version { + Err(UnsupportedEngineVersion { + required: required_version.clone(), + found: tested_version.clone(), + }) + } else { + Ok(()) + } +} + +/// Check if the given Engine version meets the requirements for this build. +/// +/// See [`check_engine_version_requirement`] for more details. +pub fn check_engine_version( + engine_version: &semver::Version, +) -> Result<(), UnsupportedEngineVersion> { + check_engine_version_requirement(&engine_version_required(), engine_version) } @@ -64,3 +123,37 @@ pub fn read_args() -> Args { lazy_static! { pub static ref ARGS: Args = read_args(); } + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_that_version_requirement_parses() { + // We just expect that it won't panic. + let _ = engine_version_required(); + } + + #[test] + fn new_project_engine_version_fills_requirements() { + // Sanity check: required version must be supported. + assert!(check_engine_version(&engine_version_required()).is_ok()); + } + + #[test] + fn newer_prerelease_matches() -> anyhow::Result<()> { + // Whatever version we have currently defined with `-dev` prerelease. + let current = + semver::Version { pre: semver::Prerelease::new("dev")?, ..engine_version_required() }; + let newer = semver::Version { major: current.major + 1, ..current.clone() }; + + check_engine_version_requirement(¤t, &newer)?; + Ok(()) + } +} diff --git a/app/gui/controller/engine-protocol/src/common/error.rs b/app/gui/controller/engine-protocol/src/common/error.rs index ea842f8c46c6..adbe8c46aadd 100644 --- a/app/gui/controller/engine-protocol/src/common/error.rs +++ b/app/gui/controller/engine-protocol/src/common/error.rs @@ -30,4 +30,7 @@ pub mod code { /// Signals that requested project is already under version control. pub const VCS_ALREADY_EXISTS: i64 = 1005; + + /// Signals that project name is invalid. + pub const PROJECT_NAME_INVALID: i64 = 4001; } diff --git a/app/gui/src/controller/project.rs b/app/gui/src/controller/project.rs index 6c9c205b754a..e78384ead0cd 100644 --- a/app/gui/src/controller/project.rs +++ b/app/gui/src/controller/project.rs @@ -214,16 +214,8 @@ impl Project { } fn display_warning_on_unsupported_engine_version(&self) { - let requirement = enso_config::engine_version_requirement(); - let version = self.model.engine_version(); - if !requirement.matches(&version) { - let message = format!( - "Unsupported Engine version. Please update edition in {} \ - to {}.", - package_yaml_path(&self.model.name()), - enso_config::language_edition_supported - ); - self.status_notifications.publish_event(message); + if let Err(e) = enso_config::check_engine_version(&self.model.engine_version()) { + self.status_notifications.publish_event(e.to_string()); } } } @@ -273,19 +265,6 @@ mod tests { use engine_protocol::language_server; use std::assert_matches::assert_matches; - #[test] - fn parse_supported_engine_version() { - // Should not panic. - enso_config::engine_version_requirement(); - } - - #[test] - fn new_project_engine_version_fills_requirements() { - let requirements = enso_config::engine_version_requirement(); - let version = semver::Version::parse(enso_config::engine_version_supported).unwrap(); - assert!(requirements.matches(&version)) - } - #[wasm_bindgen_test] fn adding_missing_main() { let _ctx = TestWithLocalPoolExecutor::set_up(); diff --git a/app/gui/src/model/project/synchronized.rs b/app/gui/src/model/project/synchronized.rs index 4967efe096b5..62e92a6ba246 100644 --- a/app/gui/src/model/project/synchronized.rs +++ b/app/gui/src/model/project/synchronized.rs @@ -224,6 +224,13 @@ async fn update_modules_on_file_change( #[fail(display = "Project Manager is unavailable.")] pub struct ProjectManagerUnavailable; +/// An error signalling the project name was invalid. +#[derive(Clone, Debug, Fail)] +#[fail(display = "The project name is not allowed: {}", cause)] +pub struct ProjectNameInvalid { + cause: String, +} + #[allow(missing_docs)] #[derive(Clone, Copy, Debug, Fail)] #[fail(display = "Project renaming is not available in read-only mode.")] @@ -233,8 +240,9 @@ pub struct RenameInReadOnly; /// engine's version (which is likely the cause of the problems). #[derive(Debug, Fail)] pub struct UnsupportedEngineVersion { - project_name: String, - root_cause: failure::Error, + project_name: String, + version_mismatch: enso_config::UnsupportedEngineVersion, + root_cause: failure::Error, } impl UnsupportedEngineVersion { @@ -242,10 +250,13 @@ impl UnsupportedEngineVersion { let engine_version = properties.engine_version.clone(); let project_name = properties.name.project.as_str().to_owned(); move |root_cause| { - let requirement = enso_config::engine_version_requirement(); - if !requirement.matches(&engine_version) { - let project_name = project_name.clone(); - UnsupportedEngineVersion { project_name, root_cause }.into() + if let Err(version_mismatch) = enso_config::check_engine_version(&engine_version) { + UnsupportedEngineVersion { + project_name: project_name.clone(), + version_mismatch, + root_cause, + } + .into() } else { root_cause } @@ -720,7 +731,14 @@ impl model::project::API for Project { self.project_manager.as_ref().ok_or(ProjectManagerUnavailable)?; let project_id = self.properties.borrow().id; let project_name = ProjectName::new_unchecked(name); - project_manager.rename_project(&project_id, &project_name).await?; + project_manager.rename_project(&project_id, &project_name).await.map_err( + |error| match error { + RpcError::RemoteError(cause) + if cause.code == code::PROJECT_NAME_INVALID => + failure::Error::from(ProjectNameInvalid { cause: cause.message }), + error => error.into(), + }, + )?; self.properties.borrow_mut().name.project = referent_name.clone_ref(); self.execution_contexts.rename_project(old_name, referent_name); Ok(()) diff --git a/app/gui/src/presenter/project.rs b/app/gui/src/presenter/project.rs index 78f6218b4ced..ca3ccda55cc1 100644 --- a/app/gui/src/presenter/project.rs +++ b/app/gui/src/presenter/project.rs @@ -4,6 +4,7 @@ use crate::prelude::*; use crate::executor::global::spawn_stream_handler; +use crate::model::project::synchronized::ProjectNameInvalid; use crate::presenter; use crate::presenter::graph::ViewNodeId; @@ -159,11 +160,23 @@ impl Model { if self.controller.model.name() != name.as_ref() { let project = self.controller.model.clone_ref(); let breadcrumbs = self.view.graph().model.breadcrumbs.clone_ref(); + let popup = self.view.popup().clone_ref(); let name = name.into(); executor::global::spawn(async move { - if let Err(e) = project.rename_project(name).await { - error!("The project couldn't be renamed: {e}"); - breadcrumbs.cancel_project_name_editing.emit(()); + if let Err(error) = project.rename_project(name).await { + let error_message = match error.downcast::() { + Ok(error) => error.to_string(), + Err(error) => { + // Other errors aren't geared towards users, so display a generic + // message. + let prefix = "The project couldn't be renamed".to_string(); + error!("{prefix}: {error}"); + prefix + } + }; + popup.set_label.emit(error_message); + // Reset name to old, valid value + breadcrumbs.input.project_name.emit(project.name()); } }); } diff --git a/app/gui/view/documentation/src/html.rs b/app/gui/view/documentation/src/html.rs index 4d28ce1de4b9..802a7c3a1416 100644 --- a/app/gui/view/documentation/src/html.rs +++ b/app/gui/view/documentation/src/html.rs @@ -63,16 +63,29 @@ fn svg_icon(content: &'static str) -> impl Render { /// Render entry documentation to HTML code with Tailwind CSS styles. #[profile(Detail)] pub fn render(docs: EntryDocumentation) -> String { - match docs { + let html = match docs { EntryDocumentation::Placeholder(placeholder) => match placeholder { Placeholder::NoDocumentation => String::from("No documentation available."), Placeholder::VirtualComponentGroup { name } => render_virtual_component_group_docs(name), }, EntryDocumentation::Docs(docs) => render_documentation(docs), + }; + match validate_utf8(&html) { + Ok(_) => html, + Err(_) => { + error!("Internal error. Generated HTML is not valid utf-8. This is bug #5813."); + String::from("Failed to load documentation.") + } } } +#[profile(Debug)] +fn validate_utf8(s: &str) -> Result<&str, std::str::Utf8Error> { + let bytes = s.as_bytes(); + std::str::from_utf8(bytes) +} + fn render_documentation(docs: Documentation) -> String { match docs { Documentation::Module(module_docs) => render_module_documentation(&module_docs, None), diff --git a/app/gui/view/documentation/src/lib.rs b/app/gui/view/documentation/src/lib.rs index a1f827815cf3..1436b07675a7 100644 --- a/app/gui/view/documentation/src/lib.rs +++ b/app/gui/view/documentation/src/lib.rs @@ -18,21 +18,12 @@ //! [`Tailwind CSS`]: https://tailwindcss.com/ // === Features === -#![feature(associated_type_bounds)] -#![feature(associated_type_defaults)] #![feature(drain_filter)] -#![feature(fn_traits)] #![feature(option_result_contains)] -#![feature(specialization)] -#![feature(trait_alias)] -#![feature(type_alias_impl_trait)] -#![feature(unboxed_closures)] // === Standard Linter Configuration === #![deny(non_ascii_idents)] -#![warn(unsafe_code)] #![allow(clippy::bool_to_int_with_if)] #![allow(clippy::let_and_return)] -#![allow(incomplete_features)] // To be removed, see: https://github.com/enso-org/ide/issues/1559 #![warn(missing_copy_implementations)] #![warn(missing_debug_implementations)] #![warn(missing_docs)] @@ -64,10 +55,8 @@ use ensogl::Animation; use ensogl_component::shadow; use ensogl_derive_theme::FromTheme; use ensogl_hardcoded_theme::application::component_browser::documentation as theme; -use web::Closure; use web::HtmlElement; use web::JsCast; -use web::MouseEvent; pub mod html; @@ -89,6 +78,7 @@ const MIN_CAPTION_HEIGHT: f32 = 1.0; /// Delay before updating the displayed documentation. const DISPLAY_DELAY_MS: i32 = 0; + // === Style === #[derive(Debug, Clone, Copy, Default, FromTheme)] @@ -108,24 +98,20 @@ pub struct Style { // === Model === // ============= -type CodeCopyClosure = Closure; - /// Model of Native visualization that generates documentation for given Enso code and embeds /// it in a HTML container. #[derive(Clone, CloneRef, Debug)] #[allow(missing_docs)] pub struct Model { - outer_dom: DomSymbol, - caption_dom: DomSymbol, - inner_dom: DomSymbol, + outer_dom: DomSymbol, + caption_dom: DomSymbol, + inner_dom: DomSymbol, /// The purpose of this overlay is stop propagating mouse events under the documentation panel /// to EnsoGL shapes, and pass them to the DOM instead. - overlay: overlay::View, - display_object: display::object::Instance, - code_copy_closures: Rc>>, + overlay: overlay::View, + display_object: display::object::Instance, } - impl Model { /// Constructor. fn new(scene: &Scene) -> Self { @@ -164,9 +150,7 @@ impl Model { scene.dom.layers.node_searcher.manage(&inner_dom); scene.dom.layers.node_searcher.manage(&caption_dom); - let code_copy_closures = default(); - Model { outer_dom, inner_dom, caption_dom, overlay, display_object, code_copy_closures } - .init() + Model { outer_dom, inner_dom, caption_dom, overlay, display_object }.init() } fn init(self) -> Self { diff --git a/app/gui/view/examples/interface/src/lib.rs b/app/gui/view/examples/interface/src/lib.rs index d8a9639068be..a8c275ebb1ff 100644 --- a/app/gui/view/examples/interface/src/lib.rs +++ b/app/gui/view/examples/interface/src/lib.rs @@ -19,6 +19,7 @@ use ensogl::prelude::*; use enso_frp as frp; use ensogl::application::Application; +use ensogl::control::io::mouse; use ensogl::display::object::ObjectOps; use ensogl::display::shape::StyleWatch; use ensogl::gui::text; @@ -260,6 +261,23 @@ fn init(app: &Application) { graph_editor.set_available_execution_environments(make_dummy_execution_environments()); + // === Pop-up === + + // Create node to trigger a pop-up. + let node_id = graph_editor.model.add_node(); + graph_editor.frp.set_node_position.emit((node_id, Vector2(-300.0, -100.0))); + let expression = expression_mock_string("Click me to show a pop-up"); + graph_editor.frp.set_node_expression.emit((node_id, expression)); + let node = graph_editor.nodes().all.get_cloned_ref(&node_id).unwrap(); + + let popup = project_view.popup(); + let network = node.network(); + let node_clicked = node.on_event::(); + frp::extend! { network + eval_ node_clicked (popup.set_label.emit("This is a test pop-up.")); + } + + // === Rendering === // let tgt_type = dummy_type_generator.get_dummy_type(); diff --git a/app/gui/view/graph-editor/Cargo.toml b/app/gui/view/graph-editor/Cargo.toml index 5566d5961139..6b4bd34dc32f 100644 --- a/app/gui/view/graph-editor/Cargo.toml +++ b/app/gui/view/graph-editor/Cargo.toml @@ -29,6 +29,7 @@ indexmap = "1.9.2" js-sys = { workspace = true } nalgebra = { workspace = true } ordered-float = { workspace = true } +parser = { path = "../../language/parser" } serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = { workspace = true } serde_json = { workspace = true } diff --git a/app/gui/view/graph-editor/src/component/breadcrumbs.rs b/app/gui/view/graph-editor/src/component/breadcrumbs.rs index d30ad663a546..17ba6ff12ff3 100644 --- a/app/gui/view/graph-editor/src/component/breadcrumbs.rs +++ b/app/gui/view/graph-editor/src/component/breadcrumbs.rs @@ -81,12 +81,6 @@ ensogl::define_endpoints! { /// Signalizes a mouse press happened outside the breadcrumb panel. It's used to finish /// project renaming, committing the name in text field. outside_press (), - /// Signalizes we want to cancel project name renaming, bringing back the project name - /// before editing. - cancel_project_name_editing (), - /// Signalizes we want to start editing the project name. Adds a cursor to the text edit - /// field at the mouse position. - start_project_name_editing (), /// Sets the project name. project_name (String), /// Select the breadcrumb by its index. @@ -126,6 +120,8 @@ ensogl::define_endpoints! { project_name_hovered (bool), /// Indicates whether the project name was clicked. project_mouse_down (), + /// Signalizes an error if the user tried to rename the project to an invalid name. + project_name_error (String), /// Indicates if the read-only mode is enabled. read_only(bool), } @@ -478,7 +474,6 @@ impl Breadcrumbs { eval frp.input.project_name((name) model.project_name.set_name.emit(name)); frp.source.project_name <+ model.project_name.output.name; - eval_ frp.input.start_project_name_editing( model.project_name.start_editing.emit(()) ); eval frp.ide_text_edit_mode((value) model.project_name.ide_text_edit_mode.emit(value) ); frp.source.project_name_hovered <+ model.project_name.is_hovered; @@ -486,10 +481,12 @@ impl Breadcrumbs { eval frp.input.set_project_changed((v) model.project_name.set_project_changed(v)); + frp.source.project_name_error <+ model.project_name.error; + + // === User Interaction === frp.select_breadcrumb <+ model.project_name.frp.output.mouse_down.constant(0); - model.project_name.frp.cancel_editing <+ frp.cancel_project_name_editing; model.project_name.frp.outside_press <+ frp.outside_press; popped_count <= frp.output.breadcrumb_select.map(|selected| (0..selected.0).collect_vec()); diff --git a/app/gui/view/graph-editor/src/component/breadcrumbs/project_name.rs b/app/gui/view/graph-editor/src/component/breadcrumbs/project_name.rs index 2f5e39beb61f..431d374ffbd5 100644 --- a/app/gui/view/graph-editor/src/component/breadcrumbs/project_name.rs +++ b/app/gui/view/graph-editor/src/component/breadcrumbs/project_name.rs @@ -19,6 +19,7 @@ use ensogl::DEPRECATED_Animation; use ensogl_component::text; use ensogl_component::text::formatting::Size as TextSize; use ensogl_hardcoded_theme::graph_editor::breadcrumbs as breadcrumbs_theme; +use parser::Parser; @@ -66,8 +67,8 @@ ensogl::define_endpoints_2! { cancel_editing (), /// Enable editing the project name field and add a cursor at the mouse position. start_editing (), - /// Commit current project name. - commit (), + /// Try committing current project name. + try_commit (), outside_press (), /// Indicates that this is the currently active breadcrumb. select (), @@ -90,6 +91,7 @@ ensogl::define_endpoints_2! { edit_mode (bool), selected (bool), is_hovered (bool), + error (String), read_only (bool), } } @@ -217,6 +219,24 @@ impl ProjectNameModel { self.commit(name); } + /// Confirm the given name as the current project name if it's valid. + fn try_commit(&self, name: impl Str) -> Result<(), String> { + let name = name.into(); + Self::validate(&name) + .map_err(|error| format!("The project couldn't be renamed. {error}"))?; + self.commit(name); + Ok(()) + } + + /// Check whether the given name is a valid project name. + fn validate(name: impl Str) -> Result<(), String> { + let parser = Parser::new(); + match parser.parse_line_ast(name).map(|ast| ast.shape().clone()) { + Ok(ast::Shape::Cons(_)) => Ok(()), + _ => Err("The project name should use the 'Upper_Snake' case.".to_owned()), + } + } + /// Confirm the given name as the current project name. fn commit>(&self, name: T) { let name = name.into(); @@ -323,11 +343,15 @@ impl ProjectName { // === Commit === - do_commit <- any(&frp.commit,&frp.outside_press).gate(&frp.output.edit_mode); - commit_text <- text_content.sample(&do_commit); - output.name <+ commit_text; - eval commit_text((text) model.commit(text)); - on_commit <- commit_text.constant(()); + try_commit <- any(&frp.try_commit, &frp.outside_press).gate(&frp.output.edit_mode); + commit_result <- try_commit.map2(&text_content, f!([model] (_, text) { + let result = model.try_commit(text); + (result.as_ref().ok().copied(), result.err()) + })); + commit_success <- commit_result.filter_map(|(ok, _)| *ok); + commit_failure <- commit_result.filter_map(|(_, error)| error.clone()); + output.name <+ text_content.sample(&commit_success); + output.error <+ commit_failure; not_selected <- frp.output.selected.map(|selected| !selected); on_deselect <- not_selected.gate(¬_selected).constant(()); @@ -337,7 +361,7 @@ impl ProjectName { // === Selection === output.selected <+ frp.select.to_true(); - set_inactive <- any(&frp.deselect,&on_commit); + set_inactive <- any(&frp.deselect, &commit_success); eval_ set_inactive ([text,model] { text.deprecated_set_focus(false); text.remove_all_cursors(); @@ -409,7 +433,7 @@ impl View for ProjectName { fn default_shortcuts() -> Vec { use shortcut::ActionType::*; [ - (Press, "!read_only", "enter", "commit"), + (Press, "!read_only", "enter", "try_commit"), (Release, "", "escape", "cancel_editing"), (DoublePress, "is_hovered & !read_only", "left-mouse-button", "start_editing"), ] diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 33a73885fd17..34dfc1fb17ef 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -373,6 +373,8 @@ ensogl::define_endpoints_2! { /// [`visualization_visible`] is updated. Please remember, that the [`position`] is not /// immediately updated, only during the Display Object hierarchy update bounding_box (BoundingBox), + /// The bounding box of the node without the visualization. + inner_bounding_box (BoundingBox), /// A set of widgets attached to a method requires metadata to be queried. The tuple /// contains the ID of the call expression the widget is attached to, and the ID of that /// call's target expression (`self` or first argument). @@ -1009,7 +1011,10 @@ impl Node { visualization_enabled_and_visible <- visualization_enabled && visualization_visible; bbox_input <- all4( &out.position,&new_size,&visualization_enabled_and_visible,visualization_size); - out.bounding_box <+ bbox_input.map(|(a,b,c,d)| bounding_box(*a,*b,*c,*d)); + out.bounding_box <+ bbox_input.map(|(a,b,c,d)| bounding_box(*a,*b,c.then(|| *d))); + + inner_bbox_input <- all2(&out.position,&new_size); + out.inner_bounding_box <+ inner_bbox_input.map(|(a,b)| bounding_box(*a,*b,None)); // === VCS Handling === @@ -1069,13 +1074,12 @@ fn visualization_offset(node_width: f32) -> Vector2 { fn bounding_box( node_position: Vector2, node_size: Vector2, - visualization_enabled_and_visible: bool, - visualization_size: Vector2, + visualization_size: Option, ) -> BoundingBox { let x_offset_to_node_center = x_offset_to_node_center(node_size.x); let node_bbox_pos = node_position + Vector2(x_offset_to_node_center, 0.0) - node_size / 2.0; let node_bbox = BoundingBox::from_position_and_size(node_bbox_pos, node_size); - if visualization_enabled_and_visible { + if let Some(visualization_size) = visualization_size { let visualization_offset = visualization_offset(node_size.x); let visualization_pos = node_position + visualization_offset; let visualization_bbox_pos = visualization_pos - visualization_size / 2.0; diff --git a/app/gui/view/graph-editor/src/component/visualization/container.rs b/app/gui/view/graph-editor/src/component/visualization/container.rs index 5d713495cca5..cfdbeb15ce49 100644 --- a/app/gui/view/graph-editor/src/component/visualization/container.rs +++ b/app/gui/view/graph-editor/src/component/visualization/container.rs @@ -315,6 +315,15 @@ impl ContainerModel { impl ContainerModel { fn set_visibility(&self, visibility: bool) { + // This is a workaround for #6600. It ensures the action bar is removed + // and receive no further mouse events. + if visibility { + self.view.add_child(&self.action_bar); + } else { + self.action_bar.unset_parent(); + } + + // Show or hide the visualization. if visibility { self.drag_root.add_child(&self.view); self.show_visualisation(); diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 843071cfb6f0..25f471c97078 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -2795,17 +2795,8 @@ fn new_graph_editor(app: &Application) -> GraphEditor { // ======================== frp::extend! { network - no_vis_selected <- out.some_visualisation_selected.on_false(); - some_vis_selected <- out.some_visualisation_selected.on_true(); - - set_navigator_false <- inputs.set_navigator_disabled.on_true(); - set_navigator_true <- inputs.set_navigator_disabled.on_false(); - - disable_navigator <- any_(&set_navigator_false,&some_vis_selected); - enable_navigator <- any_(&set_navigator_true,&no_vis_selected); - - model.navigator.frp.set_enabled <+ bool(&disable_navigator,&enable_navigator); - + navigator_disabled <- out.some_visualisation_selected.or(&inputs.set_navigator_disabled); + model.navigator.frp.set_enabled <+ navigator_disabled.not(); out.navigator_active <+ model.navigator.frp.enabled; } diff --git a/app/gui/view/graph-editor/src/selection.rs b/app/gui/view/graph-editor/src/selection.rs index 2528f25e9591..b59724cf3925 100644 --- a/app/gui/view/graph-editor/src/selection.rs +++ b/app/gui/view/graph-editor/src/selection.rs @@ -235,7 +235,7 @@ fn get_nodes_in_bounding_box(bounding_box: &BoundingBox, nodes: &Nodes) -> Vec, -} - -impl display::Object for PopupLabel { - fn display_object(&self) -> &display::object::Instance { - self.label.display_object() - } -} - -impl PopupLabel { - /// Constructor. - pub fn new(app: &Application) -> Self { - let network = frp::Network::new("PopupLabel"); - let label = Label::new(app); - label.set_opacity(0.0); - let background_layer = &app.display.default_scene.layers.panel; - let text_layer = &app.display.default_scene.layers.panel_text; - label.set_layers(background_layer, text_layer); - - let opacity_animation = Animation::new(&network); - network.store(&opacity_animation); - let delay_animation = DelayedAnimation::new(&network); - delay_animation.set_delay(0.0); - delay_animation.set_duration(0.0); - network.store(&delay_animation); - - frp::extend! { network - show <- source::(); - - eval show ([label, delay_animation](text) { - label.set_content(text); - delay_animation.reset(); - delay_animation.start(); - }); - - opacity_animation.target <+ show.constant(1.0); - opacity_animation.target <+ delay_animation.on_end.constant(0.0); - label.set_opacity <+ opacity_animation.value; - } - - Self { label, network, show, delay_animation } - } - - /// Set a delay in milliseconds after which the label will disappear. - pub fn set_delay(&self, delay: f32) { - self.delay_animation.set_delay(delay); - } -} - - - -// ============= -// === Model === -// ============= - -#[derive(Debug, Clone, CloneRef)] -struct Model { - display_object: display::object::Instance, - label: PopupLabel, -} - -impl Model { - /// Constructor. - pub fn new(app: &Application) -> Self { - let display_object = display::object::Instance::new(); - let label = PopupLabel::new(app); - label.set_delay(LABEL_VISIBILITY_DELAY_MS); - display_object.add_child(&label); - - Self { display_object, label } - } - - /// Show "Debug Mode enabled" label. - pub fn show_enabled_label(&self) { - self.label.show.emit(String::from(DEBUG_MODE_ENABLED)); - } - - /// Show "Debug Mode disabled" label. - pub fn show_disabled_label(&self) { - self.label.show.emit(String::from(DEBUG_MODE_DISABLED)); - } - - /// Return the height of the label. - pub fn label_height(&self) -> f32 { - self.label.label.size.value().y - } -} @@ -155,48 +50,39 @@ ensogl::define_endpoints! { // === View === // ============ -/// Text message on top of the screen that signals about enabling/disabling Debug Mode of Graph -/// Editor. +/// A pop-up that signals about enabling/disabling Debug Mode of Graph Editor. #[derive(Debug, Clone, CloneRef)] pub struct View { frp: Frp, - model: Model, + popup: popup::View, } impl View { /// Constructor. pub fn new(app: &Application) -> Self { let frp = Frp::new(); - let model = Model::new(app); let network = &frp.network; + let popup = popup::View::new(app); + + popup.set_delay(LABEL_VISIBILITY_DELAY_MS); frp::extend! { network - init <- source_(); - let shape = app.display.default_scene.shape(); - _eval <- all_with(shape, &init, f!([model](scene_size, _init) { - let half_height = scene_size.height / 2.0; - let label_height = model.label_height(); - let pos_y = half_height - LABEL_PADDING_TOP - label_height / 2.0; - model.display_object.set_y(pos_y); - })); - - eval_ frp.enabled(model.show_enabled_label()); - eval_ frp.disabled(model.show_disabled_label()); + eval_ frp.enabled (popup.set_label(DEBUG_MODE_ENABLED.to_string())); + eval_ frp.disabled (popup.set_label(DEBUG_MODE_DISABLED.to_string())); } - init.emit(()); - Self { frp, model } + Self { frp, popup } } - /// Get the label of the popup. - pub fn label(&self) -> &PopupLabel { - &self.model.label + /// Get the FRP node for the content of the pop-up, for testing purposes. + pub fn content_frp_node(&self) -> impl EventOutput + HasLabel { + self.popup.content_frp_node() } } impl display::Object for View { fn display_object(&self) -> &display::object::Instance { - &self.model.display_object + self.popup.display_object() } } diff --git a/app/gui/view/src/lib.rs b/app/gui/view/src/lib.rs index ab816ae54bc7..96853628a874 100644 --- a/app/gui/view/src/lib.rs +++ b/app/gui/view/src/lib.rs @@ -32,6 +32,7 @@ #[allow(clippy::option_map_unit_fn)] pub mod code_editor; pub mod debug_mode_popup; +pub mod popup; pub mod project; pub mod project_list; pub mod root; diff --git a/app/gui/view/src/popup.rs b/app/gui/view/src/popup.rs new file mode 100644 index 000000000000..a7b8ec556869 --- /dev/null +++ b/app/gui/view/src/popup.rs @@ -0,0 +1,143 @@ +//! A temporary text message on top of the screen. + +use crate::prelude::*; + +use ensogl::animation::delayed::DelayedAnimation; +use ensogl::application::Application; +use ensogl::display; +use ensogl::Animation; +use ensogl_component::label::Label; +use frp::stream::EventOutput; +use frp::HasLabel; + + + +// ================= +// === Constants === +// ================= + +const PADDING_TOP: f32 = 50.0; +const DEFAULT_DELAY_MS: f32 = 5_000.0; + + + +// ============= +// === Model === +// ============= + +/// Text label that disappears after a predefined delay. +#[derive(Debug, Clone, CloneRef)] +struct Model { + label: Label, + opacity_animation: Animation, + delay_animation: DelayedAnimation, +} + +impl Model { + /// Constructor. + fn new(app: &Application, network: &frp::Network) -> Self { + let label = Label::new(app); + label.set_opacity(0.0); + // Add the pop-up to the panel layer so its position is fixed. The default for Label is the + // tooltip layer, which moves when panning. + let scene = &app.display.default_scene; + let background_layer = &scene.layers.panel; + let text_layer = &scene.layers.panel_text; + label.set_layers(background_layer, text_layer); + + let opacity_animation = Animation::new(network); + network.store(&opacity_animation); + let delay_animation = DelayedAnimation::new(network); + delay_animation.set_delay(DEFAULT_DELAY_MS); + delay_animation.set_duration(0.0); + network.store(&delay_animation); + + Self { label, opacity_animation, delay_animation } + } + + /// Set the message. + fn set_label(&self, content: String) { + self.label.set_content(content); + self.delay_animation.reset(); + self.delay_animation.start(); + } + + /// Set the position of the label based on the height of the scene. + fn set_label_position(&self, scene_height: f32) { + let half_height = scene_height / 2.0; + let label_height = self.label.size.value().y; + let pos_y = half_height - PADDING_TOP - label_height / 2.0; + self.label.display_object().set_y(pos_y); + } + + /// Set a delay in milliseconds after which the label will disappear. + fn set_delay(&self, delay: f32) { + self.delay_animation.set_delay(delay); + } +} + + + +// =========== +// === FRP === +// =========== + +ensogl::define_endpoints! { + Input { + set_label (String), + set_delay (f32), + } + Output {} +} + + + +// ============ +// === View === +// ============ + +/// A temporary text message on top of the screen. +#[derive(Debug, Clone, CloneRef, Deref)] +pub struct View { + #[deref] + frp: Frp, + model: Model, +} + +impl View { + /// Constructor. + pub fn new(app: &Application) -> Self { + let frp = Frp::new(); + let network = &frp.network; + let model = Model::new(app, network); + + frp::extend! { network + init <- source_(); + let scene_shape = app.display.default_scene.shape(); + _eval <- all_with(scene_shape, &init, f!((scene_shape, _init) + model.set_label_position(scene_shape.height); + )); + + model.opacity_animation.target <+ frp.set_label.constant(1.0); + model.opacity_animation.target <+ model.delay_animation.on_end.constant(0.0); + model.label.set_opacity <+ model.opacity_animation.value; + + eval frp.set_label ((content) model.set_label(content.clone())); + eval frp.set_delay ((delay) model.set_delay(*delay)); + } + init.emit(()); + + Self { frp, model } + } + + /// Get the FRP node for the content of the pop-up, for testing purposes. + pub fn content_frp_node(&self) -> impl EventOutput + HasLabel { + self.frp.set_label.clone_ref() + } +} + +impl display::Object for View { + fn display_object(&self) -> &display::object::Instance { + self.model.label.display_object() + } +} diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index a29ac980da97..962978bf5413 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -13,6 +13,7 @@ use crate::graph_editor::component::node::Expression; use crate::graph_editor::component::visualization; use crate::graph_editor::GraphEditor; use crate::graph_editor::NodeId; +use crate::popup; use crate::project_list::ProjectList; use crate::searcher; @@ -155,6 +156,7 @@ struct Model { fullscreen_vis: Rc>>, project_list: Rc, debug_mode_popup: debug_mode_popup::View, + popup: popup::View, } impl Model { @@ -166,6 +168,7 @@ impl Model { let code_editor = app.new_view::(); let fullscreen_vis = default(); let debug_mode_popup = debug_mode_popup::View::new(app); + let popup = popup::View::new(app); let runs_in_web = ARGS.groups.startup.options.platform.value == "web"; let window_control_buttons = runs_in_web.as_some_from(|| { let window_control_buttons = app.new_view::(); @@ -181,6 +184,7 @@ impl Model { display_object.add_child(&code_editor); display_object.add_child(&searcher); display_object.add_child(&debug_mode_popup); + display_object.add_child(&popup); display_object.add_child(&dashboard_button); display_object.remove_child(&searcher); @@ -197,6 +201,7 @@ impl Model { fullscreen_vis, project_list, debug_mode_popup, + popup, } } @@ -392,10 +397,7 @@ impl View { // FIXME[WD]: Think how to refactor it, as it needs to be done before model, as we do not // want shader recompilation. Model uses styles already. model.set_style(theme); - // TODO[WD]: This should not be needed after the theme switching issue is implemented. - // See: https://github.com/enso-org/ide/issues/795 let input_change_delay = frp::io::timer::Timeout::new(network); - let searcher_open_delay = frp::io::timer::Timeout::new(network); frp::extend! { network init <- source_(); @@ -520,8 +522,7 @@ impl View { existing_node_edited <- graph.node_expression_edited.gate_not(&frp.is_searcher_opened); open_searcher <- existing_node_edited.map2(&node_edited_by_user, |(id, _, _), edited| edited.map_or(false, |edited| *id == edited) - ).on_true(); - searcher_open_delay.restart <+ open_searcher.constant(0); + ).on_true().debounce(); cursor_position <- existing_node_edited.map2( &node_edited_by_user, |(node_id, _, selections), edited| { @@ -533,7 +534,7 @@ impl View { ).filter_map(|pos| *pos); edited_node <- node_edited_by_user.filter_map(|node| *node); position_and_edited_node <- cursor_position.map2(&edited_node, |pos, id| (*pos, *id)); - prepare_params <- position_and_edited_node.sample(&searcher_open_delay.on_expired); + prepare_params <- position_and_edited_node.sample(&open_searcher); frp.source.searcher <+ prepare_params.map(|(pos, node_id)| { Some(SearcherParams::new_for_edited_node(*node_id, *pos)) }); @@ -548,7 +549,7 @@ impl View { update_searcher_input_on_commit <- frp.output.editing_committed.constant(()); input_change_delay.cancel <+ update_searcher_input_on_commit; update_searcher_input <- any(&input_change_delay.on_expired, &update_searcher_input_on_commit); - input_change_and_searcher <- map2(&searcher_input_change, &frp.searcher, + input_change_and_searcher <- all_with(&searcher_input_change, &frp.searcher, |c, s| (c.clone(), *s) ); updated_input <- input_change_and_searcher.sample(&update_searcher_input); @@ -667,6 +668,10 @@ impl View { model.debug_mode_popup.enabled <+ frp.enable_debug_mode; model.debug_mode_popup.disabled <+ frp.disable_debug_mode; + + // === Error Pop-up === + + model.popup.set_label <+ model.graph_editor.model.breadcrumbs.project_name_error; } init.emit(()); @@ -698,6 +703,11 @@ impl View { pub fn debug_mode_popup(&self) -> &debug_mode_popup::View { &self.model.debug_mode_popup } + + /// Pop-up + pub fn popup(&self) -> &popup::View { + &self.model.popup + } } impl display::Object for View { diff --git a/app/ide-desktop/lib/client/src/file-associations.ts b/app/ide-desktop/lib/client/src/file-associations.ts index 4a6e1c67c330..02dc0bfbfd3c 100644 --- a/app/ide-desktop/lib/client/src/file-associations.ts +++ b/app/ide-desktop/lib/client/src/file-associations.ts @@ -104,7 +104,7 @@ export function isFileOpenable(path: string): boolean { * we manually start a new instance of the application and pass the file path to it (using the * Windows-style command). */ -export function onFileOpened(event: Event, path: string): string | null { +export function onFileOpened(event: Event, path: string): string | void { logger.log(`Received 'open-file' event for path '${path}'.`) if (isFileOpenable(path)) { logger.log(`The file '${path}' is openable.`) @@ -114,6 +114,7 @@ export function onFileOpened(event: Event, path: string): string | null { if (!electron.app.isReady() && CLIENT_ARGUMENTS.length === 0) { event.preventDefault() logger.log(`Opening file '${path}'.`) + // eslint-disable-next-line no-restricted-syntax return handleOpenFile(path) } else { // We need to start another copy of the application, as the first one is already running. @@ -127,11 +128,9 @@ export function onFileOpened(event: Event, path: string): string | null { }) // Prevent parent (this) process from waiting for the child to exit. child.unref() - return null } } else { logger.log(`The file '${path}' is not openable, ignoring the 'open-file' event.`) - return null } } diff --git a/app/ide-desktop/lib/client/src/security.ts b/app/ide-desktop/lib/client/src/security.ts index f5c0d4238e76..81b36206f862 100644 --- a/app/ide-desktop/lib/client/src/security.ts +++ b/app/ide-desktop/lib/client/src/security.ts @@ -10,6 +10,9 @@ import * as electron from 'electron' /** The list of hosts that the app can access. They are required for user authentication to work. */ const TRUSTED_HOSTS = ['accounts.google.com', 'accounts.youtube.com', 'github.com'] +/** The list of hosts that the app can open external links to. */ +const TRUSTED_EXTERNAL_HOSTS = ['discord.gg'] + /** The list of URLs a new WebView can be pointed to. */ const WEBVIEW_URL_WHITELIST: string[] = [] @@ -79,7 +82,12 @@ function preventNavigation() { electron.app.on('web-contents-created', (_event, contents) => { contents.on('will-navigate', (event, navigationUrl) => { const parsedUrl = new URL(navigationUrl) - if (parsedUrl.origin !== origin && !TRUSTED_HOSTS.includes(parsedUrl.host)) { + const currentWindowUrl = electron.BrowserWindow.getFocusedWindow()?.webContents.getURL() + const parsedCurrentWindowUrl = currentWindowUrl ? new URL(currentWindowUrl) : null + if ( + parsedUrl.origin !== parsedCurrentWindowUrl?.origin && + !TRUSTED_HOSTS.includes(parsedUrl.host) + ) { event.preventDefault() console.error(`Prevented navigation to '${navigationUrl}'.`) } @@ -95,8 +103,14 @@ function preventNavigation() { function disableNewWindowsCreation() { electron.app.on('web-contents-created', (_event, contents) => { contents.setWindowOpenHandler(({ url }) => { - console.error(`Blocking new window creation request to '${url}'.`) - return { action: 'deny' } + const parsedUrl = new URL(url) + if (TRUSTED_EXTERNAL_HOSTS.includes(parsedUrl.host)) { + void electron.shell.openExternal(url) + return { action: 'deny' } + } else { + console.error(`Blocking new window creation request to '${url}'.`) + return { action: 'deny' } + } }) }) } diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index 0f71f036b00d..b3f27b68b35c 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -142,8 +142,6 @@ class Main implements AppRunner { { loader: { wasmUrl: 'pkg-opt.wasm', - jsUrl: 'pkg.js', - assetsUrl: 'dynamic-assets', }, }, inputConfig diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/setUsername.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/setUsername.tsx index 903fad09e884..7aff6c16d8c4 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/setUsername.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/setUsername.tsx @@ -3,7 +3,9 @@ import * as react from 'react' import * as auth from '../providers/auth' +import * as backendProvider from '../../providers/backend' import * as svg from '../../components/svg' + import Input from './input' import SvgIcon from './svgIcon' @@ -14,6 +16,7 @@ import SvgIcon from './svgIcon' function SetUsername() { const { setUsername: authSetUsername } = auth.useAuth() const { email } = auth.usePartialUserSession() + const { backend } = backendProvider.useBackend() const [username, setUsername] = react.useState('') @@ -32,7 +35,7 @@ function SetUsername() {
{ event.preventDefault() - await authSetUsername(username, email) + await authSetUsername(backend, username, email) }} >
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx index 1f18c902bd5a..e1adaee206f6 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx @@ -86,7 +86,11 @@ export interface PartialUserSession { interface AuthContextType { signUp: (email: string, password: string) => Promise confirmSignUp: (email: string, code: string) => Promise - setUsername: (username: string, email: string) => Promise + setUsername: ( + backend: backendProvider.AnyBackendAPI, + username: string, + email: string + ) => Promise signInWithGoogle: () => Promise signInWithGitHub: () => Promise signInWithPassword: (email: string, password: string) => Promise @@ -166,7 +170,7 @@ export function AuthProvider(props: AuthProviderProps) { const client = new http.Client(headers) const backend = new remoteBackend.RemoteBackend(client, logger) setBackend(backend) - const organization = await backend.usersMe() + const organization = await backend.usersMe().catch(() => null) let newUserSession: UserSession if (!organization) { newUserSession = { @@ -257,10 +261,14 @@ export function AuthProvider(props: AuthProviderProps) { return result.ok }) - const setUsername = async (username: string, email: string) => { - const { backend } = backendProvider.useBackend() + const setUsername = async ( + backend: backendProvider.AnyBackendAPI, + username: string, + email: string + ) => { if (backend.platform === platform.Platform.desktop) { - throw new Error('') + toast.error('You cannot set your username on the local backend.') + return false } else { try { await backend.createUser({ @@ -270,7 +278,8 @@ export function AuthProvider(props: AuthProviderProps) { navigate(app.DASHBOARD_PATH) toast.success(MESSAGES.setUsernameSuccess) return true - } catch { + } catch (e) { + toast.error('Could not set your username.') return false } } @@ -377,6 +386,22 @@ export function ProtectedLayout() { if (!session) { return + } else if (session.variant === 'partial') { + return + } else { + return + } +} + +// =========================== +// === SemiProtectedLayout === +// =========================== + +export function SemiProtectedLayout() { + const { session } = useAuth() + + if (session?.variant === 'full') { + return } else { return } diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx index d5f5fa458679..cc1f34e22ffc 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx @@ -144,6 +144,9 @@ function AppRouter(props: AppProps) { path={DASHBOARD_PATH} element={showDashboard && } /> + + {/* Semi-protected pages are visible to users currently registering. */} + }> } /> {/* Other pages are visible to unauthenticated and authenticated users. */} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx index 8964a327ff61..9759f73002ee 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx @@ -33,7 +33,7 @@ function ChangePasswordModal() { } return ( - +
{ event.stopPropagation() diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx index 02a0140f444a..4a08633b7c03 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx @@ -20,8 +20,9 @@ export interface ConfirmDeleteModalProps { function ConfirmDeleteModal(props: ConfirmDeleteModalProps) { const { assetType, name, doDelete, onSuccess } = props const { unsetModal } = modalProvider.useSetModal() + return ( - + { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx index b99273ee142a..535566efdd3d 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx @@ -31,10 +31,10 @@ function CreateForm(props: CreateFormProps) { } return ( - + { event.stopPropagation() diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index adef2ebca13f..aca32eb2426c 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -270,7 +270,8 @@ function Dashboard(props: DashboardProps) { setTab(Tab.dashboard) const ideElement = document.getElementById(IDE_ELEMENT_ID) if (ideElement) { - ideElement.hidden = true + ideElement.style.top = '-100vh' + ideElement.style.display = 'fixed' } } } @@ -370,10 +371,13 @@ function Dashboard(props: DashboardProps) { }} openIde={async () => { setTab(Tab.ide) - setProject(await backend.getProjectDetails(projectAsset.id)) + if (project?.projectId !== projectAsset.id) { + setProject(await backend.getProjectDetails(projectAsset.id)) + } const ideElement = document.getElementById(IDE_ELEMENT_ID) if (ideElement) { - ideElement.hidden = false + ideElement.style.top = '' + ideElement.style.display = 'absolute' } }} /> @@ -521,8 +525,8 @@ function Dashboard(props: DashboardProps) { const CreateForm = ASSET_TYPE_CREATE_FORM[assetType] setModal(() => ( @@ -619,11 +623,11 @@ function Dashboard(props: DashboardProps) { return `${prefix}${highestProjectIndex + 1}` } - async function handleCreateProject(templateName?: string | null) { - const projectName = getNewProjectName(templateName) + async function handleCreateProject(templateId?: string | null) { + const projectName = getNewProjectName(templateId) const body: backendModule.CreateProjectRequestBody = { projectName, - projectTemplateName: templateName?.replace(/_/g, '').toLocaleLowerCase() ?? null, + projectTemplateName: templateId ?? null, parentDirectoryId: directoryId, } const projectAsset = await backend.createProject(body) @@ -641,7 +645,7 @@ function Dashboard(props: DashboardProps) { return (
{ @@ -662,31 +666,35 @@ function Dashboard(props: DashboardProps) { setTab(Tab.ide) const ideElement = document.getElementById(IDE_ELEMENT_ID) if (ideElement) { - ideElement.hidden = false + ideElement.style.top = '' + ideElement.style.display = 'absolute' } } else { setTab(Tab.dashboard) const ideElement = document.getElementById(IDE_ELEMENT_ID) if (ideElement) { - ideElement.hidden = true + ideElement.style.top = '-100vh' + ideElement.style.display = 'fixed' } } }} setBackendPlatform={newBackendPlatform => { - setProjectAssets([]) - setDirectoryAssets([]) - setSecretAssets([]) - setFileAssets([]) - switch (newBackendPlatform) { - case platformModule.Platform.desktop: - setBackend(new localBackend.LocalBackend()) - break - case platformModule.Platform.cloud: { - const headers = new Headers() - headers.append('Authorization', `Bearer ${accessToken}`) - const client = new http.Client(headers) - setBackend(new remoteBackendModule.RemoteBackend(client, logger)) - break + if (newBackendPlatform !== backend.platform) { + setProjectAssets([]) + setDirectoryAssets([]) + setSecretAssets([]) + setFileAssets([]) + switch (newBackendPlatform) { + case platformModule.Platform.desktop: + setBackend(new localBackend.LocalBackend()) + break + case platformModule.Platform.cloud: { + const headers = new Headers() + headers.append('Authorization', `Bearer ${accessToken}`) + const client = new http.Client(headers) + setBackend(new remoteBackendModule.RemoteBackend(client, logger)) + break + } } } }} @@ -719,6 +727,7 @@ function Dashboard(props: DashboardProps) { ? 'opacity-50' : '' }`} + disabled={backend.platform === platformModule.Platform.desktop} onClick={event => { event.stopPropagation() setModal(() => ( @@ -732,8 +741,8 @@ function Dashboard(props: DashboardProps) { {svg.UPLOAD_ICON}