From 6b45498ae026727708a3f2de5aa8baedf3629ece Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Thu, 11 Jul 2024 14:09:41 +0200 Subject: [PATCH 01/40] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa8a6726..7f5619d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [538](https://github.com/thoth-pub/thoth/issues/538) - Update Project MUSE ONIX 3.0 export to reflect new specifications provided by Project MUSE. +### Fixed + - [324](https://github.com/thoth-pub/thoth/issues/324) - Make Locations updatable. + ## [[0.12.6]](https://github.com/thoth-pub/thoth/releases/tag/v0.12.6) - 2024-06-17 ### Fixed - [#513](https://github.com/thoth-pub/thoth/issues/513) - Expand DOI regex to include `+`, `[`, and `]` From cc9425fc960e5e74cefe2f4bb1791037237dd985 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Fri, 2 Aug 2024 16:04:37 +0200 Subject: [PATCH 02/40] location.rs merging example from publication.rs with new_location.rs --- thoth-app/src/component/location.rs | 468 ++++++++++++++++++ thoth-app/src/component/mod.rs | 3 +- thoth-app/src/component/new_location.rs | 427 ++++++++++++++++ thoth-app/src/component/publication.rs | 2 +- .../src/models/location/location_query.rs | 53 ++ thoth-app/src/models/location/mod.rs | 1 + 6 files changed, 952 insertions(+), 2 deletions(-) create mode 100644 thoth-app/src/component/location.rs create mode 100644 thoth-app/src/component/new_location.rs create mode 100644 thoth-app/src/models/location/location_query.rs diff --git a/thoth-app/src/component/location.rs b/thoth-app/src/component/location.rs new file mode 100644 index 00000000..39a152f9 --- /dev/null +++ b/thoth-app/src/component/location.rs @@ -0,0 +1,468 @@ +use std::str::FromStr; +use thoth_api::account::model::AccountDetails; +use thoth_api::model::location::Location; +use thoth_api::model::location::LocationPlatform; +use thoth_errors::ThothError; +use uuid::Uuid; +use yew::html; +use yew::prelude::*; +use yew_agent::Dispatched; +use yewtil::fetch::Fetch; +use yewtil::fetch::FetchAction; +use yewtil::fetch::FetchState; +use yewtil::NeqAssign; + +use crate::agent::notification_bus::NotificationBus; +use crate::agent::notification_bus::NotificationDispatcher; +use crate::agent::notification_bus::NotificationStatus; +use crate::agent::notification_bus::Request; +use crate::component::utils::FormBooleanSelect; +use crate::component::utils::FormLocationPlatformSelect; +use crate::component::utils::FormUrlInput; +use crate::component::utils::Loader; +use crate::models::location::delete_location_mutation::DeleteLocationRequest; +use crate::models::location::delete_location_mutation::DeleteLocationRequestBody; +use crate::models::location::delete_location_mutation::PushActionDeleteLocation; +use crate::models::location::delete_location_mutation::PushDeleteLocation; +use crate::models::location::delete_location_mutation::Variables as DeleteVariables; +use crate::models::location::location_query::FetchActionLocation; +use crate::models::location::location_query::FetchLocation; +use crate::models::location::location_query::LocationRequest; +use crate::models::location::location_query::LocationRequestBody; +use crate::models::location::location_query::Variables; +use crate::models::location::location_platforms_query::FetchActionLocationPlatforms; +use crate::models::location::location_platforms_query::FetchLocationPlatforms; +use crate::models::location::LocationPlatformValues; +use crate::route::AdminRoute; +use crate::string::EDIT_BUTTON; +use crate::string::YES; +use crate::string::NO; +use crate::string::REMOVE_BUTTON; + +use super::ToElementValue; +use super::ToOption; + +pub struct LocationComponent { + location: Location, + fetch_location: FetchLocation, + delete_location: PushDeleteLocation, + show_modal_form: bool, + location_under_edit: Option, + fetch_location_platforms: FetchLocationPlatforms, + notification_bus: NotificationDispatcher, +} +pub struct LocationsFormComponent { + data: LocationsFormData, + new_location: Location, + show_add_form: bool, + fetch_location_platforms: FetchLocationPlatforms, + delete_location: PushDeleteLocation, + notification_bus: NotificationDispatcher, +} + +#[derive(Default)] +struct LocationsFormData { + location_platforms: Vec, +} + +#[allow(clippy::large_enum_variant)] +pub enum Msg { + ToggleModalFormDisplay(bool), + UpdateLocation(Location), + SetLocationFetchState(FetchActionLocation), + SetLocationPlatformsFetchState(FetchActionLocationPlatforms), + GetLocation, + GetLocationPlatforms, + SetLocationDeleteState(PushActionDeleteLocation), + DeleteLocation(Uuid), + ChangeLandingPage(String), + ChangeFullTextUrl(String), + ChangeLocationPlatform(LocationPlatform), + ChangeCanonical(bool), +} + +#[derive(Clone, Properties, PartialEq)] +pub struct Props { + pub locations: Option>, + pub publication_id: Uuid, + pub update_locations: Callback>>, + pub current_user: AccountDetails, +} + +impl Component for LocationComponent { + type Message = Msg; + type Properties = Props; + + fn create(ctx: &Context) -> Self { + let fetch_location: FetchLocation = Default::default(); + let delete_location = Default::default(); + let show_modal_form = false; + let fetch_location_platforms = Default::default(); + let location_under_edit = Default::default(); + let notification_bus = NotificationBus::dispatcher(); + let location: Location = Default::default(); + + ctx.link().send_message(Msg::GetLocation); + ctx.link().send_message(Msg::GetLocationPlatforms); + + LocationComponent { + location, + fetch_location, + delete_location, + show_modal_form, + location_under_edit, + fetch_location_platforms, + notification_bus, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + let mut data: LocationsFormData = Default::default(); + match msg { + Msg::ToggleModalFormDisplay(show_form) => { + self.show_modal_form = show_form; + // Opening the modal form from this form always means + // we are about to edit the current location + self.location_under_edit = match self.show_modal_form { + true => Some(Location { + location_id: self.location.location_id, + publication_id: self.location.publication_id, + created_at: Default::default(), + updated_at: self.location.updated_at.clone(), + landing_page: self.location.landing_page.clone(), + full_text_url: self.location.full_text_url.clone(), + location_platform: self.location.location_platform.clone(), + canonical: self.location.canonical, + }), + false => None, + }; + true + } + Msg::UpdateLocation(l) => { + if l.location_id == self.location.location_id + && l.publication_id == self.location.publication_id + { + self.notification_bus.send(Request::NotificationBusMsg(( + format!( + "Saved {}", + &l.location_id + .to_string() + ), + NotificationStatus::Success, + ))); + // Child form has updated the current location - replace its values + self.location.updated_at = l.updated_at; + self.location.landing_page = l.landing_page; + self.location.full_text_url = l.full_text_url; + self.location.location_platform = l.location_platform; + self.location.canonical = l.canonical; + } else { + // This should not be possible: the updated location returned from the + // database does not match the locally-stored location data. + // Refreshing the page will reload the local data from the database. + self.notification_bus.send(Request::NotificationBusMsg(( + "Changes were saved but display failed to update. Refresh your browser to view current data.".to_string(), + NotificationStatus::Warning, + ))); + } + // Close child form + ctx.link().send_message(Msg::ToggleModalFormDisplay(false)); + true + } + Msg::SetLocationFetchState(fetch_state) => { + self.fetch_location.apply(fetch_state); + match self.fetch_location.as_ref().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => { + // self.location = match &body.data.location { + // Some(c) => c.to_owned(), + // None => Default::default(), + // }; + // If user doesn't have permission to edit this object, redirect to dashboard + // TODO: implement this. But how can I get to this from + // location? I don't have publisher_id, just publication_id + // if let Some(publishers) = + // ctx.props().current_user.resource_access.restricted_to() + // { + // if !publishers.contains( + // &self + // .publication + // .work + // .imprint + // .publisher + // .publisher_id + // .to_string(), + // ) { + // ctx.link().history().unwrap().push(AdminRoute::Dashboard); + // } + // } + true + } + FetchState::Failed(_, _err) => false, + } + } + Msg::GetLocation => { + let body = LocationRequestBody { + variables: Variables { + location_id: Some(ctx.props().publication_id), + }, + ..Default::default() + }; + let request = LocationRequest { body }; + self.fetch_location = Fetch::new(request); + + ctx.link() + .send_future(self.fetch_location.fetch(Msg::SetLocationFetchState)); + ctx.link() + .send_message(Msg::SetLocationFetchState(FetchAction::Fetching)); + false + } + Msg::SetLocationPlatformsFetchState(fetch_state) => { + self.fetch_location_platforms.apply(fetch_state); + data.location_platforms = match self.fetch_location_platforms.as_ref().state() + { + FetchState::NotFetching(_) => vec![], + FetchState::Fetching(_) => vec![], + FetchState::Fetched(body) => body.data.location_platforms.enum_values.clone(), + FetchState::Failed(_, _err) => vec![], + }; + true + } + Msg::GetLocationPlatforms => { + ctx.link().send_future( + self.fetch_location_platforms + .fetch(Msg::SetLocationPlatformsFetchState), + ); + ctx.link() + .send_message(Msg::SetLocationPlatformsFetchState(FetchAction::Fetching)); + false + } + Msg::SetLocationDeleteState(fetch_state) => { + self.delete_location.apply(fetch_state); + match self.delete_location.as_ref().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => match &body.data.delete_location { + Some(location) => { + let to_keep: Vec = ctx + .props() + .locations + .clone() + .unwrap_or_default() + .into_iter() + .filter(|l| l.location_id != location.location_id) + .collect(); + ctx.props().update_locations.emit(Some(to_keep)); + true + } + None => { + self.notification_bus.send(Request::NotificationBusMsg(( + "Failed to save".to_string(), + NotificationStatus::Danger, + ))); + false + } + }, + FetchState::Failed(_, err) => { + self.notification_bus.send(Request::NotificationBusMsg(( + ThothError::from(err).to_string(), + NotificationStatus::Danger, + ))); + false + } + } + } + Msg::DeleteLocation(location_id) => { + let body = DeleteLocationRequestBody { + variables: DeleteVariables { location_id }, + ..Default::default() + }; + let request = DeleteLocationRequest { body }; + self.delete_location = Fetch::new(request); + ctx.link() + .send_future(self.delete_location.fetch(Msg::SetLocationDeleteState)); + ctx.link() + .send_message(Msg::SetLocationDeleteState(FetchAction::Fetching)); + false + } + Msg::ChangeLandingPage(val) => self + .location + .landing_page + .neq_assign(val.to_opt_string()), + Msg::ChangeFullTextUrl(val) => self + .location + .full_text_url + .neq_assign(val.to_opt_string()), + Msg::ChangeLocationPlatform(code) => { + self.location.location_platform.neq_assign(code) + } + Msg::ChangeCanonical(val) => self.location.canonical.neq_assign(val), + } + } + + fn view(&self, ctx: &Context) -> Html { + let locations = ctx.props().locations.clone().unwrap_or_default(); + let open_modal = ctx.link().callback(|e: MouseEvent| { + e.prevent_default(); + Msg::ToggleModalFormDisplay(true) + }); + let close_modal = ctx.link().callback(|e: MouseEvent| { + e.prevent_default(); + Msg::ToggleModalFormDisplay(false) + }); + match self.fetch_location.as_ref().state() { + FetchState::NotFetching(_) => html! {}, + FetchState::Fetching(_) => html! {}, + FetchState::Fetched(_body) => { + html! { + <> + +
+ + +
+ + } + } + FetchState::Failed(_, err) => html! { + { ThothError::from(err).to_string() } + }, + } + } +} + +impl LocationsFormComponent { + fn edit_form_status(&self) -> String { + match self.show_edit_form { + true => "modal is-active".to_string(), + false => "modal".to_string(), + } + } + + fn render_location(&self, ctx: &Context, l: &Location) -> Html { + let location_id = l.location_id; + let mut delete_callback = Some( + ctx.link() + .callback(move |_| Msg::DeleteLocation(location_id)), + ); + let mut delete_deactivated = false; + // If the location is canonical and other (non-canonical) locations exist, prevent it from + // being deleted by deactivating the delete button and unsetting its callback attribute + if l.canonical && ctx.props().locations.as_ref().unwrap_or(&vec![]).len() > 1 { + delete_callback = None; + delete_deactivated = true; + } + html! { +
+ + + +
+
+ +
+ {&l.landing_page.clone().unwrap_or_default()} +
+
+
+ +
+ {&l.full_text_url.clone().unwrap_or_default()} +
+
+
+ +
+ {&l.location_platform} +
+
+
+ +
+ { + match l.canonical { + true => { YES }, + false => { NO } + } + } +
+
+ + +
+
+ } + } +} diff --git a/thoth-app/src/component/mod.rs b/thoth-app/src/component/mod.rs index dd2b77a0..a1bbfea8 100644 --- a/thoth-app/src/component/mod.rs +++ b/thoth-app/src/component/mod.rs @@ -466,7 +466,7 @@ pub mod institution_select; pub mod institutions; pub mod issues_form; pub mod languages_form; -pub mod locations_form; +pub mod location; pub mod login; pub mod menu; pub mod navbar; @@ -474,6 +474,7 @@ pub mod new_chapter; pub mod new_contributor; pub mod new_imprint; pub mod new_institution; +pub mod new_location; pub mod new_publisher; pub mod new_series; pub mod new_work; diff --git a/thoth-app/src/component/new_location.rs b/thoth-app/src/component/new_location.rs new file mode 100644 index 00000000..8ee8c1f8 --- /dev/null +++ b/thoth-app/src/component/new_location.rs @@ -0,0 +1,427 @@ +use std::str::FromStr; +use thoth_api::model::location::Location; +use thoth_api::model::location::LocationPlatform; +use thoth_errors::ThothError; +use uuid::Uuid; +use yew::html; +use yew::prelude::*; +use yew_agent::Dispatched; +use yewtil::fetch::Fetch; +use yewtil::fetch::FetchAction; +use yewtil::fetch::FetchState; +use yewtil::NeqAssign; + +use crate::agent::notification_bus::NotificationBus; +use crate::agent::notification_bus::NotificationDispatcher; +use crate::agent::notification_bus::NotificationStatus; +use crate::agent::notification_bus::Request; +use crate::component::utils::FormBooleanSelect; +use crate::component::utils::FormLocationPlatformSelect; +use crate::component::utils::FormUrlInput; +use crate::models::location::create_location_mutation::CreateLocationRequest; +use crate::models::location::create_location_mutation::CreateLocationRequestBody; +use crate::models::location::create_location_mutation::PushActionCreateLocation; +use crate::models::location::create_location_mutation::PushCreateLocation; +use crate::models::location::create_location_mutation::Variables; +use crate::models::location::delete_location_mutation::DeleteLocationRequest; +use crate::models::location::delete_location_mutation::DeleteLocationRequestBody; +use crate::models::location::delete_location_mutation::PushActionDeleteLocation; +use crate::models::location::delete_location_mutation::PushDeleteLocation; +use crate::models::location::delete_location_mutation::Variables as DeleteVariables; +use crate::models::location::location_platforms_query::FetchActionLocationPlatforms; +use crate::models::location::location_platforms_query::FetchLocationPlatforms; +use crate::models::location::LocationPlatformValues; +use crate::string::CANCEL_BUTTON; +use crate::string::EMPTY_LOCATIONS; +use crate::string::NO; +use crate::string::REMOVE_BUTTON; +use crate::string::YES; + +use super::ToElementValue; +use super::ToOption; + +pub struct LocationsFormComponent { + data: LocationsFormData, + new_location: Location, + show_add_form: bool, + fetch_location_platforms: FetchLocationPlatforms, + push_location: PushCreateLocation, + delete_location: PushDeleteLocation, + notification_bus: NotificationDispatcher, +} + +#[derive(Default)] +struct LocationsFormData { + location_platforms: Vec, +} + +pub enum Msg { + ToggleAddFormDisplay(bool), + SetLocationPlatformsFetchState(FetchActionLocationPlatforms), + GetLocationPlatforms, + SetLocationPushState(PushActionCreateLocation), + CreateLocation, + SetLocationDeleteState(PushActionDeleteLocation), + DeleteLocation(Uuid), + ChangeLandingPage(String), + ChangeFullTextUrl(String), + ChangeLocationPlatform(LocationPlatform), + ChangeCanonical(bool), +} + +#[derive(Clone, Properties, PartialEq)] +pub struct Props { + pub locations: Option>, + pub publication_id: Uuid, + pub update_locations: Callback>>, +} + +impl Component for LocationsFormComponent { + type Message = Msg; + type Properties = Props; + + fn create(ctx: &Context) -> Self { + let data: LocationsFormData = Default::default(); + let show_add_form = false; + // The first location needs to be canonical = true (as it will be + // the only location); subsequent locations need to be canonical = false + let new_location = Location { + canonical: ctx.props().locations.as_ref().unwrap_or(&vec![]).is_empty(), + ..Default::default() + }; + let fetch_location_platforms = Default::default(); + let push_location = Default::default(); + let delete_location = Default::default(); + let notification_bus = NotificationBus::dispatcher(); + + ctx.link().send_message(Msg::GetLocationPlatforms); + + LocationsFormComponent { + data, + new_location, + show_add_form, + fetch_location_platforms, + push_location, + delete_location, + notification_bus, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ToggleAddFormDisplay(value) => { + self.show_add_form = value; + true + } + Msg::SetLocationPlatformsFetchState(fetch_state) => { + self.fetch_location_platforms.apply(fetch_state); + self.data.location_platforms = match self.fetch_location_platforms.as_ref().state() + { + FetchState::NotFetching(_) => vec![], + FetchState::Fetching(_) => vec![], + FetchState::Fetched(body) => body.data.location_platforms.enum_values.clone(), + FetchState::Failed(_, _err) => vec![], + }; + true + } + Msg::GetLocationPlatforms => { + ctx.link().send_future( + self.fetch_location_platforms + .fetch(Msg::SetLocationPlatformsFetchState), + ); + ctx.link() + .send_message(Msg::SetLocationPlatformsFetchState(FetchAction::Fetching)); + false + } + Msg::SetLocationPushState(fetch_state) => { + self.push_location.apply(fetch_state); + match self.push_location.as_ref().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => match &body.data.create_location { + Some(l) => { + let location = l.clone(); + let mut locations: Vec = + ctx.props().locations.clone().unwrap_or_default(); + locations.push(location); + ctx.props().update_locations.emit(Some(locations)); + ctx.link().send_message(Msg::ToggleAddFormDisplay(false)); + true + } + None => { + ctx.link().send_message(Msg::ToggleAddFormDisplay(false)); + self.notification_bus.send(Request::NotificationBusMsg(( + "Failed to save".to_string(), + NotificationStatus::Danger, + ))); + false + } + }, + FetchState::Failed(_, err) => { + ctx.link().send_message(Msg::ToggleAddFormDisplay(false)); + self.notification_bus.send(Request::NotificationBusMsg(( + ThothError::from(err).to_string(), + NotificationStatus::Danger, + ))); + false + } + } + } + Msg::CreateLocation => { + let body = CreateLocationRequestBody { + variables: Variables { + publication_id: ctx.props().publication_id, + landing_page: self.new_location.landing_page.clone(), + full_text_url: self.new_location.full_text_url.clone(), + location_platform: self.new_location.location_platform.clone(), + canonical: self.new_location.canonical, + }, + ..Default::default() + }; + let request = CreateLocationRequest { body }; + self.push_location = Fetch::new(request); + ctx.link() + .send_future(self.push_location.fetch(Msg::SetLocationPushState)); + ctx.link() + .send_message(Msg::SetLocationPushState(FetchAction::Fetching)); + false + } + Msg::SetLocationDeleteState(fetch_state) => { + self.delete_location.apply(fetch_state); + match self.delete_location.as_ref().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => match &body.data.delete_location { + Some(location) => { + let to_keep: Vec = ctx + .props() + .locations + .clone() + .unwrap_or_default() + .into_iter() + .filter(|l| l.location_id != location.location_id) + .collect(); + ctx.props().update_locations.emit(Some(to_keep)); + true + } + None => { + self.notification_bus.send(Request::NotificationBusMsg(( + "Failed to save".to_string(), + NotificationStatus::Danger, + ))); + false + } + }, + FetchState::Failed(_, err) => { + self.notification_bus.send(Request::NotificationBusMsg(( + ThothError::from(err).to_string(), + NotificationStatus::Danger, + ))); + false + } + } + } + Msg::DeleteLocation(location_id) => { + let body = DeleteLocationRequestBody { + variables: DeleteVariables { location_id }, + ..Default::default() + }; + let request = DeleteLocationRequest { body }; + self.delete_location = Fetch::new(request); + ctx.link() + .send_future(self.delete_location.fetch(Msg::SetLocationDeleteState)); + ctx.link() + .send_message(Msg::SetLocationDeleteState(FetchAction::Fetching)); + false + } + Msg::ChangeLandingPage(val) => self + .new_location + .landing_page + .neq_assign(val.to_opt_string()), + Msg::ChangeFullTextUrl(val) => self + .new_location + .full_text_url + .neq_assign(val.to_opt_string()), + Msg::ChangeLocationPlatform(code) => { + self.new_location.location_platform.neq_assign(code) + } + Msg::ChangeCanonical(val) => self.new_location.canonical.neq_assign(val), + } + } + + fn view(&self, ctx: &Context) -> Html { + let locations = ctx.props().locations.clone().unwrap_or_default(); + let open_modal = ctx.link().callback(|e: MouseEvent| { + e.prevent_default(); + Msg::ToggleAddFormDisplay(true) + }); + let close_modal = ctx.link().callback(|e: MouseEvent| { + e.prevent_default(); + Msg::ToggleAddFormDisplay(false) + }); + html! { + + } + } +} + +impl LocationsFormComponent { + fn add_form_status(&self) -> String { + match self.show_add_form { + true => "modal is-active".to_string(), + false => "modal".to_string(), + } + } + + fn render_location(&self, ctx: &Context, l: &Location) -> Html { + let location_id = l.location_id; + let mut delete_callback = Some( + ctx.link() + .callback(move |_| Msg::DeleteLocation(location_id)), + ); + let mut delete_deactivated = false; + // If the location is canonical and other (non-canonical) locations exist, prevent it from + // being deleted by deactivating the delete button and unsetting its callback attribute + if l.canonical && ctx.props().locations.as_ref().unwrap_or(&vec![]).len() > 1 { + delete_callback = None; + delete_deactivated = true; + } + html! { +
+ + + +
+
+ +
+ {&l.landing_page.clone().unwrap_or_default()} +
+
+
+ +
+ {&l.full_text_url.clone().unwrap_or_default()} +
+
+
+ +
+ {&l.location_platform} +
+
+
+ +
+ { + match l.canonical { + true => { YES }, + false => { NO } + } + } +
+
+ + +
+
+ } + } +} diff --git a/thoth-app/src/component/publication.rs b/thoth-app/src/component/publication.rs index b36d5d77..918c753f 100644 --- a/thoth-app/src/component/publication.rs +++ b/thoth-app/src/component/publication.rs @@ -22,7 +22,7 @@ use crate::agent::notification_bus::NotificationDispatcher; use crate::agent::notification_bus::NotificationStatus; use crate::agent::notification_bus::Request; use crate::component::delete_dialogue::ConfirmDeleteComponent; -use crate::component::locations_form::LocationsFormComponent; +use crate::component::new_location::LocationsFormComponent; use crate::component::prices_form::PricesFormComponent; use crate::component::publication_modal::PublicationModalComponent; use crate::component::utils::Loader; diff --git a/thoth-app/src/models/location/location_query.rs b/thoth-app/src/models/location/location_query.rs new file mode 100644 index 00000000..1b4c1a8d --- /dev/null +++ b/thoth-app/src/models/location/location_query.rs @@ -0,0 +1,53 @@ +use serde::Deserialize; +use serde::Serialize; +use thoth_api::model::location::Location; +use uuid::Uuid; + +// locationId: Uuid! +// publicationId: Uuid! +// landingPage: String +// fullTextUrl: String +// locationPlatform: LocationPlatform! +// canonical: Boolean! +// createdAt: Timestamp! +// updatedAt: Timestamp! +// publication: Publication! + +pub const LOCATION_QUERY: &str = " + query LocationQuery($locationId: Uuid!) { + location(locationId: $locationId) { + locationId + publicationId + landingPage + fullTextUrl + locationPlatform + canonical + createdAt + updatedAt + publication + + } + } +"; + +graphql_query_builder! { + LocationRequest, + LocationRequestBody, + Variables, + LOCATION_QUERY, + LocationResponseBody, + LocationResponseData, + FetchLocation, + FetchActionLocation +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Variables { + pub location_id: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct LocationResponseData { + pub publication: Option, +} diff --git a/thoth-app/src/models/location/mod.rs b/thoth-app/src/models/location/mod.rs index bf91fc5a..38c38082 100644 --- a/thoth-app/src/models/location/mod.rs +++ b/thoth-app/src/models/location/mod.rs @@ -17,3 +17,4 @@ pub struct LocationPlatformValues { pub mod create_location_mutation; pub mod delete_location_mutation; pub mod location_platforms_query; +pub mod location_query; From dc93225a2fe31625b7957f7085f7fe5c042e1cc0 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Mon, 5 Aug 2024 11:52:06 +0200 Subject: [PATCH 03/40] Moved changes to location.rs to be discarded to untracked file --- thoth-app/src/component/location.rs | 468 ---------------------------- thoth-app/src/component/mod.rs | 2 +- 2 files changed, 1 insertion(+), 469 deletions(-) delete mode 100644 thoth-app/src/component/location.rs diff --git a/thoth-app/src/component/location.rs b/thoth-app/src/component/location.rs deleted file mode 100644 index 39a152f9..00000000 --- a/thoth-app/src/component/location.rs +++ /dev/null @@ -1,468 +0,0 @@ -use std::str::FromStr; -use thoth_api::account::model::AccountDetails; -use thoth_api::model::location::Location; -use thoth_api::model::location::LocationPlatform; -use thoth_errors::ThothError; -use uuid::Uuid; -use yew::html; -use yew::prelude::*; -use yew_agent::Dispatched; -use yewtil::fetch::Fetch; -use yewtil::fetch::FetchAction; -use yewtil::fetch::FetchState; -use yewtil::NeqAssign; - -use crate::agent::notification_bus::NotificationBus; -use crate::agent::notification_bus::NotificationDispatcher; -use crate::agent::notification_bus::NotificationStatus; -use crate::agent::notification_bus::Request; -use crate::component::utils::FormBooleanSelect; -use crate::component::utils::FormLocationPlatformSelect; -use crate::component::utils::FormUrlInput; -use crate::component::utils::Loader; -use crate::models::location::delete_location_mutation::DeleteLocationRequest; -use crate::models::location::delete_location_mutation::DeleteLocationRequestBody; -use crate::models::location::delete_location_mutation::PushActionDeleteLocation; -use crate::models::location::delete_location_mutation::PushDeleteLocation; -use crate::models::location::delete_location_mutation::Variables as DeleteVariables; -use crate::models::location::location_query::FetchActionLocation; -use crate::models::location::location_query::FetchLocation; -use crate::models::location::location_query::LocationRequest; -use crate::models::location::location_query::LocationRequestBody; -use crate::models::location::location_query::Variables; -use crate::models::location::location_platforms_query::FetchActionLocationPlatforms; -use crate::models::location::location_platforms_query::FetchLocationPlatforms; -use crate::models::location::LocationPlatformValues; -use crate::route::AdminRoute; -use crate::string::EDIT_BUTTON; -use crate::string::YES; -use crate::string::NO; -use crate::string::REMOVE_BUTTON; - -use super::ToElementValue; -use super::ToOption; - -pub struct LocationComponent { - location: Location, - fetch_location: FetchLocation, - delete_location: PushDeleteLocation, - show_modal_form: bool, - location_under_edit: Option, - fetch_location_platforms: FetchLocationPlatforms, - notification_bus: NotificationDispatcher, -} -pub struct LocationsFormComponent { - data: LocationsFormData, - new_location: Location, - show_add_form: bool, - fetch_location_platforms: FetchLocationPlatforms, - delete_location: PushDeleteLocation, - notification_bus: NotificationDispatcher, -} - -#[derive(Default)] -struct LocationsFormData { - location_platforms: Vec, -} - -#[allow(clippy::large_enum_variant)] -pub enum Msg { - ToggleModalFormDisplay(bool), - UpdateLocation(Location), - SetLocationFetchState(FetchActionLocation), - SetLocationPlatformsFetchState(FetchActionLocationPlatforms), - GetLocation, - GetLocationPlatforms, - SetLocationDeleteState(PushActionDeleteLocation), - DeleteLocation(Uuid), - ChangeLandingPage(String), - ChangeFullTextUrl(String), - ChangeLocationPlatform(LocationPlatform), - ChangeCanonical(bool), -} - -#[derive(Clone, Properties, PartialEq)] -pub struct Props { - pub locations: Option>, - pub publication_id: Uuid, - pub update_locations: Callback>>, - pub current_user: AccountDetails, -} - -impl Component for LocationComponent { - type Message = Msg; - type Properties = Props; - - fn create(ctx: &Context) -> Self { - let fetch_location: FetchLocation = Default::default(); - let delete_location = Default::default(); - let show_modal_form = false; - let fetch_location_platforms = Default::default(); - let location_under_edit = Default::default(); - let notification_bus = NotificationBus::dispatcher(); - let location: Location = Default::default(); - - ctx.link().send_message(Msg::GetLocation); - ctx.link().send_message(Msg::GetLocationPlatforms); - - LocationComponent { - location, - fetch_location, - delete_location, - show_modal_form, - location_under_edit, - fetch_location_platforms, - notification_bus, - } - } - - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let mut data: LocationsFormData = Default::default(); - match msg { - Msg::ToggleModalFormDisplay(show_form) => { - self.show_modal_form = show_form; - // Opening the modal form from this form always means - // we are about to edit the current location - self.location_under_edit = match self.show_modal_form { - true => Some(Location { - location_id: self.location.location_id, - publication_id: self.location.publication_id, - created_at: Default::default(), - updated_at: self.location.updated_at.clone(), - landing_page: self.location.landing_page.clone(), - full_text_url: self.location.full_text_url.clone(), - location_platform: self.location.location_platform.clone(), - canonical: self.location.canonical, - }), - false => None, - }; - true - } - Msg::UpdateLocation(l) => { - if l.location_id == self.location.location_id - && l.publication_id == self.location.publication_id - { - self.notification_bus.send(Request::NotificationBusMsg(( - format!( - "Saved {}", - &l.location_id - .to_string() - ), - NotificationStatus::Success, - ))); - // Child form has updated the current location - replace its values - self.location.updated_at = l.updated_at; - self.location.landing_page = l.landing_page; - self.location.full_text_url = l.full_text_url; - self.location.location_platform = l.location_platform; - self.location.canonical = l.canonical; - } else { - // This should not be possible: the updated location returned from the - // database does not match the locally-stored location data. - // Refreshing the page will reload the local data from the database. - self.notification_bus.send(Request::NotificationBusMsg(( - "Changes were saved but display failed to update. Refresh your browser to view current data.".to_string(), - NotificationStatus::Warning, - ))); - } - // Close child form - ctx.link().send_message(Msg::ToggleModalFormDisplay(false)); - true - } - Msg::SetLocationFetchState(fetch_state) => { - self.fetch_location.apply(fetch_state); - match self.fetch_location.as_ref().state() { - FetchState::NotFetching(_) => false, - FetchState::Fetching(_) => false, - FetchState::Fetched(body) => { - // self.location = match &body.data.location { - // Some(c) => c.to_owned(), - // None => Default::default(), - // }; - // If user doesn't have permission to edit this object, redirect to dashboard - // TODO: implement this. But how can I get to this from - // location? I don't have publisher_id, just publication_id - // if let Some(publishers) = - // ctx.props().current_user.resource_access.restricted_to() - // { - // if !publishers.contains( - // &self - // .publication - // .work - // .imprint - // .publisher - // .publisher_id - // .to_string(), - // ) { - // ctx.link().history().unwrap().push(AdminRoute::Dashboard); - // } - // } - true - } - FetchState::Failed(_, _err) => false, - } - } - Msg::GetLocation => { - let body = LocationRequestBody { - variables: Variables { - location_id: Some(ctx.props().publication_id), - }, - ..Default::default() - }; - let request = LocationRequest { body }; - self.fetch_location = Fetch::new(request); - - ctx.link() - .send_future(self.fetch_location.fetch(Msg::SetLocationFetchState)); - ctx.link() - .send_message(Msg::SetLocationFetchState(FetchAction::Fetching)); - false - } - Msg::SetLocationPlatformsFetchState(fetch_state) => { - self.fetch_location_platforms.apply(fetch_state); - data.location_platforms = match self.fetch_location_platforms.as_ref().state() - { - FetchState::NotFetching(_) => vec![], - FetchState::Fetching(_) => vec![], - FetchState::Fetched(body) => body.data.location_platforms.enum_values.clone(), - FetchState::Failed(_, _err) => vec![], - }; - true - } - Msg::GetLocationPlatforms => { - ctx.link().send_future( - self.fetch_location_platforms - .fetch(Msg::SetLocationPlatformsFetchState), - ); - ctx.link() - .send_message(Msg::SetLocationPlatformsFetchState(FetchAction::Fetching)); - false - } - Msg::SetLocationDeleteState(fetch_state) => { - self.delete_location.apply(fetch_state); - match self.delete_location.as_ref().state() { - FetchState::NotFetching(_) => false, - FetchState::Fetching(_) => false, - FetchState::Fetched(body) => match &body.data.delete_location { - Some(location) => { - let to_keep: Vec = ctx - .props() - .locations - .clone() - .unwrap_or_default() - .into_iter() - .filter(|l| l.location_id != location.location_id) - .collect(); - ctx.props().update_locations.emit(Some(to_keep)); - true - } - None => { - self.notification_bus.send(Request::NotificationBusMsg(( - "Failed to save".to_string(), - NotificationStatus::Danger, - ))); - false - } - }, - FetchState::Failed(_, err) => { - self.notification_bus.send(Request::NotificationBusMsg(( - ThothError::from(err).to_string(), - NotificationStatus::Danger, - ))); - false - } - } - } - Msg::DeleteLocation(location_id) => { - let body = DeleteLocationRequestBody { - variables: DeleteVariables { location_id }, - ..Default::default() - }; - let request = DeleteLocationRequest { body }; - self.delete_location = Fetch::new(request); - ctx.link() - .send_future(self.delete_location.fetch(Msg::SetLocationDeleteState)); - ctx.link() - .send_message(Msg::SetLocationDeleteState(FetchAction::Fetching)); - false - } - Msg::ChangeLandingPage(val) => self - .location - .landing_page - .neq_assign(val.to_opt_string()), - Msg::ChangeFullTextUrl(val) => self - .location - .full_text_url - .neq_assign(val.to_opt_string()), - Msg::ChangeLocationPlatform(code) => { - self.location.location_platform.neq_assign(code) - } - Msg::ChangeCanonical(val) => self.location.canonical.neq_assign(val), - } - } - - fn view(&self, ctx: &Context) -> Html { - let locations = ctx.props().locations.clone().unwrap_or_default(); - let open_modal = ctx.link().callback(|e: MouseEvent| { - e.prevent_default(); - Msg::ToggleModalFormDisplay(true) - }); - let close_modal = ctx.link().callback(|e: MouseEvent| { - e.prevent_default(); - Msg::ToggleModalFormDisplay(false) - }); - match self.fetch_location.as_ref().state() { - FetchState::NotFetching(_) => html! {}, - FetchState::Fetching(_) => html! {}, - FetchState::Fetched(_body) => { - html! { - <> - -
- - -
- - } - } - FetchState::Failed(_, err) => html! { - { ThothError::from(err).to_string() } - }, - } - } -} - -impl LocationsFormComponent { - fn edit_form_status(&self) -> String { - match self.show_edit_form { - true => "modal is-active".to_string(), - false => "modal".to_string(), - } - } - - fn render_location(&self, ctx: &Context, l: &Location) -> Html { - let location_id = l.location_id; - let mut delete_callback = Some( - ctx.link() - .callback(move |_| Msg::DeleteLocation(location_id)), - ); - let mut delete_deactivated = false; - // If the location is canonical and other (non-canonical) locations exist, prevent it from - // being deleted by deactivating the delete button and unsetting its callback attribute - if l.canonical && ctx.props().locations.as_ref().unwrap_or(&vec![]).len() > 1 { - delete_callback = None; - delete_deactivated = true; - } - html! { -
- - - -
-
- -
- {&l.landing_page.clone().unwrap_or_default()} -
-
-
- -
- {&l.full_text_url.clone().unwrap_or_default()} -
-
-
- -
- {&l.location_platform} -
-
-
- -
- { - match l.canonical { - true => { YES }, - false => { NO } - } - } -
-
- - -
-
- } - } -} diff --git a/thoth-app/src/component/mod.rs b/thoth-app/src/component/mod.rs index a1bbfea8..eba80e81 100644 --- a/thoth-app/src/component/mod.rs +++ b/thoth-app/src/component/mod.rs @@ -466,7 +466,7 @@ pub mod institution_select; pub mod institutions; pub mod issues_form; pub mod languages_form; -pub mod location; +// pub mod location; pub mod login; pub mod menu; pub mod navbar; From c0e5e30b6a690c36d55ee3718e07381e88376a56 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Mon, 5 Aug 2024 14:20:25 +0200 Subject: [PATCH 04/40] Added logic for edit locations, following model of contributions_form.rs --- thoth-app/src/component/locations_form.rs | 179 ++++++-- thoth-app/src/component/mod.rs | 2 +- thoth-app/src/component/new_location.rs | 427 ------------------ thoth-app/src/component/publication.rs | 2 +- thoth-app/src/models/location/mod.rs | 1 + .../location/update_location_mutation.rs | 62 +++ 6 files changed, 205 insertions(+), 468 deletions(-) delete mode 100644 thoth-app/src/component/new_location.rs create mode 100644 thoth-app/src/models/location/update_location_mutation.rs diff --git a/thoth-app/src/component/locations_form.rs b/thoth-app/src/component/locations_form.rs index 8ee8c1f8..935e3c3d 100644 --- a/thoth-app/src/component/locations_form.rs +++ b/thoth-app/src/component/locations_form.rs @@ -22,16 +22,22 @@ use crate::models::location::create_location_mutation::CreateLocationRequest; use crate::models::location::create_location_mutation::CreateLocationRequestBody; use crate::models::location::create_location_mutation::PushActionCreateLocation; use crate::models::location::create_location_mutation::PushCreateLocation; -use crate::models::location::create_location_mutation::Variables; +use crate::models::location::create_location_mutation::Variables as CreateVariables; use crate::models::location::delete_location_mutation::DeleteLocationRequest; use crate::models::location::delete_location_mutation::DeleteLocationRequestBody; use crate::models::location::delete_location_mutation::PushActionDeleteLocation; use crate::models::location::delete_location_mutation::PushDeleteLocation; use crate::models::location::delete_location_mutation::Variables as DeleteVariables; +use crate::models::location::update_location_mutation::PushActionUpdateLocation; +use crate::models::location::update_location_mutation::PushUpdateLocation; +use crate::models::location::update_location_mutation::UpdateLocationRequest; +use crate::models::location::update_location_mutation::UpdateLocationRequestBody; +use crate::models::location::update_location_mutation::Variables as UpdateVariables; use crate::models::location::location_platforms_query::FetchActionLocationPlatforms; use crate::models::location::location_platforms_query::FetchLocationPlatforms; use crate::models::location::LocationPlatformValues; use crate::string::CANCEL_BUTTON; +use crate::string::EDIT_BUTTON; use crate::string::EMPTY_LOCATIONS; use crate::string::NO; use crate::string::REMOVE_BUTTON; @@ -42,11 +48,13 @@ use super::ToOption; pub struct LocationsFormComponent { data: LocationsFormData, - new_location: Location, - show_add_form: bool, + location: Location, + show_modal_form: bool, + in_edit_mode: bool, fetch_location_platforms: FetchLocationPlatforms, - push_location: PushCreateLocation, + create_location: PushCreateLocation, delete_location: PushDeleteLocation, + update_location: PushUpdateLocation, notification_bus: NotificationDispatcher, } @@ -56,13 +64,15 @@ struct LocationsFormData { } pub enum Msg { - ToggleAddFormDisplay(bool), + ToggleModalFormDisplay(bool, Option), SetLocationPlatformsFetchState(FetchActionLocationPlatforms), GetLocationPlatforms, - SetLocationPushState(PushActionCreateLocation), + SetLocationCreateState(PushActionCreateLocation), CreateLocation, SetLocationDeleteState(PushActionDeleteLocation), DeleteLocation(Uuid), + SetLocationUpdateState(PushActionUpdateLocation), + UpdateLocation, ChangeLandingPage(String), ChangeFullTextUrl(String), ChangeLocationPlatform(LocationPlatform), @@ -82,35 +92,46 @@ impl Component for LocationsFormComponent { fn create(ctx: &Context) -> Self { let data: LocationsFormData = Default::default(); - let show_add_form = false; + let show_modal_form = false; + let in_edit_mode = false; // The first location needs to be canonical = true (as it will be // the only location); subsequent locations need to be canonical = false - let new_location = Location { + let location = Location { canonical: ctx.props().locations.as_ref().unwrap_or(&vec![]).is_empty(), ..Default::default() }; let fetch_location_platforms = Default::default(); - let push_location = Default::default(); + let create_location = Default::default(); let delete_location = Default::default(); + let update_location = Default::default(); let notification_bus = NotificationBus::dispatcher(); ctx.link().send_message(Msg::GetLocationPlatforms); LocationsFormComponent { data, - new_location, - show_add_form, + location, + show_modal_form, + in_edit_mode, fetch_location_platforms, - push_location, + create_location, delete_location, + update_location, notification_bus, } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - Msg::ToggleAddFormDisplay(value) => { - self.show_add_form = value; + Msg::ToggleModalFormDisplay(show_form, l) => { + self.show_modal_form = show_form; + self.in_edit_mode = l.is_some(); + if show_form { + if let Some(location) = l { + // Editing existing location: load its current values. + self.location = location; + } + } true } Msg::SetLocationPlatformsFetchState(fetch_state) => { @@ -133,9 +154,9 @@ impl Component for LocationsFormComponent { .send_message(Msg::SetLocationPlatformsFetchState(FetchAction::Fetching)); false } - Msg::SetLocationPushState(fetch_state) => { - self.push_location.apply(fetch_state); - match self.push_location.as_ref().state() { + Msg::SetLocationCreateState(fetch_state) => { + self.create_location.apply(fetch_state); + match self.create_location.as_ref().state() { FetchState::NotFetching(_) => false, FetchState::Fetching(_) => false, FetchState::Fetched(body) => match &body.data.create_location { @@ -145,11 +166,11 @@ impl Component for LocationsFormComponent { ctx.props().locations.clone().unwrap_or_default(); locations.push(location); ctx.props().update_locations.emit(Some(locations)); - ctx.link().send_message(Msg::ToggleAddFormDisplay(false)); + ctx.link().send_message(Msg::ToggleModalFormDisplay(false, None)); true } None => { - ctx.link().send_message(Msg::ToggleAddFormDisplay(false)); + ctx.link().send_message(Msg::ToggleModalFormDisplay(false, None)); self.notification_bus.send(Request::NotificationBusMsg(( "Failed to save".to_string(), NotificationStatus::Danger, @@ -158,7 +179,7 @@ impl Component for LocationsFormComponent { } }, FetchState::Failed(_, err) => { - ctx.link().send_message(Msg::ToggleAddFormDisplay(false)); + ctx.link().send_message(Msg::ToggleModalFormDisplay(false, None)); self.notification_bus.send(Request::NotificationBusMsg(( ThothError::from(err).to_string(), NotificationStatus::Danger, @@ -169,21 +190,92 @@ impl Component for LocationsFormComponent { } Msg::CreateLocation => { let body = CreateLocationRequestBody { - variables: Variables { + variables: CreateVariables { publication_id: ctx.props().publication_id, - landing_page: self.new_location.landing_page.clone(), - full_text_url: self.new_location.full_text_url.clone(), - location_platform: self.new_location.location_platform.clone(), - canonical: self.new_location.canonical, + landing_page: self.location.landing_page.clone(), + full_text_url: self.location.full_text_url.clone(), + location_platform: self.location.location_platform.clone(), + canonical: self.location.canonical, }, ..Default::default() }; let request = CreateLocationRequest { body }; - self.push_location = Fetch::new(request); + self.create_location = Fetch::new(request); ctx.link() - .send_future(self.push_location.fetch(Msg::SetLocationPushState)); + .send_future(self.create_location.fetch(Msg::SetLocationCreateState)); ctx.link() - .send_message(Msg::SetLocationPushState(FetchAction::Fetching)); + .send_message(Msg::SetLocationCreateState(FetchAction::Fetching)); + false + } + Msg::SetLocationUpdateState(fetch_state) => { + self.update_location.apply(fetch_state); + match self.update_location.as_ref().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => match &body.data.update_location { + Some(l) => { + let mut locations: Vec = + ctx.props().locations.clone().unwrap_or_default(); + if let Some(location) = locations + .iter_mut() + .find(|ln| ln.location_id == l.location_id) + { + *location = l.clone(); + ctx.props().update_locations.emit(Some(locations)); + } else { + // This should not be possible: the updated location returned from the + // database does not match any of the locally-stored location data. + // Refreshing the page will reload the local data from the database. + self.notification_bus.send(Request::NotificationBusMsg(( + "Changes were saved but display failed to update. Refresh your browser to view current data.".to_string(), + NotificationStatus::Warning, + ))); + } + ctx.link() + .send_message(Msg::ToggleModalFormDisplay(false, None)); + true + } + None => { + ctx.link() + .send_message(Msg::ToggleModalFormDisplay(false, None)); + self.notification_bus.send(Request::NotificationBusMsg(( + "Failed to save".to_string(), + NotificationStatus::Danger, + ))); + false + } + }, + FetchState::Failed(_, err) => { + ctx.link() + .send_message(Msg::ToggleModalFormDisplay(false, None)); + self.notification_bus.send(Request::NotificationBusMsg(( + ThothError::from(err).to_string(), + NotificationStatus::Danger, + ))); + false + } + } + } + Msg::UpdateLocation => { + let body = UpdateLocationRequestBody { + variables: UpdateVariables { + location_id: self.location.location_id, + publication_id: self.location.publication_id, + landing_page: self.location.landing_page.clone(), + full_text_url: self.location.full_text_url.clone(), + location_platform: self.location.location_platform.clone(), + canonical: self.location.canonical, + }, + ..Default::default() + }; + let request = UpdateLocationRequest { body }; + self.update_location = Fetch::new(request); + ctx.link().send_future( + self.update_location + .fetch(Msg::SetLocationUpdateState), + ); + ctx.link() + .send_message(Msg::SetLocationUpdateState(FetchAction::Fetching)); false } Msg::SetLocationDeleteState(fetch_state) => { @@ -235,17 +327,17 @@ impl Component for LocationsFormComponent { false } Msg::ChangeLandingPage(val) => self - .new_location + .location .landing_page .neq_assign(val.to_opt_string()), Msg::ChangeFullTextUrl(val) => self - .new_location + .location .full_text_url .neq_assign(val.to_opt_string()), Msg::ChangeLocationPlatform(code) => { - self.new_location.location_platform.neq_assign(code) + self.location.location_platform.neq_assign(code) } - Msg::ChangeCanonical(val) => self.new_location.canonical.neq_assign(val), + Msg::ChangeCanonical(val) => self.location.canonical.neq_assign(val), } } @@ -253,11 +345,11 @@ impl Component for LocationsFormComponent { let locations = ctx.props().locations.clone().unwrap_or_default(); let open_modal = ctx.link().callback(|e: MouseEvent| { e.prevent_default(); - Msg::ToggleAddFormDisplay(true) + Msg::ToggleModalFormDisplay(true, None) }); let close_modal = ctx.link().callback(|e: MouseEvent| { e.prevent_default(); - Msg::ToggleAddFormDisplay(false) + Msg::ToggleModalFormDisplay(false, None) }); html! {