Add invite users page and handle invitation page

This commit is contained in:
Adrian Woźniak 2020-04-20 11:22:39 +02:00
parent aa56c2fe52
commit a16ddbcf2e
20 changed files with 507 additions and 44 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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
View 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])
}

View File

@ -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)),
}
}

View File

@ -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()

View File

@ -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();

View File

@ -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)]

View File

@ -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,
]
]
}

View File

@ -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)
}

View File

@ -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()),

View File

@ -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

View File

@ -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
View 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![])
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS users DROP COLUMN role;
DROP TYPE IF EXISTS "UserRoleType";

View File

@ -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;

View File

@ -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,
}
}
}

View File

@ -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,
}
}