Add invite users page and handle invitation page
This commit is contained in:
parent
aa56c2fe52
commit
a16ddbcf2e
@ -11,12 +11,14 @@
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .styledInput,
|
||||
.issueDetails > .content > .left > .styledTextArea {
|
||||
margin: 18px 0 0 -8px;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .styledInput > input,
|
||||
.issueDetails > .content > .left > .styledTextArea > textarea {
|
||||
padding: 7px 7px 8px;
|
||||
line-height: 1.28;
|
||||
@ -27,7 +29,8 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .styledTextArea > textarea:not(:focus) {
|
||||
.issueDetails > .content > .left > .styledTextArea > textarea:not(:focus),
|
||||
.issueDetails > .content > .left > .styledInput > input:not(:focus) {
|
||||
background: #fff;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 0 1px transparent;
|
||||
|
@ -67,6 +67,10 @@ nav#sidebar .linkItem {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
nav#sidebar .linkItem > a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav#sidebar .linkItem.notAllowed, nav#sidebar .linkItem.notAllowed > a {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ i.styledIcon.top {
|
||||
}
|
||||
|
||||
i.styledIcon:before {
|
||||
font-family: "jira";
|
||||
font-family: 'IcoFont';
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
@ -26,141 +26,145 @@ i.styledIcon:before {
|
||||
}
|
||||
|
||||
i.styledIcon.stopwatch:before {
|
||||
content: "\e914";
|
||||
content: "\ec89";
|
||||
}
|
||||
|
||||
i.styledIcon.bug:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\eec7";
|
||||
}
|
||||
|
||||
i.styledIcon.task:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\ef27";
|
||||
}
|
||||
|
||||
i.styledIcon.story:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\ef2d";
|
||||
}
|
||||
|
||||
i.styledIcon.arrowDown:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
|
||||
i.styledIcon.arrowLeftCircle:before {
|
||||
content: "\e917";
|
||||
content: "\ea92";
|
||||
}
|
||||
|
||||
i.styledIcon.arrowUp:before {
|
||||
content: "\e90b";
|
||||
content: "\ea95";
|
||||
}
|
||||
|
||||
i.styledIcon.arrowLeftCircle:before {
|
||||
font-family: "jira";
|
||||
content: "\e917";
|
||||
}
|
||||
|
||||
i.styledIcon.chevronDown:before {
|
||||
font-family: "jira";
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
i.styledIcon.chevronLeft:before {
|
||||
font-family: "jira";
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
i.styledIcon.chevronRight:before {
|
||||
font-family: "jira";
|
||||
content: "\e902";
|
||||
}
|
||||
|
||||
i.styledIcon.chevronUp:before {
|
||||
font-family: "jira";
|
||||
content: "\e903";
|
||||
}
|
||||
|
||||
i.styledIcon.board:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\ead0";
|
||||
}
|
||||
|
||||
i.styledIcon.help:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\efca";
|
||||
}
|
||||
|
||||
i.styledIcon.link:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\ef71";
|
||||
|
||||
}
|
||||
|
||||
i.styledIcon.menu:before {
|
||||
font-family: "jira";
|
||||
content: "\e916";
|
||||
}
|
||||
|
||||
i.styledIcon.more:before {
|
||||
font-family: "jira";
|
||||
content: "\e90e";
|
||||
}
|
||||
|
||||
i.styledIcon.attach:before {
|
||||
font-family: "jira";
|
||||
content: "\e90d";
|
||||
}
|
||||
|
||||
i.styledIcon.plus:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\efc2";
|
||||
}
|
||||
|
||||
i.styledIcon.search:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\ec82";
|
||||
}
|
||||
|
||||
i.styledIcon.issues:before {
|
||||
content: "\e908";
|
||||
content: "\ed19";
|
||||
}
|
||||
|
||||
i.styledIcon.settings:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\efe2";
|
||||
}
|
||||
|
||||
i.styledIcon.close:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\eee4";
|
||||
}
|
||||
|
||||
i.styledIcon.feedback:before {
|
||||
font-family: "jira";
|
||||
content: "\e918";
|
||||
}
|
||||
|
||||
i.styledIcon.trash:before {
|
||||
font-family: 'IcoFont';
|
||||
content: "\ee09";
|
||||
content: "\eebb";
|
||||
}
|
||||
|
||||
i.styledIcon.github:before {
|
||||
content: "\e915";
|
||||
content: "\ed3e";
|
||||
}
|
||||
|
||||
i.styledIcon.shipping:before {
|
||||
content: "\e91c";
|
||||
content: "\efbe";
|
||||
}
|
||||
|
||||
i.styledIcon.component:before {
|
||||
content: "\e91a";
|
||||
content: "\eef8";
|
||||
}
|
||||
|
||||
i.styledIcon.reports:before {
|
||||
content: "\e91b";
|
||||
content: "\eeaf";
|
||||
}
|
||||
|
||||
i.styledIcon.page:before {
|
||||
content: "\e919";
|
||||
content: "\efb2";
|
||||
}
|
||||
|
||||
i.styledIcon.calendar:before {
|
||||
content: "\e91d";
|
||||
content: "\ec45";
|
||||
}
|
||||
|
||||
i.styledIcon.cop:before {
|
||||
content: "\ebb4";
|
||||
}
|
||||
|
||||
i.styledIcon.arrowLeft:before {
|
||||
font-family: "jira";
|
||||
content: "\e91e";
|
||||
}
|
||||
|
||||
i.styledIcon.arrowRight:before {
|
||||
font-family: "jira";
|
||||
content: "\e91f";
|
||||
}
|
||||
|
59
jirs-client/src/invite.rs
Normal file
59
jirs-client/src/invite.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::InviteFieldId;
|
||||
|
||||
use crate::model::{InvitePage, Model, Page, PageContent};
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::{outer_layout, ToNode};
|
||||
use crate::validations::is_token;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ChangePage(Page::Project) => {
|
||||
model.page_content = PageContent::Invite(InvitePage::default());
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::InputChanged(FieldId::Invite(InviteFieldId::Token), text) => {
|
||||
page.token_touched = true;
|
||||
page.token = text.clone();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let token = StyledInput::build(FieldId::Invite(InviteFieldId::Token))
|
||||
.valid(!page.token_touched || is_token(page.token.as_str()))
|
||||
.build()
|
||||
.into_node();
|
||||
let token_field = StyledField::build()
|
||||
.input(token)
|
||||
.label("Your invite token")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let form = StyledForm::build()
|
||||
.heading("Welcome in JIRS")
|
||||
.add_field(token_field)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
outer_layout(model, "invite", vec![form])
|
||||
}
|
@ -10,6 +10,7 @@ use crate::shared::styled_editor::Mode as TabMode;
|
||||
use crate::shared::styled_select::StyledSelectChange;
|
||||
|
||||
mod api;
|
||||
mod invite;
|
||||
mod modal;
|
||||
mod model;
|
||||
mod project;
|
||||
@ -17,6 +18,7 @@ mod project_settings;
|
||||
mod shared;
|
||||
mod sign_in;
|
||||
mod sign_up;
|
||||
mod users;
|
||||
mod validations;
|
||||
mod ws;
|
||||
|
||||
@ -33,6 +35,8 @@ pub enum EditIssueModalSection {
|
||||
pub enum FieldId {
|
||||
SignIn(SignInFieldId),
|
||||
SignUp(SignUpFieldId),
|
||||
Invite(InviteFieldId),
|
||||
Users(UsersFieldId),
|
||||
// issue
|
||||
AddIssueModal(IssueFieldId),
|
||||
EditIssueModal(EditIssueModalSection),
|
||||
@ -114,6 +118,14 @@ impl std::fmt::Display for FieldId {
|
||||
SignUpFieldId::Username => f.write_str("signUp-email"),
|
||||
SignUpFieldId::Email => f.write_str("signUp-username"),
|
||||
},
|
||||
FieldId::Invite(sub) => match sub {
|
||||
InviteFieldId::Token => f.write_str("invite-token"),
|
||||
},
|
||||
FieldId::Users(sub) => match sub {
|
||||
UsersFieldId::Username => f.write_str("users-username"),
|
||||
UsersFieldId::Email => f.write_str("users-email"),
|
||||
UsersFieldId::UserRole => f.write_str("users-userRole"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -221,6 +233,8 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
Page::ProjectSettings => project_settings::update(msg, model, orders),
|
||||
Page::SignIn => sign_in::update(msg, model, orders),
|
||||
Page::SignUp => sign_up::update(msg, model, orders),
|
||||
Page::Invite => invite::update(msg, model, orders),
|
||||
Page::Users => users::update(msg, model, orders),
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
// debug!(model);
|
||||
@ -234,6 +248,8 @@ fn view(model: &model::Model) -> Node<Msg> {
|
||||
Page::ProjectSettings => project_settings::view(model),
|
||||
Page::SignIn => sign_in::view(model),
|
||||
Page::SignUp => sign_up::view(model),
|
||||
Page::Invite => invite::view(model),
|
||||
Page::Users => users::view(model),
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,6 +268,8 @@ fn routes(url: Url) -> Option<Msg> {
|
||||
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
|
||||
"login" => Some(Msg::ChangePage(model::Page::SignIn)),
|
||||
"register" => Some(Msg::ChangePage(model::Page::SignUp)),
|
||||
"invite" => Some(Msg::ChangePage(model::Page::Invite)),
|
||||
"users" => Some(Msg::ChangePage(model::Page::Users)),
|
||||
_ => Some(Msg::ChangePage(model::Page::Project)),
|
||||
}
|
||||
}
|
||||
|
@ -153,6 +153,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
|
||||
let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description))
|
||||
.height(110)
|
||||
.add_class("textarea")
|
||||
.build()
|
||||
.into_node();
|
||||
let description_field = StyledField::build()
|
||||
|
@ -372,13 +372,14 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let title = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
let title = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Title,
|
||||
)))
|
||||
.add_input_class("issueSummary")
|
||||
.add_wrapper_class("issueSummary")
|
||||
.add_wrapper_class("textarea")
|
||||
.value(payload.title.as_str())
|
||||
.add_class("textarea")
|
||||
.max_height(48)
|
||||
.height(0)
|
||||
.valid(payload.title.len() >= 3)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
|
@ -142,6 +142,8 @@ pub enum Page {
|
||||
ProjectSettings,
|
||||
SignIn,
|
||||
SignUp,
|
||||
Invite,
|
||||
Users,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
@ -153,6 +155,8 @@ impl Page {
|
||||
Page::ProjectSettings => "/project-settings".to_string(),
|
||||
Page::SignIn => "/login".to_string(),
|
||||
Page::SignUp => "/register".to_string(),
|
||||
Page::Invite => "/invite".to_string(),
|
||||
Page::Users => "/users".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,6 +188,12 @@ pub struct ProjectPage {
|
||||
pub dirty_issues: Vec<IssueId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InvitePage {
|
||||
pub token: String,
|
||||
pub token_touched: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectSettingsPage {
|
||||
pub payload: UpdateProjectPayload,
|
||||
@ -242,12 +252,42 @@ pub struct SignUpPage {
|
||||
pub email_touched: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UsersPage {
|
||||
pub name: String,
|
||||
pub name_touched: bool,
|
||||
pub email: String,
|
||||
pub email_touched: bool,
|
||||
pub user_role: UserRole,
|
||||
|
||||
pub user_role_state: StyledSelectState,
|
||||
pub pending_invitations: Vec<String>,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
impl Default for UsersPage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "".to_string(),
|
||||
name_touched: false,
|
||||
email: "".to_string(),
|
||||
email_touched: false,
|
||||
user_role: Default::default(),
|
||||
user_role_state: StyledSelectState::new(FieldId::Users(UsersFieldId::UserRole)),
|
||||
pending_invitations: vec![],
|
||||
error: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PageContent {
|
||||
SignIn(SignInPage),
|
||||
SignUp(SignUpPage),
|
||||
Project(ProjectPage),
|
||||
ProjectSettings(ProjectSettingsPage),
|
||||
Invite(InvitePage),
|
||||
Users(UsersPage),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -1,5 +1,7 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::UserRole;
|
||||
|
||||
use crate::model::{Model, Page};
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::{divider, ToNode};
|
||||
@ -29,6 +31,23 @@ pub fn render(model: &Model) -> Node<Msg> {
|
||||
],
|
||||
],
|
||||
};
|
||||
let mut links = vec![
|
||||
sidebar_link_item(model, "Releases", Icon::Shipping, None),
|
||||
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
|
||||
sidebar_link_item(model, "Pages", Icon::Page, None),
|
||||
sidebar_link_item(model, "Reports", Icon::Reports, None),
|
||||
sidebar_link_item(model, "Components", Icon::Component, None),
|
||||
];
|
||||
|
||||
if model.user.as_ref().map(|u| u.user_role).unwrap_or_default() > UserRole::User {
|
||||
links.push(sidebar_link_item(
|
||||
model,
|
||||
"Users",
|
||||
Icon::Cop,
|
||||
Some(Page::Users),
|
||||
));
|
||||
}
|
||||
|
||||
nav![
|
||||
id!["sidebar"],
|
||||
ul![
|
||||
@ -41,11 +60,7 @@ pub fn render(model: &Model) -> Node<Msg> {
|
||||
Some(Page::ProjectSettings)
|
||||
),
|
||||
li![divider()],
|
||||
sidebar_link_item(model, "Releases", Icon::Shipping, None),
|
||||
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
|
||||
sidebar_link_item(model, "Pages", Icon::Page, None),
|
||||
sidebar_link_item(model, "Reports", Icon::Reports, None),
|
||||
sidebar_link_item(model, "Components", Icon::Component, None),
|
||||
links,
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ pub enum Icon {
|
||||
Calendar,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Cop,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
@ -90,6 +91,7 @@ impl std::fmt::Display for Icon {
|
||||
Icon::Calendar => "calendar",
|
||||
Icon::ArrowLeft => "arrowLeft",
|
||||
Icon::ArrowRight => "arrowRight",
|
||||
Icon::Cop => "cop",
|
||||
};
|
||||
f.write_str(code)
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ pub struct StyledInput {
|
||||
valid: bool,
|
||||
value: Option<String>,
|
||||
input_type: Option<String>,
|
||||
input_class_list: Vec<String>,
|
||||
wrapper_class_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl StyledInput {
|
||||
@ -21,6 +23,8 @@ impl StyledInput {
|
||||
valid: None,
|
||||
value: None,
|
||||
input_type: None,
|
||||
input_class_list: vec![],
|
||||
wrapper_class_list: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,6 +36,8 @@ pub struct StyledInputBuilder {
|
||||
valid: Option<bool>,
|
||||
value: Option<String>,
|
||||
input_type: Option<String>,
|
||||
input_class_list: Vec<String>,
|
||||
wrapper_class_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl StyledInputBuilder {
|
||||
@ -53,6 +59,22 @@ impl StyledInputBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_input_class<S>(mut self, name: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.input_class_list.push(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_wrapper_class<S>(mut self, name: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.wrapper_class_list.push(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledInput {
|
||||
StyledInput {
|
||||
id: self.id,
|
||||
@ -60,6 +82,8 @@ impl StyledInputBuilder {
|
||||
valid: self.valid.unwrap_or_default(),
|
||||
value: self.value,
|
||||
input_type: self.input_type,
|
||||
input_class_list: self.input_class_list,
|
||||
wrapper_class_list: self.wrapper_class_list,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,14 +101,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
valid,
|
||||
value,
|
||||
input_type,
|
||||
mut input_class_list,
|
||||
mut wrapper_class_list,
|
||||
} = values;
|
||||
|
||||
let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)];
|
||||
wrapper_class_list.push("styledInput".to_string());
|
||||
wrapper_class_list.push(format!("{}", id));
|
||||
if !valid {
|
||||
wrapper_class_list.push("invalid".to_string());
|
||||
}
|
||||
|
||||
let mut input_class_list = vec!["inputElement".to_string()];
|
||||
input_class_list.push("inputElement".to_string());
|
||||
if icon.is_some() {
|
||||
input_class_list.push("withIcon".to_string());
|
||||
}
|
||||
@ -111,6 +138,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
icon,
|
||||
seed::input![
|
||||
attrs![
|
||||
At::Id => format!("{}", id),
|
||||
At::Class => input_class_list.join(" "),
|
||||
At::Value => value.unwrap_or_default(),
|
||||
At::Type => input_type.unwrap_or_else(|| "text".to_string()),
|
||||
|
@ -140,6 +140,11 @@ impl StyledSelectBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_state(self, state: &StyledSelectState) -> Self {
|
||||
self.opened(state.opened)
|
||||
.text_filter(state.text_filter.as_str())
|
||||
}
|
||||
|
||||
pub fn dropdown_width(mut self, dropdown_width: usize) -> Self {
|
||||
self.dropdown_width = Some(dropdown_width);
|
||||
self
|
||||
|
@ -249,3 +249,14 @@ impl ToStyledSelectChild for jirs_data::ProjectCategory {
|
||||
.value(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStyledSelectChild for jirs_data::UserRole {
|
||||
fn to_select_child(&self) -> StyledSelectChildBuilder {
|
||||
let name = self.to_string();
|
||||
|
||||
StyledSelectChild::build()
|
||||
.add_class(name.as_str())
|
||||
.text(name)
|
||||
.value(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
119
jirs-client/src/users.rs
Normal file
119
jirs-client/src/users.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::UserRole;
|
||||
use jirs_data::{ToVec, UsersFieldId};
|
||||
|
||||
use crate::model::{Model, Page, PageContent, UsersPage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_select::*;
|
||||
use crate::shared::styled_select_child::ToStyledSelectChild;
|
||||
use crate::shared::{inner_layout, ToNode};
|
||||
use crate::validations::is_email;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ChangePage(Page::Users) => {
|
||||
model.page_content = PageContent::Users(UsersPage::default());
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::Users(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
page.user_role_state.update(&msg, orders);
|
||||
|
||||
match msg {
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::Users(UsersFieldId::UserRole),
|
||||
StyledSelectChange::Changed(role),
|
||||
) => {
|
||||
page.user_role = role.into();
|
||||
}
|
||||
Msg::InputChanged(FieldId::Users(UsersFieldId::Username), name) => {
|
||||
page.name = name;
|
||||
page.name_touched = true;
|
||||
}
|
||||
Msg::InputChanged(FieldId::Users(UsersFieldId::Email), email) => {
|
||||
page.email = email;
|
||||
page.email_touched = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::Users(page) => page,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let name = StyledInput::build(FieldId::Users(UsersFieldId::Username))
|
||||
.valid(!page.name_touched || page.name.len() >= 3)
|
||||
.value(page.name.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
let name_field = StyledField::build()
|
||||
.input(name)
|
||||
.label("Name")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let email = StyledInput::build(FieldId::Users(UsersFieldId::Email))
|
||||
.valid(!page.email_touched || is_email(page.email.as_str()))
|
||||
.value(page.email.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
let email_field = StyledField::build()
|
||||
.input(email)
|
||||
.label("E-Mail")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let user_role = StyledSelect::build(FieldId::Users(UsersFieldId::UserRole))
|
||||
.name("user_role")
|
||||
.valid(true)
|
||||
.normal()
|
||||
.with_state(&page.user_role_state)
|
||||
.selected(vec![page.user_role.to_select_child()])
|
||||
.options(
|
||||
UserRole::ordered()
|
||||
.into_iter()
|
||||
.map(|role| role.to_select_child())
|
||||
.collect(),
|
||||
)
|
||||
.build()
|
||||
.into_node();
|
||||
let user_role_field = StyledField::build()
|
||||
.input(user_role)
|
||||
.label("Role")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let submit = StyledButton::build()
|
||||
.add_class("submitUserInvite")
|
||||
.active(true)
|
||||
.primary()
|
||||
.text("Invite user")
|
||||
.build()
|
||||
.into_node();
|
||||
let submit_field = StyledField::build().input(submit).build().into_node();
|
||||
|
||||
let form = StyledForm::build()
|
||||
.heading("Invite new user")
|
||||
.add_field(name_field)
|
||||
.add_field(email_field)
|
||||
.add_field(user_role_field)
|
||||
.add_field(submit_field)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
inner_layout(model, "users", vec![form], empty![])
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
@ -249,6 +250,88 @@ impl Into<IssuePriority> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
|
||||
#[cfg_attr(feature = "backend", sql_type = "UserRoleType")]
|
||||
#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Hash)]
|
||||
pub enum UserRole {
|
||||
User,
|
||||
Manager,
|
||||
Owner,
|
||||
}
|
||||
|
||||
impl PartialOrd for UserRole {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
use UserRole::*;
|
||||
|
||||
if self == other {
|
||||
return Some(Ordering::Equal);
|
||||
}
|
||||
let order = match (self, other) {
|
||||
(User, Manager) | (User, Owner) | (Manager, Owner) => Ordering::Less,
|
||||
_ => Ordering::Greater,
|
||||
};
|
||||
Some(order)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToVec for UserRole {
|
||||
type Item = UserRole;
|
||||
|
||||
fn ordered() -> Vec<Self::Item> {
|
||||
vec![UserRole::User, UserRole::Manager, UserRole::Owner]
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for UserRole {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().trim() {
|
||||
"user" => Ok(UserRole::User),
|
||||
"manager" => Ok(UserRole::Manager),
|
||||
"owner" => Ok(UserRole::Owner),
|
||||
_ => Err(format!("Unknown user role {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserRole {
|
||||
fn default() -> Self {
|
||||
UserRole::User
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UserRole {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserRole::User => f.write_str("user"),
|
||||
UserRole::Manager => f.write_str("manager"),
|
||||
UserRole::Owner => f.write_str("owner"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<u32> for UserRole {
|
||||
fn into(self) -> u32 {
|
||||
match self {
|
||||
UserRole::User => 0,
|
||||
UserRole::Manager => 1,
|
||||
UserRole::Owner => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<UserRole> for u32 {
|
||||
fn into(self) -> UserRole {
|
||||
match self {
|
||||
0 => UserRole::User,
|
||||
1 => UserRole::Manager,
|
||||
2 => UserRole::Owner,
|
||||
_ => UserRole::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
|
||||
#[cfg_attr(feature = "backend", sql_type = "ProjectCategoryType")]
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
|
||||
@ -376,6 +459,7 @@ pub struct User {
|
||||
pub project_id: ProjectId,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub user_role: UserRole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@ -496,6 +580,18 @@ pub enum SignUpFieldId {
|
||||
Email,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum UsersFieldId {
|
||||
Username,
|
||||
Email,
|
||||
UserRole,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum InviteFieldId {
|
||||
Token,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum CommentFieldId {
|
||||
Body,
|
||||
|
@ -2,7 +2,7 @@ use std::io::Write;
|
||||
|
||||
use diesel::{deserialize::*, pg::*, serialize::*, *};
|
||||
|
||||
use crate::{IssuePriority, IssueStatus, IssueType, ProjectCategory};
|
||||
use crate::{IssuePriority, IssueStatus, IssueType, ProjectCategory, UserRole};
|
||||
|
||||
#[derive(SqlType)]
|
||||
#[postgres(type_name = "IssuePriorityType")]
|
||||
@ -122,14 +122,12 @@ impl ToSql<IssueStatusType, Pg> for IssueStatus {
|
||||
}
|
||||
}
|
||||
|
||||
///////////
|
||||
|
||||
#[derive(SqlType)]
|
||||
#[postgres(type_name = "ProjectCategoryType")]
|
||||
pub struct ProjectCategoryType;
|
||||
|
||||
impl diesel::query_builder::QueryId for ProjectCategoryType {
|
||||
type QueryId = IssueStatus;
|
||||
type QueryId = ProjectCategory;
|
||||
}
|
||||
|
||||
fn project_category_from_sql(bytes: Option<&[u8]>) -> deserialize::Result<ProjectCategory> {
|
||||
@ -163,3 +161,43 @@ impl ToSql<ProjectCategoryType, Pg> for ProjectCategory {
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(SqlType)]
|
||||
#[postgres(type_name = "UserRoleType")]
|
||||
pub struct UserRoleType;
|
||||
|
||||
impl diesel::query_builder::QueryId for UserRoleType {
|
||||
type QueryId = UserRole;
|
||||
}
|
||||
|
||||
fn user_role_from_sql(bytes: Option<&[u8]>) -> deserialize::Result<UserRole> {
|
||||
match not_none!(bytes) {
|
||||
b"user" => Ok(UserRole::User),
|
||||
b"manager" => Ok(UserRole::Manager),
|
||||
b"owner" => Ok(UserRole::Owner),
|
||||
_ => Ok(UserRole::User),
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<UserRoleType, Pg> for UserRole {
|
||||
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
|
||||
user_role_from_sql(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<sql_types::Text, Pg> for UserRole {
|
||||
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
|
||||
user_role_from_sql(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<UserRoleType, Pg> for UserRole {
|
||||
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> serialize::Result {
|
||||
match *self {
|
||||
UserRole::User => out.write_all(b"user")?,
|
||||
UserRole::Manager => out.write_all(b"manager")?,
|
||||
UserRole::Owner => out.write_all(b"owner")?,
|
||||
}
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE IF EXISTS users DROP COLUMN role;
|
||||
DROP TYPE IF EXISTS "UserRoleType";
|
@ -0,0 +1,8 @@
|
||||
DROP TYPE IF EXISTS "UserRoleType" CASCADE;
|
||||
CREATE TYPE "UserRoleType" AS ENUM (
|
||||
'user',
|
||||
'manager',
|
||||
'owner'
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN role "UserRoleType" DEFAULT 'user' NOT NULL;
|
@ -2,7 +2,7 @@ use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use jirs_data::{IssuePriority, IssueStatus, IssueType, ProjectCategory};
|
||||
use jirs_data::{IssuePriority, IssueStatus, IssueType, ProjectCategory, UserRole};
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
@ -173,6 +173,7 @@ pub struct User {
|
||||
pub project_id: i32,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub user_role: UserRole,
|
||||
}
|
||||
|
||||
impl Into<jirs_data::User> for User {
|
||||
@ -185,6 +186,7 @@ impl Into<jirs_data::User> for User {
|
||||
project_id: self.project_id,
|
||||
created_at: self.created_at,
|
||||
updated_at: self.updated_at,
|
||||
user_role: self.user_role,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -199,6 +201,7 @@ impl Into<jirs_data::User> for &User {
|
||||
project_id: self.project_id,
|
||||
created_at: self.created_at,
|
||||
updated_at: self.updated_at,
|
||||
user_role: self.user_role,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -345,6 +345,12 @@ table! {
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
/// The `role` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `UserRoleType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
role -> UserRoleType,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user