thoth-app 0.2.1

WASM APP for bibliographic data
use std::str::FromStr;
use thoth_api::language::model::LanguageCode;
use thoth_api::language::model::LanguageRelation;
use yew::html;
use yew::prelude::*;
use yew::ComponentLink;
use yewtil::fetch::Fetch;
use yewtil::fetch::FetchAction;
use yewtil::fetch::FetchState;
use yewtil::future::LinkFuture;
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::FormLanguageCodeSelect;
use crate::component::utils::FormLanguageRelationSelect;
use crate::models::language::create_language_mutation::CreateLanguageRequest;
use crate::models::language::create_language_mutation::CreateLanguageRequestBody;
use crate::models::language::create_language_mutation::PushActionCreateLanguage;
use crate::models::language::create_language_mutation::PushCreateLanguage;
use crate::models::language::create_language_mutation::Variables;
use crate::models::language::delete_language_mutation::DeleteLanguageRequest;
use crate::models::language::delete_language_mutation::DeleteLanguageRequestBody;
use crate::models::language::delete_language_mutation::PushActionDeleteLanguage;
use crate::models::language::delete_language_mutation::PushDeleteLanguage;
use crate::models::language::delete_language_mutation::Variables as DeleteVariables;
use crate::models::language::language_codes_query::FetchActionLanguageCodes;
use crate::models::language::language_codes_query::FetchLanguageCodes;
use crate::models::language::language_relations_query::FetchActionLanguageRelations;
use crate::models::language::language_relations_query::FetchLanguageRelations;
use crate::models::language::Language;
use crate::models::language::LanguageCodeValues;
use crate::models::language::LanguageRelationValues;
use crate::string::EMPTY_LANGUAGES;
use crate::string::NO;
use crate::string::REMOVE_BUTTON;
use crate::string::YES;

pub struct LanguagesFormComponent {
    props: Props,
    data: LanguagesFormData,
    new_language: Language,
    show_add_form: bool,
    fetch_language_codes: FetchLanguageCodes,
    fetch_language_relations: FetchLanguageRelations,
    push_language: PushCreateLanguage,
    delete_language: PushDeleteLanguage,
    link: ComponentLink<Self>,
    notification_bus: NotificationDispatcher,
}

#[derive(Default)]
struct LanguagesFormData {
    language_codes: Vec<LanguageCodeValues>,
    language_relations: Vec<LanguageRelationValues>,
}

pub enum Msg {
    ToggleAddFormDisplay(bool),
    SetLanguageCodesFetchState(FetchActionLanguageCodes),
    GetLanguageCodes,
    SetLanguageRelationsFetchState(FetchActionLanguageRelations),
    GetLanguageRelations,
    SetLanguagePushState(PushActionCreateLanguage),
    CreateLanguage,
    SetLanguageDeleteState(PushActionDeleteLanguage),
    DeleteLanguage(String),
    ChangeLanguageCode(LanguageCode),
    ChangeLanguageRelation(LanguageRelation),
    ChangeMainLanguage(bool),
    DoNothing,
}

#[derive(Clone, Properties, PartialEq)]
pub struct Props {
    pub languages: Option<Vec<Language>>,
    pub work_id: String,
    pub update_languages: Callback<Option<Vec<Language>>>,
}

impl Component for LanguagesFormComponent {
    type Message = Msg;
    type Properties = Props;

    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
        let data: LanguagesFormData = Default::default();
        let show_add_form = false;
        let new_language: Language = Default::default();
        let fetch_language_codes = Default::default();
        let fetch_language_relations = Default::default();
        let push_language = Default::default();
        let delete_language = Default::default();
        let notification_bus = NotificationBus::dispatcher();

        link.send_message(Msg::GetLanguageCodes);
        link.send_message(Msg::GetLanguageRelations);

        LanguagesFormComponent {
            props,
            data,
            new_language,
            show_add_form,
            fetch_language_codes,
            fetch_language_relations,
            push_language,
            delete_language,
            link,
            notification_bus,
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::ToggleAddFormDisplay(value) => {
                self.show_add_form = value;
                true
            }
            Msg::SetLanguageCodesFetchState(fetch_state) => {
                self.fetch_language_codes.apply(fetch_state);
                self.data.language_codes = match self.fetch_language_codes.as_ref().state() {
                    FetchState::NotFetching(_) => vec![],
                    FetchState::Fetching(_) => vec![],
                    FetchState::Fetched(body) => body.data.language_codes.enum_values.clone(),
                    FetchState::Failed(_, _err) => vec![],
                };
                true
            }
            Msg::GetLanguageCodes => {
                self.link.send_future(
                    self.fetch_language_codes
                        .fetch(Msg::SetLanguageCodesFetchState),
                );
                self.link
                    .send_message(Msg::SetLanguageCodesFetchState(FetchAction::Fetching));
                false
            }
            Msg::SetLanguageRelationsFetchState(fetch_state) => {
                self.fetch_language_relations.apply(fetch_state);
                self.data.language_relations = match self.fetch_language_relations.as_ref().state()
                {
                    FetchState::NotFetching(_) => vec![],
                    FetchState::Fetching(_) => vec![],
                    FetchState::Fetched(body) => body.data.language_relations.enum_values.clone(),
                    FetchState::Failed(_, _err) => vec![],
                };
                true
            }
            Msg::GetLanguageRelations => {
                self.link.send_future(
                    self.fetch_language_relations
                        .fetch(Msg::SetLanguageRelationsFetchState),
                );
                self.link
                    .send_message(Msg::SetLanguageRelationsFetchState(FetchAction::Fetching));
                false
            }
            Msg::SetLanguagePushState(fetch_state) => {
                self.push_language.apply(fetch_state);
                match self.push_language.as_ref().state() {
                    FetchState::NotFetching(_) => false,
                    FetchState::Fetching(_) => false,
                    FetchState::Fetched(body) => match &body.data.create_language {
                        Some(l) => {
                            let language = l.clone();
                            let mut languages: Vec<Language> =
                                self.props.languages.clone().unwrap_or_default();
                            languages.push(language);
                            self.new_language = Default::default();
                            self.props.update_languages.emit(Some(languages));
                            self.link.send_message(Msg::ToggleAddFormDisplay(false));
                            true
                        }
                        None => {
                            self.link.send_message(Msg::ToggleAddFormDisplay(false));
                            self.notification_bus.send(Request::NotificationBusMsg((
                                "Failed to save".to_string(),
                                NotificationStatus::Danger,
                            )));
                            false
                        }
                    },
                    FetchState::Failed(_, err) => {
                        self.link.send_message(Msg::ToggleAddFormDisplay(false));
                        self.notification_bus.send(Request::NotificationBusMsg((
                            err.to_string(),
                            NotificationStatus::Danger,
                        )));
                        false
                    }
                }
            }
            Msg::CreateLanguage => {
                let body = CreateLanguageRequestBody {
                    variables: Variables {
                        work_id: self.props.work_id.clone(),
                        language_relation: self.new_language.language_relation.clone(),
                        language_code: self.new_language.language_code.clone(),
                        main_language: self.new_language.main_language,
                    },
                    ..Default::default()
                };
                let request = CreateLanguageRequest { body };
                self.push_language = Fetch::new(request);
                self.link
                    .send_future(self.push_language.fetch(Msg::SetLanguagePushState));
                self.link
                    .send_message(Msg::SetLanguagePushState(FetchAction::Fetching));
                false
            }
            Msg::SetLanguageDeleteState(fetch_state) => {
                self.delete_language.apply(fetch_state);
                match self.delete_language.as_ref().state() {
                    FetchState::NotFetching(_) => false,
                    FetchState::Fetching(_) => false,
                    FetchState::Fetched(body) => match &body.data.delete_language {
                        Some(language) => {
                            let to_keep: Vec<Language> = self
                                .props
                                .languages
                                .clone()
                                .unwrap_or_default()
                                .into_iter()
                                .filter(|p| p.language_id != language.language_id)
                                .collect();
                            self.props.update_languages.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((
                            err.to_string(),
                            NotificationStatus::Danger,
                        )));
                        false
                    }
                }
            }
            Msg::DeleteLanguage(language_id) => {
                let body = DeleteLanguageRequestBody {
                    variables: DeleteVariables { language_id },
                    ..Default::default()
                };
                let request = DeleteLanguageRequest { body };
                self.delete_language = Fetch::new(request);
                self.link
                    .send_future(self.delete_language.fetch(Msg::SetLanguageDeleteState));
                self.link
                    .send_message(Msg::SetLanguageDeleteState(FetchAction::Fetching));
                false
            }
            Msg::ChangeLanguageRelation(val) => self.new_language.language_relation.neq_assign(val),
            Msg::ChangeLanguageCode(code) => self.new_language.language_code.neq_assign(code),
            Msg::ChangeMainLanguage(val) => self.new_language.main_language.neq_assign(val),
            Msg::DoNothing => false, // callbacks need to return a message
        }
    }

    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        self.props.neq_assign(props)
    }

    fn view(&self) -> Html {
        let languages = self.props.languages.clone().unwrap_or_default();
        let open_modal = self.link.callback(|e: MouseEvent| {
            e.prevent_default();
            Msg::ToggleAddFormDisplay(true)
        });
        let close_modal = self.link.callback(|e: MouseEvent| {
            e.prevent_default();
            Msg::ToggleAddFormDisplay(false)
        });
        html! {
            <nav class="panel">
                <p class="panel-heading">
                    { "Languages" }
                </p>
                <div class="panel-block">
                    <button
                        class="button is-link is-outlined is-success is-fullwidth"
                        onclick=open_modal
                    >
                        { "Add Language" }
                    </button>
                </div>
                <div class=self.add_form_status()>
                    <div class="modal-background" onclick=&close_modal></div>
                    <div class="modal-card">
                        <header class="modal-card-head">
                            <p class="modal-card-title">{ "New Language" }</p>
                            <button
                                class="delete"
                                aria-label="close"
                                onclick=&close_modal
                            ></button>
                        </header>
                        <section class="modal-card-body">
                            <form onsubmit=self.link.callback(|e: FocusEvent| {
                                e.prevent_default();
                                Msg::DoNothing
                            })
                            >
                                <FormLanguageCodeSelect
                                    label = "Language Code"
                                    value=&self.new_language.language_code
                                    data=&self.data.language_codes
                                    onchange=self.link.callback(|event| match event {
                                        ChangeData::Select(elem) => {
                                            let value = elem.value();
                                            Msg::ChangeLanguageCode(
                                                LanguageCode::from_str(&value).unwrap()
                                            )
                                        }
                                        _ => unreachable!(),
                                    })
                                    required = true
                                />
                                <FormLanguageRelationSelect
                                    label = "Language Relation"
                                    value=&self.new_language.language_relation
                                    data=&self.data.language_relations
                                    onchange=self.link.callback(|event| match event {
                                        ChangeData::Select(elem) => {
                                            let value = elem.value();
                                            Msg::ChangeLanguageRelation(
                                                LanguageRelation::from_str(&value).unwrap()
                                            )
                                        }
                                        _ => unreachable!(),
                                    })
                                    required = true
                                />
                                <FormBooleanSelect
                                    label = "Main"
                                    value=&self.new_language.main_language
                                    onchange=self.link.callback(|event| match event {
                                        ChangeData::Select(elem) => {
                                            let value = elem.value();
                                            let boolean = value == "true";
                                            Msg::ChangeMainLanguage(boolean)
                                        }
                                        _ => unreachable!(),
                                    })
                                    required = true
                                />
                            </form>
                        </section>
                        <footer class="modal-card-foot">
                            <button
                                class="button is-success"
                                onclick=self.link.callback(|e: MouseEvent| {
                                    e.prevent_default();
                                    Msg::CreateLanguage
                                })
                            >
                                { "Add Language" }
                            </button>
                            <button
                                class="button"
                                onclick=&close_modal
                            >
                                { "Cancel" }
                            </button>
                        </footer>
                    </div>
                </div>
                {
                    if languages.len() > 0 {
                        html!{{for languages.iter().map(|p| self.render_language(p))}}
                    } else {
                        html! {
                            <div class="notification is-warning is-light">
                                { EMPTY_LANGUAGES }
                            </div>
                        }
                    }
                }
            </nav>
        }
    }
}

impl LanguagesFormComponent {
    fn add_form_status(&self) -> String {
        match self.show_add_form {
            true => "modal is-active".to_string(),
            false => "modal".to_string(),
        }
    }

    fn render_language(&self, l: &Language) -> Html {
        // there's probably a better way to do this. We basically need to copy 3 instances
        // of contributor_id and take ownership of them so they can be passed on to
        // the callback functions
        let language_id = l.language_id.clone();
        html! {
            <div class="panel-block field is-horizontal">
                <span class="panel-icon">
                    <i class="fas fa-language" aria-hidden="true"></i>
                </span>
                <div class="field-body">
                    <div class="field" style="width: 8em;">
                        <label class="label">{ "Language Code" }</label>
                        <div class="control is-expanded">
                            {&l.language_code}
                        </div>
                    </div>

                    <div class="field" style="width: 8em;">
                        <label class="label">{ "Language Relation" }</label>
                        <div class="control is-expanded">
                            {&l.language_relation}
                        </div>
                    </div>

                    <div class="field" style="width: 8em;">
                        <label class="label">{ "Main" }</label>
                        <div class="control is-expanded">
                            {
                                match &l.main_language {
                                    true => { YES },
                                    false => { NO }
                                }
                            }
                        </div>
                    </div>

                    <div class="field">
                        <label class="label"></label>
                        <div class="control is-expanded">
                            <a
                                class="button is-danger"
                                onclick=self.link.callback(move |_| Msg::DeleteLanguage(language_id.clone()))
                            >
                                { REMOVE_BUTTON }
                            </a>
                        </div>
                    </div>
                </div>
            </div>
        }
    }
}