Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Yewdux state management #19

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = "0.19"
yew = { version = "0.21", features = ["csr"] }
serde = { version = "1.0.117", features = ["derive"] }
serde_json = "1.0"
reqwasm = "0.5.0"
gloo = "0.6.0"
yew-router = "0.16.0"
yew-router = "0.18.0"
wasm-bindgen = "0.2.81"
wasm-bindgen-futures = "0.4"
chrono = { version = "0.4", features = [ "serde" ] }
url = "2.2.2"
yewdux = "0.8.2"
yewdux = "0.10"
yew-oauth2 = "0.4.0"

[dependencies.web-sys]
Expand Down
4 changes: 2 additions & 2 deletions src/components/about.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use yew::{function_component, html};
use yew::{function_component, html, Html};

#[function_component(About)]
pub fn about() -> Html {
Expand All @@ -7,4 +7,4 @@ pub fn about() -> Html {
<p>{ "Explain the basic idea of the app here" }</p>
</div>
}
}
}
2 changes: 1 addition & 1 deletion src/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub mod welcome;
pub mod about;
pub mod organization_entry;
pub mod repository_card;
pub mod repository_list;
pub mod repository_paginator;
pub mod review_and_submit;
pub mod welcome;
55 changes: 34 additions & 21 deletions src/components/organization_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,65 @@ use web_sys::HtmlInputElement;
use yew::prelude::*;
use yewdux::prelude::*;

use crate::repository::Organization;
use crate::{organization::Organization, services::get_repos::load_organization};

// * Change the state when the text area loses focus instead of requiring a click on the
// submit button.
// * There is an `onfocusout` event that we should be able to leverage.
// * This will trigger when we tab out, but I'm thinking that might be OK since there's
// nowhere else to go in this simple interface.
// * There's an `onsubmit` event. Would that be potentially useful?
// * Allow the user to press "Enter" instead of having to click on "Submit"

/// Controlled Text Input Component
#[function_component(OrganizationEntry)]
pub fn organization_entry() -> Html {
let field_contents = use_state(|| String::from(""));
let (_, dispatch) = use_store::<Organization>();
let org_dispatch = use_dispatch::<Organization>();
let pagination_dispatch = use_dispatch::<crate::components::repository_paginator::State>();

let oninput = {
let field_contents = field_contents.clone();
Callback::from(move |input_event: InputEvent| {
field_contents.set(get_value_from_input_event(input_event));
})
};

let onclick: Callback<MouseEvent> = {
let onsubmit = {
let field_contents = field_contents.clone();
Callback::from(move |_| {
if !field_contents.is_empty() {
dispatch.set(Organization { name: Some(field_contents.deref().clone()) });
Callback::from(move |event: SubmitEvent| {
event.prevent_default();

if field_contents.is_empty() {
return;
}

org_dispatch.reduce_mut(|org| {
let name = field_contents.deref().clone().into();
org.set_name(name);
});

pagination_dispatch.reduce_mut(|state| {
state.reset();
});

load_organization(&field_contents, org_dispatch.clone());
})
};

html! {
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div class="card-body">
<div class="form-control">
<label class="label">
<span class="label-text">{ "What organization would you like to archive repositories for?" }</span>
</label>
<input type="text" placeholder="organization" class="input input-bordered" {oninput} value={ (*field_contents).clone() }/>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary" {onclick}>{ "Submit" }</button>
<form {onsubmit}>
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div class="card-body">
<div class="form-control">
<label class="label">
<span class="label-text">{ "What organization would you like to archive repositories for?" }</span>
</label>
<input type="text" placeholder="organization" class="input input-bordered" {oninput}
value={ (*field_contents).clone() }/>
</div>
<div class="form-control mt-6">
<input type="submit" class="btn btn-primary" value="Submit" />
</div>
</div>
</div>
</div>
</form>
}
}

Expand Down
98 changes: 53 additions & 45 deletions src/components/repository_card.rs
Original file line number Diff line number Diff line change
@@ -1,82 +1,90 @@
use wasm_bindgen::{UnwrapThrowExt, JsCast};
use web_sys::HtmlInputElement;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use yew::prelude::*;
use yewdux::use_store;

use crate::repository::{Repository, DesiredArchiveState};
use crate::organization::{ArchiveState, Organization, RepoId};

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
// TODO: Having to clone the repository in `RepositoryList` is annoying and it
// would be cool to turn this into a reference without making a mess of the
// memory management.
pub repository: Repository,
// If this is the None variant, then this repository should already be archived.
// If it's a Some variant, then the enclosed boolean should indicate the desired
// state for this repository.
pub desired_archive_state: Option<bool>,
pub on_checkbox_change: Callback<DesiredArchiveState>
/// The ID of the repository to display.
pub repo_id: RepoId,
/// The state to set the repository to when the checkbox is checked/unchecked.
pub toggle_state: ToggleState,
}

/// The toggle state for a repository card. Decsribes what ArchiveState to set when the checkbox is
/// checked/unchecked.
#[derive(Clone, PartialEq, Eq, Copy)]
pub struct ToggleState {
pub on: ArchiveState,
pub off: ArchiveState,
}

#[function_component(RepositoryCard)]
pub fn repository_card(props: &Props) -> Html {
let Props { repository, desired_archive_state, on_checkbox_change }
= props;

// If we pass this assertion, then the desired_archive_state.unwrap() in the HTML
// should be safe.
if desired_archive_state.is_none() {
assert!(repository.archived);
}

let onclick: Callback<MouseEvent> = {
let id = repository.id;
let on_checkbox_change = on_checkbox_change.clone();
let (org, org_dispatch) = use_store::<Organization>();
let Props {
repo_id,
toggle_state,
} = *props;

Callback::from(move |mouse_event: MouseEvent| {
let event_target = mouse_event.target().unwrap_throw();
let target: HtmlInputElement = event_target.dyn_into().unwrap_throw();
let desired_archive_state = target.checked();
let repo = match org.repositories.get(&repo_id) {
Some(repo) => repo,
None => return Default::default(),
};

web_sys::console::log_1(&format!("In ME callback desired state is {desired_archive_state}.").into());
let onclick = org_dispatch.reduce_mut_callback_with(move |state, mouse_event: MouseEvent| {
let Some(repo) = state.repositories.get_mut(&repo_id) else {
return;
};

on_checkbox_change.emit(DesiredArchiveState {
id,
desired_archive_state
});
})
};
repo.archive_state = if is_checked(&mouse_event) {
toggle_state.on
} else {
toggle_state.off
};
});

html! {
<div class="card card-compact">
<div class="card-body">
if repository.archived {
if repo.info.archived {
<p class="italic">{ "This repository is already archived" }</p>
} else {
<div class="card-actions">
<div class="form-control">
<label class="label cursor-pointer">
<input type="checkbox"
checked={ #[allow(clippy::unwrap_used)] desired_archive_state.unwrap() }
<input type="checkbox"
checked={ repo.archive_state == toggle_state.on }
class="checkbox" {onclick} />
<p class="label-text italic ml-2">{ "Archive this repository" }</p>
<p class="label-text italic ml-2">{ "Archive this repository" }</p>
</label>
</div>
</div>
}
if repository.archived {
<h2 class="card-title text-gray-300">{ &repository.name }</h2>
if repo.info.archived {
<h2 class="card-title text-gray-300">{ &repo.info.name }</h2>
} else {
<h2 class="card-title">{ &repository.name }</h2>
<h2 class="card-title">{ &repo.info.name }</h2>
}
{
repository.description.as_ref().map_or_else(
repo.info.description.as_ref().map_or_else(
|| html! { <p class="text-blue-700">{ "There was no description for this repository "}</p> },
|s| html! { <p class="text-green-700">{ s }</p> }
)
}
<p>{ format!("Last updated on {}; ", repository.updated_at.format("%Y-%m-%d")) }
{ format!("last pushed to on {}", repository.pushed_at.format("%Y-%m-%d")) }</p>
<p>{ format!("Last updated on {}; ", repo.info.updated_at.format("%Y-%m-%d")) }
{ format!("last pushed to on {}", repo.info.pushed_at.format("%Y-%m-%d")) }</p>
</div>
</div>
}
}

fn is_checked(mouse_event: &MouseEvent) -> bool {
mouse_event
.target()
.unwrap_throw()
.dyn_into::<web_sys::HtmlInputElement>()
.unwrap_throw()
.checked()
}
57 changes: 28 additions & 29 deletions src/components/repository_list.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,44 @@
use gloo::console::log;
use yew::prelude::*;
use yewdux::prelude::use_store;
use yewdux::use_store_value;

use crate::repository::{RepoId, DesiredArchiveState, DesiredStateMap};
use crate::components::repository_card::RepositoryCard;
use crate::organization::{Organization, RepoFilter};

use super::repository_card::ToggleState;

// TODO: Can we use `AttrValue` instead of `String` here?
// `AttrValue` is supposed to be more efficient
// because cloning `String`s can be expensive.
// https://yew.rs/docs/concepts/components/properties#memoryspeed-overhead-of-using-properties
#[derive(Clone, PartialEq, Properties)]
pub struct Props {
pub repo_ids: Option<Vec<RepoId>>,
pub empty_repo_list_message: String,
pub on_checkbox_change: Callback<DesiredArchiveState>
pub range: std::ops::Range<usize>,
pub filter: RepoFilter,
pub toggle_state: ToggleState,
pub empty_repo_list_message: AttrValue,
}

#[function_component(RepositoryList)]
pub fn repository_list(props: &Props) -> Html {
let Props { repo_ids,
empty_repo_list_message,
on_checkbox_change } = props;
let org = use_store_value::<Organization>();
let Props {
range,
filter,
empty_repo_list_message,
toggle_state,
} = props;

let (state_map, _) = use_store::<DesiredStateMap>();
let mut repos = org.repositories.select(range.clone(), filter).peekable();

log!(format!("We're in repo list with repo IDs {repo_ids:?}"));
log!(format!("We're in repo list with ArchiveStateMap {state_map:?}"));
let is_empty = repos.peek().is_none();
if is_empty {
return html! {
<p>{ empty_repo_list_message }</p>
};
}

#[allow(clippy::option_if_let_else)]
if let Some(repo_ids) = repo_ids {
repo_ids.iter()
.map(|repo_id: &RepoId| {
repos
.map(|repo| {
let toggle_state = *toggle_state;
html! {
<RepositoryCard repository={ state_map.get_repo(*repo_id).clone() }
desired_archive_state={ state_map.get_desired_state(*repo_id) }
{on_checkbox_change} />
<RepositoryCard repo_id={repo.info.id} {toggle_state} />
}
}).collect()
} else {
html! {
<p>{ empty_repo_list_message }</p>
}
}
})
.collect()
}
Loading
Loading