Add copy link command

This commit is contained in:
Adrian Woźniak 2020-04-02 16:14:07 +02:00
parent 5e81329378
commit f778f5a5c5
94 changed files with 741 additions and 631 deletions

View File

@ -31,5 +31,8 @@ features = [
"DataTransfer", "DataTransfer",
"DragEvent", "DragEvent",
"HtmlDivElement", "HtmlDivElement",
"DomRect" "DomRect",
"HtmlDocument",
"Document",
"Selection"
] ]

View File

@ -30,7 +30,7 @@ pub async fn update_issue(
.method(Method::Put) .method(Method::Put)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body_json(&payload) .body_json(&payload)
.fetch_json(Msg::IssueUpdateResult) .fetch_string(Msg::IssueUpdateResult)
.await .await
} }
Err(e) => return Ok(Msg::InternalFailure(e)), Err(e) => return Ok(Msg::InternalFailure(e)),

View File

@ -1,7 +1,7 @@
use seed::fetch::{FetchObject, ResponseWithDataResult}; use seed::fetch::{FetchObject, ResponseWithDataResult};
use seed::*; use seed::*;
use jirs_data::{FullProjectResponse, Issue}; use jirs_data::{FullIssue, FullProject, Issue};
use crate::model::Model; use crate::model::Model;
@ -42,9 +42,9 @@ pub fn current_project_response(fetched: &FetchObject<String>, model: &mut Model
if status.is_error() { if status.is_error() {
return; return;
} }
match serde_json::from_str::<'_, FullProjectResponse>(body.as_str()) { match serde_json::from_str::<'_, FullProject>(body.as_str()) {
Ok(project_response) => { Ok(project) => {
model.project = Some(project_response.project); model.project = Some(project);
} }
_ => (), _ => (),
} }
@ -52,8 +52,6 @@ pub fn current_project_response(fetched: &FetchObject<String>, model: &mut Model
} }
pub fn update_issue_response(fetched: &FetchObject<String>, model: &mut Model) { pub fn update_issue_response(fetched: &FetchObject<String>, model: &mut Model) {
log!("update_issue_response");
log!(fetched);
if let FetchObject { if let FetchObject {
result: result:
Ok(ResponseWithDataResult { Ok(ResponseWithDataResult {
@ -68,18 +66,21 @@ pub fn update_issue_response(fetched: &FetchObject<String>, model: &mut Model) {
return; return;
} }
match ( match (
serde_json::from_str::<'_, Issue>(body.as_str()), serde_json::from_str::<'_, FullIssue>(body.as_str()),
model.project.as_mut(), model.project.as_mut(),
) { ) {
(Ok(issue), Some(project)) => { (Ok(issue), Some(project)) => {
let mut issues: Vec<Issue> = vec![]; let mut issues: Vec<Issue> = vec![];
for i in project.issues.iter() { std::mem::swap(&mut project.issues, &mut issues);
for i in issues.into_iter() {
if i.id != issue.id { if i.id != issue.id {
issues.push(i.clone()); project.issues.push(i);
} }
} }
issues.push(issue); project.issues.push(issue.into());
project.issues = issues; }
(Err(error), _) => {
error!(error);
} }
_ => (), _ => (),
} }

View File

@ -20,6 +20,7 @@ pub struct EditIssueModal {
pub top_select_opened: bool, pub top_select_opened: bool,
pub top_select_filter: String, pub top_select_filter: String,
pub value: IssueType, pub value: IssueType,
pub link_copied: bool,
} }
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)]

View File

@ -2,6 +2,7 @@ use seed::{prelude::*, *};
use jirs_data::*; use jirs_data::*;
use crate::api::update_issue;
use crate::model::{EditIssueModal, Icon, ModalType, Model, Page}; use crate::model::{EditIssueModal, Icon, ModalType, Model, Page};
use crate::shared::modal::{Modal, Variant as ModalVariant}; use crate::shared::modal::{Modal, Variant as ModalVariant};
use crate::shared::styled_avatar::StyledAvatar; use crate::shared::styled_avatar::StyledAvatar;
@ -38,6 +39,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
top_select_opened: false, top_select_opened: false,
top_select_filter: "".to_string(), top_select_filter: "".to_string(),
value, value,
link_copied: false,
}, },
)); ));
} }
@ -97,31 +99,34 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
break; break;
} }
} }
if let Some(issue) = found { let issue = match found {
issue.status = status.clone(); Some(i) => i,
issue.list_position = position + 1f64; _ => return,
};
let payload = UpdateIssuePayload { issue.status = status.clone();
title: Some(issue.title.clone()), issue.list_position = position + 1f64;
issue_type: Some(issue.issue_type.clone()),
status: Some(status.to_payload().to_string()), let payload = UpdateIssuePayload {
priority: Some(issue.priority.clone()), title: Some(issue.title.clone()),
list_position: Some(issue.list_position), issue_type: Some(issue.issue_type.clone()),
description: Some(issue.description.clone()), status: Some(status),
description_text: Some(issue.description_text.clone()), priority: Some(issue.priority.clone()),
estimate: Some(issue.estimate), list_position: Some(issue.list_position),
time_spent: Some(issue.time_spent), description: Some(issue.description.clone()),
time_remaining: Some(issue.time_remaining), description_text: Some(issue.description_text.clone()),
project_id: Some(issue.project_id), estimate: Some(issue.estimate),
users: Some(vec![]), time_spent: Some(issue.time_spent),
user_ids: Some(issue.user_ids.clone()), time_remaining: Some(issue.time_remaining),
}; project_id: Some(issue.project_id),
orders.skip().perform_cmd(crate::api::update_issue( user_ids: Some(issue.user_ids.clone()),
model.host_url.clone(), };
issue.id, model.project_page.dragged_issue_id = None;
payload, orders.skip().perform_cmd(crate::api::update_issue(
)); model.host_url.clone(),
} issue.id,
payload,
));
} }
_ => error!("Drag stopped before drop :("), _ => error!("Drag stopped before drop :("),
} }
@ -137,8 +142,46 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
) => { ) => {
modal.top_select_opened = flag; modal.top_select_opened = flag;
} }
(StyledSelectChange::Changed(value), Some(ModalType::EditIssue(_, modal))) => { (
StyledSelectChange::Changed(value),
Some(ModalType::EditIssue(issue_id, modal)),
) => {
modal.value = value.into(); modal.value = value.into();
let project = match model.project.as_mut() {
Some(p) => p,
_ => return,
};
let mut found: Option<&mut Issue> = None;
for issue in project.issues.iter_mut() {
if issue.id == *issue_id {
found = Some(issue);
break;
}
}
let issue = match found {
Some(i) => i,
_ => return,
};
let form = UpdateIssuePayload {
title: Some(issue.title.clone()),
issue_type: Some(modal.value.clone()),
status: Some(issue.status.clone()),
priority: Some(issue.priority.clone()),
list_position: Some(issue.list_position),
description: Some(issue.description.clone()),
description_text: Some(issue.description_text.clone()),
estimate: Some(issue.estimate.clone()),
time_spent: Some(issue.time_spent.clone()),
time_remaining: Some(issue.time_remaining.clone()),
project_id: Some(issue.project_id.clone()),
user_ids: Some(issue.user_ids.clone()),
};
orders.skip().perform_cmd(update_issue(
model.host_url.clone(),
*issue_id,
form,
));
} }
_ => {} _ => {}
} }
@ -405,10 +448,10 @@ fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node<Ms
let href = format!("/issues/{id}", id = issue_id); let href = format!("/issues/{id}", id = issue_id);
a![ a![
drag_started,
attrs![At::Class => "issueLink"; At::Href => href], attrs![At::Class => "issueLink"; At::Href => href],
div![ div![
attrs![At::Class => class_list.join(" "), At::Draggable => true], attrs![At::Class => class_list.join(" "), At::Draggable => true],
drag_started,
drag_stopped, drag_stopped,
p![attrs![At::Class => "title"], issue.title,], p![attrs![At::Class => "title"], issue.title,],
div![ div![
@ -492,12 +535,48 @@ fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<
} }
.into_node(); .into_node();
let click_handler = mouse_ev(Ev::Click, move |_| {
use wasm_bindgen::JsCast;
let link = format!("http://localhost:7000/issues/{id}", id = issue_id);
let el = match seed::html_document().create_element("textarea") {
Ok(el) => el
.dyn_ref::<web_sys::HtmlTextAreaElement>()
.unwrap()
.clone(),
_ => return Msg::NoOp,
};
seed::body().append_child(&el).unwrap();
el.set_text_content(Some(link.as_str()));
el.select();
el.set_selection_range(0, 9999).unwrap();
seed::html_document().exec_command("copy").unwrap();
seed::body().remove_child(&el).unwrap();
Msg::NoOp
});
let copy_button = StyledButton {
variant: ButtonVariant::Empty,
icon_only: false,
disabled: false,
active: false,
text: None,
icon: Some(Icon::Link),
on_click: Some(click_handler),
children: vec![span![if modal.link_copied {
"Link Copied"
} else {
"Copy link"
}]],
}
.into_node();
div![ div![
attrs![At::Class => "issueDetails"], attrs![At::Class => "issueDetails"],
div![ div![
attrs![At::Class => "topActions"], attrs![At::Class => "topActions"],
issue_type_select, issue_type_select,
div![attrs![At::Class => "topActionsRight"]], div![attrs![At::Class => "topActionsRight"], copy_button],
], ],
div![ div![
attrs![At::Class => "content"], attrs![At::Class => "content"],

View File

@ -162,14 +162,6 @@ where
] ]
} }
fn render_option(
content: Node<Msg>,
on_change: EventHandler<Msg>,
on_click: EventHandler<Msg>,
) -> Node<Msg> {
div![attrs![At::Class => "option"], on_change, on_click, content]
}
fn render_value(mut content: Node<Msg>) -> Node<Msg> { fn render_value(mut content: Node<Msg>) -> Node<Msg> {
content.add_class("value"); content.add_class("value");
content content

View File

@ -12,12 +12,6 @@ pub use sql::*;
#[cfg(feature = "backend")] #[cfg(feature = "backend")]
pub mod sql; pub mod sql;
pub trait ResponseData {
type Response: Serialize;
fn into_response(self) -> Self::Response;
}
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq)] #[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq)]
@ -69,7 +63,10 @@ impl std::fmt::Display for IssueType {
} }
} }
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueStatusType")]
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq)] #[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum IssueStatus { pub enum IssueStatus {
Backlog, Backlog,
Selected, Selected,
@ -196,20 +193,6 @@ pub struct FullProject {
pub users: Vec<User>, pub users: Vec<User>,
} }
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FullProjectResponse {
pub project: FullProject,
}
impl ResponseData for FullProject {
type Response = FullProjectResponse;
fn into_response(self) -> Self::Response {
FullProjectResponse { project: self }
}
}
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct FullIssue { pub struct FullIssue {
@ -217,7 +200,7 @@ pub struct FullIssue {
pub title: String, pub title: String,
#[serde(rename = "type")] #[serde(rename = "type")]
pub issue_type: IssueType, pub issue_type: IssueType,
pub status: String, pub status: IssueStatus,
pub priority: IssuePriority, pub priority: IssuePriority,
pub list_position: f64, pub list_position: f64,
pub description: Option<String>, pub description: Option<String>,
@ -234,17 +217,26 @@ pub struct FullIssue {
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
} }
#[derive(Clone, Serialize, Deserialize, Debug)] impl Into<Issue> for FullIssue {
#[serde(rename_all = "camelCase")] fn into(self) -> Issue {
pub struct FullIssueResponse { Issue {
pub issue: FullIssue, id: self.id,
} title: self.title,
issue_type: self.issue_type,
impl ResponseData for FullIssue { status: self.status,
type Response = FullIssueResponse; priority: self.priority,
list_position: self.list_position,
fn into_response(self) -> Self::Response { description: self.description,
FullIssueResponse { issue: self } description_text: self.description_text,
estimate: self.estimate,
time_spent: self.time_spent,
time_remaining: self.time_remaining,
reporter_id: self.reporter_id,
project_id: self.project_id,
created_at: self.created_at,
updated_at: self.updated_at,
user_ids: self.user_ids,
}
} }
} }
@ -325,7 +317,7 @@ pub struct UpdateIssuePayload {
pub title: Option<String>, pub title: Option<String>,
#[serde(rename = "type")] #[serde(rename = "type")]
pub issue_type: Option<IssueType>, pub issue_type: Option<IssueType>,
pub status: Option<String>, pub status: Option<IssueStatus>,
pub priority: Option<IssuePriority>, pub priority: Option<IssuePriority>,
pub list_position: Option<f64>, pub list_position: Option<f64>,
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
@ -334,8 +326,6 @@ pub struct UpdateIssuePayload {
pub time_spent: Option<Option<i32>>, pub time_spent: Option<Option<i32>>,
pub time_remaining: Option<Option<i32>>, pub time_remaining: Option<Option<i32>>,
pub project_id: Option<i32>, pub project_id: Option<i32>,
pub users: Option<Vec<User>>,
pub user_ids: Option<Vec<i32>>, pub user_ids: Option<Vec<i32>>,
} }
@ -359,7 +349,7 @@ pub struct CreateIssuePayload {
pub title: String, pub title: String,
#[serde(rename = "type")] #[serde(rename = "type")]
pub issue_type: IssueType, pub issue_type: IssueType,
pub status: String, pub status: IssueStatus,
pub priority: IssuePriority, pub priority: IssuePriority,
pub description: Option<String>, pub description: Option<String>,
pub description_text: Option<String>, pub description_text: Option<String>,

View File

@ -2,7 +2,7 @@ use std::io::Write;
use diesel::{deserialize::*, pg::*, serialize::*, *}; use diesel::{deserialize::*, pg::*, serialize::*, *};
use crate::{IssuePriority, IssueType}; use crate::{IssuePriority, IssueStatus, IssueType};
#[derive(SqlType)] #[derive(SqlType)]
#[postgres(type_name = "IssuePriorityType")] #[postgres(type_name = "IssuePriorityType")]
@ -79,3 +79,45 @@ impl ToSql<IssueTypeType, Pg> for IssueType {
Ok(IsNull::No) Ok(IsNull::No)
} }
} }
#[derive(SqlType)]
#[postgres(type_name = "IssueStatusType")]
pub struct IssueStatusType;
impl diesel::query_builder::QueryId for IssueStatusType {
type QueryId = IssueStatus;
}
fn issue_status_from_sql(bytes: Option<&[u8]>) -> deserialize::Result<IssueStatus> {
match not_none!(bytes) {
b"backlog" => Ok(IssueStatus::Backlog),
b"selected" => Ok(IssueStatus::Selected),
b"in_progress" | b"inprogress" => Ok(IssueStatus::InProgress),
b"done" => Ok(IssueStatus::Done),
_ => Ok(IssueStatus::Backlog),
}
}
impl FromSql<IssueStatusType, Pg> for IssueStatus {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_status_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for IssueStatus {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_status_from_sql(bytes)
}
}
impl ToSql<IssueStatusType, Pg> for IssueStatus {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> serialize::Result {
match *self {
IssueStatus::Backlog => out.write_all(b"backlog")?,
IssueStatus::Selected => out.write_all(b"selected")?,
IssueStatus::InProgress => out.write_all(b"in_progress")?,
IssueStatus::Done => out.write_all(b"done")?,
}
Ok(IsNull::No)
}
}

View File

@ -0,0 +1,2 @@
DROP EXTENSION IF EXISTS "uuid-ossp";

View File

@ -0,0 +1 @@
CREATE EXTENSION "uuid-ossp";

View File

@ -0,0 +1 @@
DROP TYPE IF EXISTS "ProjectCategoryType" CASCADE;

View File

@ -0,0 +1,6 @@
DROP TYPE IF EXISTS "ProjectCategoryType" CASCADE;
CREATE TYPE "ProjectCategoryType" as ENUM (
'software',
'marketing',
'business'
);

View File

@ -0,0 +1 @@
DROP TYPE IF EXISTS "IssuePriorityType" CASCADE;

View File

@ -0,0 +1,8 @@
DROP TYPE IF EXISTS "IssuePriorityType" CASCADE;
CREATE TYPE "IssuePriorityType" as ENUM (
'highest',
'high',
'medium',
'low',
'lowest'
);

View File

@ -0,0 +1 @@
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;

View File

@ -0,0 +1,6 @@
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;
CREATE TYPE "IssueTypeType" AS ENUM (
'task',
'bug',
'story'
);

View File

@ -0,0 +1 @@
DROP TYPE IF EXISTS "IssueStatusType" CASCADE;

View File

@ -0,0 +1,7 @@
DROP TYPE IF EXISTS "IssueStatusType" CASCADE;
CREATE TYPE "IssueStatusType" AS ENUM (
'backlog',
'selected',
'in_progress',
'done'
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS projects CASCADE;

View File

@ -0,0 +1,10 @@
DROP TABLE IF EXISTS projects CASCADE;
CREATE TABLE projects (
id serial primary key not null,
name text not null,
url text not null default '',
description text not null default '',
category text not null default 'software',
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS users CASCADE;

View File

@ -0,0 +1,9 @@
CREATE TABLE users (
id serial primary key not null,
name text not null,
email text not null,
avatar_url text,
project_id integer not null references projects (id),
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS issues CASCADE;

View File

@ -0,0 +1,17 @@
CREATE TABLE issues (
id serial primary key not null,
title text not null,
issue_type "IssueTypeType" NOT NULL DEFAULT 'task',
status "IssueStatusType" NOT NULL DEFAULT 'backlog',
priority "IssuePriorityType" NOT NULL DEFAULT 'low',
list_position double precision not null default 0,
description text,
description_text text,
estimate integer,
time_spent integer,
time_remaining integer,
reporter_id integer not null references users (id),
project_id integer not null references projects (id),
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS comments CASCADE;

View File

@ -0,0 +1,8 @@
CREATE TABLE comments (
id serial primary key not null,
body text not null,
user_id integer not null references users (id),
issue_id integer not null references issues (id),
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS tokens CASCADE;

View File

@ -0,0 +1,8 @@
CREATE TABLE tokens (
id serial primary key not null,
user_id integer not null references users (id),
access_token uuid not null,
refresh_token uuid not null,
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);

View File

@ -1,16 +0,0 @@
DROP TYPE IF EXISTS "ProjectCategoryType" CASCADE;
DROP TYPE IF EXISTS "IssuePriorityType" CASCADE;
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;
DROP TABLE IF EXISTS projects CASCADE;
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS issues CASCADE;
DROP TABLE IF EXISTS comments CASCADE;
DROP TABLE IF EXISTS tokens CASCADE;
DROP EXTENSION IF EXISTS "uuid-ossp";

View File

@ -1,77 +0,0 @@
CREATE EXTENSION "uuid-ossp";
CREATE TYPE "ProjectCategoryType" as ENUM (
'software',
'marketing',
'business'
);
CREATE TYPE "IssuePriorityType" as ENUM (
'highest',
'high',
'medium',
'low',
'lowest'
);
CREATE TYPE "IssueTypeType" AS ENUM (
'task',
'bug',
'story'
);
CREATE TABLE projects (
id serial primary key not null,
name text not null,
url text not null default '',
description text not null default '',
category text not null default 'software',
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);
CREATE TABLE users (
id serial primary key not null,
name text not null,
email text not null,
avatar_url text,
project_id integer not null references projects (id),
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);
CREATE TABLE issues (
id serial primary key not null,
title text not null,
issue_type "IssueTypeType" not null,
status text not null,
priority "IssuePriorityType" not null,
list_position double precision not null default 0,
description text,
description_text text,
estimate integer,
time_spent integer,
time_remaining integer,
reporter_id integer not null references users (id),
project_id integer not null references projects (id),
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);
CREATE TABLE comments (
id serial primary key not null,
body text not null,
user_id integer not null references users (id),
issue_id integer not null references issues (id),
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);
CREATE TABLE tokens (
id serial primary key not null,
user_id integer not null references users (id),
access_token uuid not null,
refresh_token uuid not null,
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);

View File

@ -4,12 +4,14 @@ use diesel::expression::sql_literal::sql;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use jirs_data::{IssuePriority, IssueType}; use jirs_data::{IssuePriority, IssueStatus, IssueType};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
use crate::models::Issue; use crate::models::Issue;
const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue";
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadIssue { pub struct LoadIssue {
pub issue_id: i32, pub issue_id: i32,
@ -69,7 +71,7 @@ pub struct UpdateIssue {
pub issue_id: i32, pub issue_id: i32,
pub title: Option<String>, pub title: Option<String>,
pub issue_type: Option<IssueType>, pub issue_type: Option<IssueType>,
pub status: Option<String>, pub status: Option<IssueStatus>,
pub priority: Option<IssuePriority>, pub priority: Option<IssuePriority>,
pub list_position: Option<f64>, pub list_position: Option<f64>,
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
@ -78,8 +80,6 @@ pub struct UpdateIssue {
pub time_spent: Option<Option<i32>>, pub time_spent: Option<Option<i32>>,
pub time_remaining: Option<Option<i32>>, pub time_remaining: Option<Option<i32>>,
pub project_id: Option<i32>, pub project_id: Option<i32>,
pub users: Option<Vec<jirs_data::User>>,
pub user_ids: Option<Vec<i32>>, pub user_ids: Option<Vec<i32>>,
} }
@ -121,9 +121,9 @@ impl Handler<UpdateIssue> for DbExecutor {
dsl::updated_at.eq(chrono::Utc::now().naive_utc()), dsl::updated_at.eq(chrono::Utc::now().naive_utc()),
)); ));
diesel::debug_query::<diesel::pg::Pg, _>(&chain); diesel::debug_query::<diesel::pg::Pg, _>(&chain);
chain chain.get_result::<Issue>(conn).map_err(|_| {
.get_result::<Issue>(conn) ServiceErrors::DatabaseQueryFailed("Failed to update issue".to_string())
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; })?;
if let Some(user_ids) = msg.user_ids.as_ref() { if let Some(user_ids) = msg.user_ids.as_ref() {
use crate::schema::issue_assignees::dsl; use crate::schema::issue_assignees::dsl;
@ -148,7 +148,9 @@ impl Handler<UpdateIssue> for DbExecutor {
diesel::insert_into(dsl::issue_assignees) diesel::insert_into(dsl::issue_assignees)
.values(values) .values(values)
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|_| {
ServiceErrors::DatabaseQueryFailed(FAILED_CONNECT_USER_AND_ISSUE.to_string())
})?;
} }
let row = issues let row = issues
@ -194,7 +196,7 @@ impl Handler<DeleteIssue> for DbExecutor {
pub struct CreateIssue { pub struct CreateIssue {
pub title: String, pub title: String,
pub issue_type: IssueType, pub issue_type: IssueType,
pub status: String, pub status: IssueStatus,
pub priority: IssuePriority, pub priority: IssuePriority,
pub description: Option<String>, pub description: Option<String>,
pub description_text: Option<String>, pub description_text: Option<String>,
@ -223,7 +225,7 @@ impl Handler<CreateIssue> for DbExecutor {
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let list_position = issues let list_position = issues
.filter(status.eq("backlog")) .filter(status.eq(IssueStatus::Backlog))
.select(sql("max(list_position) + 1.0")) .select(sql("max(list_position) + 1.0"))
.get_result::<f64>(conn) .get_result::<f64>(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|_| ServiceErrors::DatabaseConnectionLost)?;

View File

@ -1,4 +1,5 @@
use actix_web::HttpResponse; use actix_web::HttpResponse;
use jirs_data::ErrorResponse; use jirs_data::ErrorResponse;
const TOKEN_NOT_FOUND: &str = "Token not found"; const TOKEN_NOT_FOUND: &str = "Token not found";
@ -7,6 +8,7 @@ const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
pub enum ServiceErrors { pub enum ServiceErrors {
Unauthorized, Unauthorized,
DatabaseConnectionLost, DatabaseConnectionLost,
DatabaseQueryFailed(String),
RecordNotFound(String), RecordNotFound(String),
} }
@ -27,6 +29,11 @@ impl Into<HttpResponse> for ServiceErrors {
errors: vec![DATABASE_CONNECTION_FAILED.to_owned()], errors: vec![DATABASE_CONNECTION_FAILED.to_owned()],
}) })
} }
ServiceErrors::DatabaseQueryFailed(error) => {
HttpResponse::BadRequest().json(ErrorResponse {
errors: vec![error.to_owned()],
})
}
ServiceErrors::RecordNotFound(resource_name) => { ServiceErrors::RecordNotFound(resource_name) => {
HttpResponse::BadRequest().json(ErrorResponse { HttpResponse::BadRequest().json(ErrorResponse {
errors: vec![format!("Resource not found {}", resource_name)], errors: vec![format!("Resource not found {}", resource_name)],

View File

@ -49,7 +49,7 @@ pub struct Issue {
pub title: String, pub title: String,
#[serde(rename = "type")] #[serde(rename = "type")]
pub issue_type: IssueType, pub issue_type: IssueType,
pub status: String, pub status: IssueStatus,
pub priority: IssuePriority, pub priority: IssuePriority,
pub list_position: f64, pub list_position: f64,
pub description: Option<String>, pub description: Option<String>,
@ -69,11 +69,7 @@ impl Into<jirs_data::Issue> for Issue {
id: self.id, id: self.id,
title: self.title, title: self.title,
issue_type: self.issue_type, issue_type: self.issue_type,
status: self status: self.status,
.status
.as_str()
.parse::<IssueStatus>()
.unwrap_or_else(|_| IssueStatus::Backlog),
priority: self.priority, priority: self.priority,
list_position: self.list_position, list_position: self.list_position,
description: self.description, description: self.description,
@ -123,7 +119,7 @@ pub struct CreateIssueForm {
pub title: String, pub title: String,
#[serde(rename = "type")] #[serde(rename = "type")]
pub issue_type: IssueType, pub issue_type: IssueType,
pub status: String, pub status: IssueStatus,
pub priority: IssuePriority, pub priority: IssuePriority,
pub list_position: f64, pub list_position: f64,
pub description: Option<String>, pub description: Option<String>,

View File

@ -4,8 +4,6 @@ use actix::Addr;
use actix_web::web::{Data, Json, Path}; use actix_web::web::{Data, Json, Path};
use actix_web::{delete, get, post, put, HttpRequest, HttpResponse}; use actix_web::{delete, get, post, put, HttpRequest, HttpResponse};
use jirs_data::ResponseData;
use crate::db::authorize_user::AuthorizeUser; use crate::db::authorize_user::AuthorizeUser;
use crate::db::comments::LoadIssueComments; use crate::db::comments::LoadIssueComments;
use crate::db::issues::{CreateIssue, DeleteIssue, LoadIssue, UpdateIssue}; use crate::db::issues::{CreateIssue, DeleteIssue, LoadIssue, UpdateIssue};
@ -44,7 +42,7 @@ pub async fn issue_with_users_and_comments(
}; };
match load_issue(issue_id, db).await { match load_issue(issue_id, db).await {
Ok(full_issue) => HttpResponse::Ok().json(full_issue.into_response()), Ok(full_issue) => HttpResponse::Ok().json(full_issue),
Err(e) => e.into_http_response(), Err(e) => e.into_http_response(),
} }
} }
@ -92,13 +90,13 @@ pub async fn update(
Ok(uuid) => uuid, Ok(uuid) => uuid,
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
}; };
let _user = match db match db
.send(AuthorizeUser { .send(AuthorizeUser {
access_token: token, access_token: token,
}) })
.await .await
{ {
Ok(Ok(user)) => user, Ok(Ok(_)) => (),
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
}; };
let signal = UpdateIssue { let signal = UpdateIssue {
@ -115,7 +113,6 @@ pub async fn update(
time_remaining: payload.time_remaining.clone(), time_remaining: payload.time_remaining.clone(),
project_id: payload.project_id.clone(), project_id: payload.project_id.clone(),
user_ids: payload.user_ids.clone(), user_ids: payload.user_ids.clone(),
users: payload.users.clone(),
}; };
match db.send(signal).await { match db.send(signal).await {
Ok(Ok(_)) => (), Ok(Ok(_)) => (),
@ -123,7 +120,7 @@ pub async fn update(
_ => return ServiceErrors::DatabaseConnectionLost.into_http_response(), _ => return ServiceErrors::DatabaseConnectionLost.into_http_response(),
}; };
match load_issue(issue_id, db).await { match load_issue(issue_id, db).await {
Ok(full_issue) => HttpResponse::Ok().json(full_issue.into_response()), Ok(full_issue) => HttpResponse::Ok().json(full_issue),
Err(e) => e.into_http_response(), Err(e) => e.into_http_response(),
} }
} }

View File

@ -2,8 +2,6 @@ use actix::Addr;
use actix_web::web::{Data, Json, Path}; use actix_web::web::{Data, Json, Path};
use actix_web::{get, put, HttpRequest, HttpResponse}; use actix_web::{get, put, HttpRequest, HttpResponse};
use jirs_data::ResponseData;
use crate::db::authorize_user::AuthorizeUser; use crate::db::authorize_user::AuthorizeUser;
use crate::db::issues::LoadProjectIssues; use crate::db::issues::LoadProjectIssues;
use crate::db::projects::{LoadCurrentProject, UpdateProject}; use crate::db::projects::{LoadCurrentProject, UpdateProject};
@ -73,7 +71,7 @@ pub async fn project_with_users_and_issues(
.collect(), .collect(),
users: users.into_iter().map(|u| u.into()).collect(), users: users.into_iter().map(|u| u.into()).collect(),
}; };
HttpResponse::Ok().json(res.into_response()) HttpResponse::Ok().json(res)
} }
#[put("/{id}")] #[put("/{id}")]

View File

@ -114,10 +114,10 @@ table! {
issue_type -> IssueTypeType, issue_type -> IssueTypeType,
/// The `status` column of the `issues` table. /// The `status` column of the `issues` table.
/// ///
/// Its SQL type is `Text`. /// Its SQL type is `IssueStatusType`.
/// ///
/// (Automatically generated by Diesel.) /// (Automatically generated by Diesel.)
status -> Text, status -> IssueStatusType,
/// The `priority` column of the `issues` table. /// The `priority` column of the `issues` table.
/// ///
/// Its SQL type is `IssuePriorityType`. /// Its SQL type is `IssuePriorityType`.

View File

@ -1,6 +1,6 @@
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font } from '../shared/utils/styles';
export default createGlobalStyle` export default createGlobalStyle`
html, body, #root { html, body, #root {
@ -89,7 +89,15 @@ export default createGlobalStyle`
p { p {
line-height: 1.4285; line-height: 1.4285;
a { a {
${mixin.link()} cursor: pointer;
color: ${color.textLink};
${font.medium}
&:hover, &:visited, &:active {
color: ${color.textLink};
}
&:hover {
text-decoration: underline;
}
} }
} }
@ -106,5 +114,20 @@ export default createGlobalStyle`
touch-action: manipulation; touch-action: manipulation;
} }
${mixin.placeholderColor(color.textLight)} ::-webkit-input-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
:-moz-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
::-moz-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
:-ms-input-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
`; `;

View File

@ -1,20 +1,20 @@
import React from 'react'; import React from 'react';
import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { Redirect, Route, Router, Switch } from 'react-router-dom';
import history from 'browserHistory'; import history from '../browserHistory';
import Project from 'Project'; import Project from '../Project';
import Authenticate from 'Auth/Authenticate'; import Authenticate from '../Auth/Authenticate';
import PageError from 'shared/components/PageError'; import PageError from '../shared/components/PageError';
const Routes = () => ( const Routes = () => (
<Router history={history}> <Router history={history}>
<Switch> <Switch>
<Redirect exact from="/" to="/project" /> <Redirect exact from="/" to="/project"/>
<Route path="/authenticate" component={Authenticate} /> <Route path="/authenticate" component={Authenticate}/>
<Route path="/project" component={Project} /> <Route path="/project" component={Project}/>
<Route component={PageError} /> <Route component={PageError}/>
</Switch> </Switch>
</Router> </Router>
); );
export default Routes; export default Routes;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin, zIndexValues } from 'shared/utils/styles'; import { color, font, mixin, zIndexValues } from '../../shared/utils/styles';
import { Icon } from 'shared/components'; import { Icon } from '../../shared/components';
export const Container = styled.div` export const Container = styled.div`
z-index: ${zIndexValues.modal + 1}; z-index: ${zIndexValues.modal + 1};
@ -21,7 +21,7 @@ export const StyledToast = styled.div`
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
${mixin.clearfix} ${mixin.clearfix}
${mixin.hardwareAccelerate} transform: translateZ(0);
&.jira-toast-enter, &.jira-toast-enter,
&.jira-toast-exit.jira-toast-exit-active { &.jira-toast-exit.jira-toast-exit-active {

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import * as formActions from 'actions/forms'; import * as formActions from '../actions/forms';
import { getStoredAuthToken } from 'shared/utils/authToken'; import { getStoredAuthToken } from '../shared/utils/authToken';
import { import {
ActionButton, ActionButton,
@ -13,7 +13,7 @@ import {
Header, Header,
SignIn, SignIn,
SignInSection, SignInSection,
} from 'Project/IssueCreate/Styles'; } from '../Project/IssueCreate/Styles';
const Authenticate = ({ const Authenticate = ({
onEmailChanged, onEmailChanged,

View File

@ -1,7 +1,7 @@
import styled from 'styled-components/dist/styled-components.esm'; import styled from 'styled-components/dist/styled-components.esm';
import { color, font } from 'shared/utils/styles'; import { color, font } from '../shared/utils/styles';
import { Button, Form } from 'shared/components'; import { Button, Form } from '../shared/components';
export const FormElement = styled(Form.Element)`; export const FormElement = styled(Form.Element)`;
padding: 25px 40px 35px; padding: 25px 40px 35px;
@ -9,7 +9,7 @@ export const FormElement = styled(Form.Element)`;
export const FormHeading = styled.div` export const FormHeading = styled.div`
padding-bottom: 15px; padding-bottom: 15px;
${ font.size(21) } ${font.size(21)}
`; `;
export const SelectItem = styled.div` export const SelectItem = styled.div`

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color } from '../../../shared/utils/styles';
import { InputDebounced, Avatar, Button } from 'shared/components'; import { Avatar, Button, InputDebounced } from '../../../shared/components';
export const Filters = styled.div` export const Filters = styled.div`
display: flex; display: flex;

View File

@ -1,23 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { xor } from 'lodash'; import { xor } from 'lodash';
import { import { AvatarIsActiveBorder, Avatars, ClearAll, Filters, SearchInput, StyledAvatar, StyledButton, } from './Styles';
Filters,
SearchInput,
Avatars,
AvatarIsActiveBorder,
StyledAvatar,
StyledButton,
ClearAll,
} from './Styles';
const propTypes = {
projectUsers: PropTypes.array.isRequired,
defaultFilters: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
mergeFilters: PropTypes.func.isRequired,
};
const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => { const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => {
const { searchTerm, userIds, myOnly, recent } = filters; const { searchTerm, userIds, myOnly, recent } = filters;
@ -63,6 +48,11 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilte
); );
}; };
ProjectBoardFilters.propTypes = propTypes; ProjectBoardFilters.propTypes = {
projectUsers: PropTypes.array.isRequired,
defaultFilters: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
mergeFilters: PropTypes.func.isRequired,
};
export default ProjectBoardFilters; export default ProjectBoardFilters;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { font } from 'shared/utils/styles'; import { font } from '../../../shared/utils/styles';
export const Header = styled.div` export const Header = styled.div`
margin-top: 6px; margin-top: 6px;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Button } from 'shared/components'; import { Button } from '../../../shared/components';
import { BoardName, Header } from './Styles'; import { BoardName, Header } from './Styles';

View File

@ -1,6 +1,6 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color } from '../../../../shared/utils/styles';
export const User = styled.div` export const User = styled.div`
display: flex; display: flex;

View File

@ -1,17 +1,11 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Avatar, Select, Icon } from 'shared/components'; import { Avatar, Icon, Select } from '../../../../shared/components';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { User, Username } from './Styles'; import { User, Username } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
projectUsers: PropTypes.array.isRequired,
};
const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, projectUsers }) => { const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, projectUsers }) => {
const getUserById = userId => projectUsers.find(user => user.id === userId); const getUserById = userId => projectUsers.find(user => user.id === userId);
@ -66,6 +60,10 @@ const renderUser = (user, isSelectValue, removeOptionValue) => (
</User> </User>
); );
ProjectBoardIssueDetailsAssigneesReporter.propTypes = propTypes; ProjectBoardIssueDetailsAssigneesReporter.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
projectUsers: PropTypes.array.isRequired,
};
export default ProjectBoardIssueDetailsAssigneesReporter; export default ProjectBoardIssueDetailsAssigneesReporter;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Button } from 'shared/components'; import { Button } from '../../../../../shared/components';
export const Actions = styled.div` export const Actions = styled.div`
display: flex; display: flex;

View File

@ -1,18 +1,10 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Textarea } from 'shared/components'; import { Textarea } from '../../../../../shared/components';
import { Actions, FormButton } from './Styles'; import { Actions, FormButton } from './Styles';
const propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
isWorking: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
class ProjectBoardIssueDetailsCommentsBodyForm extends React.Component { class ProjectBoardIssueDetailsCommentsBodyForm extends React.Component {
state = { textArea: React.createRef() }; state = { textArea: React.createRef() };
@ -53,6 +45,12 @@ class ProjectBoardIssueDetailsCommentsBodyForm extends React.Component {
} }
} }
ProjectBoardIssueDetailsCommentsBodyForm.propTypes = propTypes; ProjectBoardIssueDetailsCommentsBodyForm.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
isWorking: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsCommentsBodyForm; export default ProjectBoardIssueDetailsCommentsBodyForm;

View File

@ -1,7 +1,7 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font } from '../../../../../shared/utils/styles';
import { Avatar } from 'shared/components'; import { Avatar } from '../../../../../shared/components';
export const Comment = styled.div` export const Comment = styled.div`
position: relative; position: relative;
@ -39,7 +39,8 @@ export const Body = styled.p`
white-space: pre-wrap; white-space: pre-wrap;
`; `;
const actionLinkStyles = css` export const EditLink = styled.div`
margin-right: 12px;
display: inline-block; display: inline-block;
padding: 2px 0; padding: 2px 0;
color: ${color.textMedium}; color: ${color.textMedium};
@ -51,13 +52,16 @@ const actionLinkStyles = css`
} }
`; `;
export const EditLink = styled.div`
margin-right: 12px;
${actionLinkStyles}
`;
export const DeleteLink = styled.div` export const DeleteLink = styled.div`
${actionLinkStyles} display: inline-block;
padding: 2px 0;
color: ${color.textMedium};
font-size: 14.5px
cursor: pointer;
user-select: none;
&:hover {
text-decoration: underline;
}
&:before { &:before {
position: relative; position: relative;
right: 6px; right: 6px;

View File

@ -1,27 +1,13 @@
import React, { Fragment, useState } from 'react'; import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import api from 'shared/utils/api'; import api from '../../../../../shared/utils/api';
import toast from 'shared/utils/toast'; import toast from '../../../../../shared/utils/toast';
import { formatDateTimeConversational } from 'shared/utils/dateTime'; import { formatDateTimeConversational } from '../../../../../shared/utils/dateTime';
import { ConfirmModal } from 'shared/components'; import { ConfirmModal } from '../../../../../shared/components';
import BodyForm from '../BodyForm'; import BodyForm from '../BodyForm';
import { import { Body, Comment, Content, CreatedAt, DeleteLink, EditLink, UserAvatar, Username, } from './Styles';
Comment,
UserAvatar,
Content,
Username,
CreatedAt,
Body,
EditLink,
DeleteLink,
} from './Styles';
const propTypes = {
comment: PropTypes.object.isRequired,
fetchIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => { const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
const [isFormOpen, setFormOpen] = useState(false); const [isFormOpen, setFormOpen] = useState(false);
@ -82,6 +68,9 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
); );
}; };
ProjectBoardIssueDetailsComment.propTypes = propTypes; ProjectBoardIssueDetailsComment.propTypes = {
comment: PropTypes.object.isRequired,
fetchIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsComment; export default ProjectBoardIssueDetailsComment;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color } from 'shared/utils/styles'; import { color } from '../../../../../../shared/utils/styles';
export const Tip = styled.div` export const Tip = styled.div`
display: flex; display: flex;

View File

@ -1,15 +1,11 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { KeyCodes } from 'shared/constants/keyCodes'; import { KeyCodes } from '../../../../../../shared/constants/keyCodes';
import { isFocusedElementEditable } from 'shared/utils/browser'; import { isFocusedElementEditable } from '../../../../../../shared/utils/browser';
import { Tip, TipLetter } from './Styles'; import { Tip, TipLetter } from './Styles';
const propTypes = {
setFormOpen: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => { const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
useEffect(() => { useEffect(() => {
const handleKeyDown = event => { const handleKeyDown = event => {
@ -33,6 +29,8 @@ const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
); );
}; };
ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = propTypes; ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = {
setFormOpen: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsCommentsCreateProTip; export default ProjectBoardIssueDetailsCommentsCreateProTip;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color } from '../../../../../shared/utils/styles';
import { Avatar } from 'shared/components'; import { Avatar } from '../../../../../shared/components';
export const Create = styled.div` export const Create = styled.div`
position: relative; position: relative;

View File

@ -1,13 +1,13 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import api from 'shared/utils/api'; import api from '../../../../../shared/utils/api';
import toast from 'shared/utils/toast'; import toast from '../../../../../shared/utils/toast';
import { fetchCurrentUser } from "actions/users"; import { fetchCurrentUser } from "../../../../../actions/users";
import BodyForm from '../BodyForm'; import BodyForm from '../BodyForm';
import ProTip from './ProTip'; import ProTip from './ProTip';
import { Create, FakeTextarea, Right, UserAvatar } from './Styles'; import { Create, FakeTextarea, Right, UserAvatar } from './Styles';
class ProjectBoardIssueDetailsCommentsCreate extends React.Component { class ProjectBoardIssueDetailsCommentsCreate extends React.Component {

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { font } from 'shared/utils/styles'; import { font } from '../../../../shared/utils/styles';
export const Comments = styled.div` export const Comments = styled.div`
padding-top: 40px; padding-top: 40px;

View File

@ -1,28 +1,26 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { sortByNewest } from 'shared/utils/javascript'; import { sortByNewest } from '../../../../shared/utils/javascript';
import Create from './Create'; import Create from './Create';
import Comment from './Comment'; import Comment from './Comment';
import { Comments, Title } from './Styles'; import { Comments, Title } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
fetchIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => ( const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (
<Comments> <Comments>
<Title>Comments</Title> <Title>Comments</Title>
<Create issueId={issue.id} fetchIssue={fetchIssue} /> <Create issueId={issue.id} fetchIssue={fetchIssue}/>
{sortByNewest(issue.comments, 'createdAt').map(comment => ( {sortByNewest(issue.comments, 'createdAt').map(comment => (
<Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} /> <Comment key={comment.id} comment={comment} fetchIssue={fetchIssue}/>
))} ))}
</Comments> </Comments>
); );
ProjectBoardIssueDetailsComments.propTypes = propTypes; ProjectBoardIssueDetailsComments.propTypes = {
issue: PropTypes.object.isRequired,
fetchIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsComments; export default ProjectBoardIssueDetailsComments;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color } from '../../../../shared/utils/styles';
export const Dates = styled.div` export const Dates = styled.div`
margin-top: 11px; margin-top: 11px;

View File

@ -1,14 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { formatDateTimeConversational } from 'shared/utils/dateTime'; import { formatDateTimeConversational } from '../../../../shared/utils/dateTime';
import { Dates } from './Styles'; import { Dates } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
};
const ProjectBoardIssueDetailsDates = ({ issue }) => ( const ProjectBoardIssueDetailsDates = ({ issue }) => (
<Dates> <Dates>
<div>Created at {formatDateTimeConversational(issue.createdAt)}</div> <div>Created at {formatDateTimeConversational(issue.createdAt)}</div>
@ -16,6 +12,8 @@ const ProjectBoardIssueDetailsDates = ({ issue }) => (
</Dates> </Dates>
); );
ProjectBoardIssueDetailsDates.propTypes = propTypes; ProjectBoardIssueDetailsDates.propTypes = {
issue: PropTypes.object.isRequired,
};
export default ProjectBoardIssueDetailsDates; export default ProjectBoardIssueDetailsDates;

View File

@ -1,15 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import api from 'shared/utils/api'; import api from '../../../shared/utils/api';
import toast from 'shared/utils/toast'; import toast from '../../../shared/utils/toast';
import { Button, ConfirmModal } from 'shared/components'; import { Button, ConfirmModal } from '../../../shared/components';
const propTypes = {
issue: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => { const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => {
const handleIssueDelete = async () => { const handleIssueDelete = async () => {
@ -35,6 +29,10 @@ const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) =>
); );
}; };
ProjectBoardIssueDetailsDelete.propTypes = propTypes; ProjectBoardIssueDetailsDelete.propTypes = {
issue: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsDelete; export default ProjectBoardIssueDetailsDelete;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font } from '../../../../shared/utils/styles';
export const Title = styled.div` export const Title = styled.div`
padding: 20px 0 6px; padding: 20px 0 6px;

View File

@ -1,15 +1,10 @@
import React, { Fragment, useState } from 'react'; import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getTextContentsFromHtmlString } from 'shared/utils/browser'; import { getTextContentsFromHtmlString } from '../../../../shared/utils/browser';
import { TextEditor, TextEditedContent, Button } from 'shared/components'; import { Button, TextEditedContent, TextEditor } from '../../../../shared/components';
import { Title, EmptyLabel, Actions } from './Styles'; import { Actions, EmptyLabel, Title } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => { const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
const [description, setDescription] = useState(issue.description); const [description, setDescription] = useState(issue.description);
@ -54,6 +49,9 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
); );
}; };
ProjectBoardIssueDetailsDescription.propTypes = propTypes; ProjectBoardIssueDetailsDescription.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsDescription; export default ProjectBoardIssueDetailsDescription;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font } from '../../../../shared/utils/styles';
export const TrackingLink = styled.div` export const TrackingLink = styled.div`
padding: 4px 4px 2px 0; padding: 4px 4px 2px 0;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color } from '../../../../../shared/utils/styles';
import { Icon } from 'shared/components'; import { Icon } from '../../../../../shared/components';
export const TrackingWidget = styled.div` export const TrackingWidget = styled.div`
display: flex; display: flex;

View File

@ -1,12 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { TrackingWidget, WatchIcon, Right, BarCont, Bar, Values } from './Styles'; import { Bar, BarCont, Right, TrackingWidget, Values, WatchIcon } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
};
const ProjectBoardIssueDetailsTrackingWidget = ({ issue }) => ( const ProjectBoardIssueDetailsTrackingWidget = ({ issue }) => (
<TrackingWidget> <TrackingWidget>
@ -50,6 +46,8 @@ const renderRemainingOrEstimate = ({ timeRemaining, estimate }) => {
} }
}; };
ProjectBoardIssueDetailsTrackingWidget.propTypes = propTypes; ProjectBoardIssueDetailsTrackingWidget.propTypes = {
issue: PropTypes.object.isRequired,
};
export default ProjectBoardIssueDetailsTrackingWidget; export default ProjectBoardIssueDetailsTrackingWidget;

View File

@ -1,35 +1,22 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { InputDebounced, Modal, Button } from 'shared/components'; import { Button, InputDebounced, Modal } from '../../../../shared/components';
import TrackingWidget from './TrackingWidget'; import TrackingWidget from './TrackingWidget';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { import { Actions, InputCont, InputLabel, Inputs, ModalContents, ModalTitle, TrackingLink, } from './Styles';
TrackingLink,
ModalContents,
ModalTitle,
Inputs,
InputCont,
InputLabel,
Actions,
} from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => ( const ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => (
<Fragment> <Fragment>
<SectionTitle>Original Estimate (hours)</SectionTitle> <SectionTitle>Original Estimate (hours)</SectionTitle>
{renderHourInput('estimate', issue, updateIssue)} {renderHourInput('estimate', issue, updateIssue)}
<SectionTitle>Time Tracking</SectionTitle> <SectionTitle>Time Tracking</SectionTitle>
<Modal <Modal
testid="modal:tracking" testid="modal:tracking"
width={400} width={400}
renderLink={modal => ( renderLink={modal => (
<TrackingLink onClick={modal.open}> <TrackingLink onClick={modal.open}>
<TrackingWidget issue={issue} /> <TrackingWidget issue={issue} />
@ -72,6 +59,9 @@ const renderHourInput = (fieldName, issue, updateIssue) => (
/> />
); );
ProjectBoardIssueDetailsEstimateTracking.propTypes = propTypes; ProjectBoardIssueDetailsEstimateTracking.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsEstimateTracking; export default ProjectBoardIssueDetailsEstimateTracking;

View File

@ -1,6 +1,6 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color } from '../../../../shared/utils/styles';
export const Priority = styled.div` export const Priority = styled.div`
display: flex; display: flex;

View File

@ -1,26 +1,21 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues'; import { IssuePriority, IssuePriorityCopy } from '../../../../shared/constants/issues';
import { Select, IssuePriorityIcon } from 'shared/components'; import { IssuePriorityIcon, Select } from '../../../../shared/components';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { Priority, Label } from './Styles'; import { Label, Priority } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => ( const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => (
<Fragment> <Fragment>
<SectionTitle>Priority</SectionTitle> <SectionTitle>Priority</SectionTitle>
<Select <Select
variant="empty" variant="empty"
withClearValue={false} withClearValue={false}
dropdownWidth={343} dropdownWidth={343}
name="priority" name="priority"
value={issue.priority} value={issue.priority}
options={Object.values(IssuePriority).map(priority => ({ options={Object.values(IssuePriority).map(priority => ({
value: priority, value: priority,
label: IssuePriorityCopy[priority], label: IssuePriorityCopy[priority],
@ -39,6 +34,9 @@ const renderPriorityItem = (priority, isValue) => (
</Priority> </Priority>
); );
ProjectBoardIssueDetailsPriority.propTypes = propTypes; ProjectBoardIssueDetailsPriority.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsPriority; export default ProjectBoardIssueDetailsPriority;

View File

@ -1,11 +1,28 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { issueStatusColors, issueStatusBackgroundColors, mixin } from 'shared/utils/styles'; import { issueStatusBackgroundColors, issueStatusColors } from '../../../../shared/utils/styles';
export const Status = styled.div` export const Status = styled.div`
text-transform: uppercase; text-transform: uppercase;
transition: all 0.1s; transition: all 0.1s;
${props => mixin.tag(issueStatusBackgroundColors[props.color], issueStatusColors[props.color])} ${props =>
css`
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
border-radius: 4px;
cursor: pointer;
user-select: none;
color: ${issueStatusColors[props.color]};
background: ${issueStatusBackgroundColors[props.color]};
font-family: "CircularStdBold"; font-weight: normal
font-size: 12px
i {
margin-left: 4px;
}
`
}
${props => ${props =>
props.isValue && props.isValue &&
css` css`

View File

@ -1,23 +1,18 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues'; import { IssueStatus, IssueStatusCopy } from '../../../../shared/constants/issues';
import { Select, Icon } from 'shared/components'; import { Icon, Select } from '../../../../shared/components';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { Status } from './Styles'; import { Status } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => ( const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
<Fragment> <Fragment>
<SectionTitle>Status</SectionTitle> <SectionTitle>Status</SectionTitle>
<Select <Select
variant="empty" variant="empty"
dropdownWidth={343} dropdownWidth={343}
withClearValue={false} withClearValue={false}
name="status" name="status"
value={issue.status} value={issue.status}
@ -39,6 +34,9 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
</Fragment> </Fragment>
); );
ProjectBoardIssueDetailsStatus.propTypes = propTypes; ProjectBoardIssueDetailsStatus.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsStatus; export default ProjectBoardIssueDetailsStatus;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color } from 'shared/utils/styles'; import { color } from '../../../shared/utils/styles';
export const Content = styled.div` export const Content = styled.div`
display: flex; display: flex;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color, font } from '../../../../shared/utils/styles';
import { Textarea } from 'shared/components'; import { Textarea } from '../../../../shared/components';
export const TitleTextarea = styled(Textarea)` export const TitleTextarea = styled(Textarea)`
margin: 18px 0 0 -8px; margin: 18px 0 0 -8px;

View File

@ -1,15 +1,10 @@
import React, { Fragment, useRef, useState } from 'react'; import React, { Fragment, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { KeyCodes } from 'shared/constants/keyCodes'; import { KeyCodes } from '../../../../shared/constants/keyCodes';
import { is, generateErrors } from 'shared/utils/validation'; import { generateErrors, is } from '../../../../shared/utils/validation';
import { TitleTextarea, ErrorText } from './Styles'; import { ErrorText, TitleTextarea } from './Styles';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => { const ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => {
const $titleInputRef = useRef(); const $titleInputRef = useRef();
@ -49,6 +44,9 @@ const ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => {
); );
}; };
ProjectBoardIssueDetailsTitle.propTypes = propTypes; ProjectBoardIssueDetailsTitle.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsTitle; export default ProjectBoardIssueDetailsTitle;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color } from '../../../../shared/utils/styles';
import { Button } from 'shared/components'; import { Button } from '../../../../shared/components';
export const TypeButton = styled(Button)` export const TypeButton = styled(Button)`
text-transform: uppercase; text-transform: uppercase;

View File

@ -1,38 +1,30 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import api from 'shared/utils/api'; import api from '../../../shared/utils/api';
import useApi from 'shared/hooks/api'; import useApi from '../../../shared/hooks/api';
import { PageError, CopyLinkButton, Button, AboutTooltip } from 'shared/components'; import { AboutTooltip, Button, CopyLinkButton, PageError } from '../../../shared/components';
import Loader from './Loader'; import Loader from './Loader';
import Type from './Type'; import Type from './Type';
import Delete from './Delete'; import Delete from './Delete';
import Title from './Title'; import Title from './Title';
import Description from './Description'; import Description from './Description';
import Comments from './Comments'; import Comments from './Comments';
import Status from './Status'; import Status from './Status';
import AssigneesReporter from './AssigneesReporter'; import AssigneesReporter from './AssigneesReporter';
import Priority from './Priority'; import Priority from './Priority';
import EstimateTracking from './EstimateTracking'; import EstimateTracking from './EstimateTracking';
import Dates from './Dates'; import Dates from './Dates';
import { TopActions, TopActionsRight, Content, Left, Right } from './Styles'; import { Content, Left, Right, TopActions, TopActionsRight } from './Styles';
const propTypes = {
issueId: PropTypes.string.isRequired,
projectUsers: PropTypes.array.isRequired,
fetchProject: PropTypes.func.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
const ProjectBoardIssueDetails = ({ const ProjectBoardIssueDetails = ({
issueId, issueId,
projectUsers, projectUsers,
fetchProject, fetchProject,
updateLocalProjectIssues, updateLocalProjectIssues,
modalClose, modalClose,
}) => { }) => {
const [{ data, error, setLocalData }, fetchIssue] = useApi.get(`/issues/${issueId}`); const [{ data, error, setLocalData }, fetchIssue] = useApi.get(`/issues/${issueId}`);
if (!data) return <Loader />; if (!data) return <Loader />;
@ -89,6 +81,12 @@ const ProjectBoardIssueDetails = ({
); );
}; };
ProjectBoardIssueDetails.propTypes = propTypes; ProjectBoardIssueDetails.propTypes = {
issueId: PropTypes.string.isRequired,
projectUsers: PropTypes.array.isRequired,
fetchProject: PropTypes.func.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetails; export default ProjectBoardIssueDetails;

View File

@ -1,8 +1,8 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { color, font, mixin } from 'shared/utils/styles'; import { color } from '../../../../../shared/utils/styles';
import { Avatar } from 'shared/components'; import { Avatar } from '../../../../../shared/components';
export const IssueLink = styled(Link)` export const IssueLink = styled(Link)`
display: block; display: block;

View File

@ -1,18 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { IssuePriorityIcon, IssueTypeIcon } from '../../../../../shared/components'; import { IssuePriorityIcon, IssueTypeIcon } from '../../../../../shared/components';
import { AssigneeAvatar, Assignees, Bottom, Issue, IssueLink, Title } from './Styles'; import { AssigneeAvatar, Assignees, Bottom, Issue, IssueLink, Title } from './Styles';
const propTypes = {
projectUsers: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
};
const ProjectBoardListIssue = ({ projectUsers, issue, index }) => { const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
const match = useRouteMatch(); const match = useRouteMatch();
@ -54,6 +48,10 @@ const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
); );
}; };
ProjectBoardListIssue.propTypes = propTypes; ProjectBoardListIssue.propTypes = {
projectUsers: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
};
export default ProjectBoardListIssue; export default ProjectBoardListIssue;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color } from 'shared/utils/styles'; import { color } from '../../../../shared/utils/styles';
export const List = styled.div` export const List = styled.div`
display: flex; display: flex;

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { Droppable } from 'react-beautiful-dnd'; import { Droppable } from 'react-beautiful-dnd';
import { intersection } from 'lodash'; import { intersection } from 'lodash';
import { IssueStatusCopy } from 'shared/constants/issues'; import { IssueStatusCopy } from '../../../../shared/constants/issues';
import Issue from './Issue'; import Issue from './Issue';
import { Issues, IssuesCount, List, Title } from './Styles'; import { Issues, IssuesCount, List, Title } from './Styles';
const ProjectBoardList = ({ status, project, filters, currentUserId }) => { const ProjectBoardList = ({ status, project, filters, currentUserId }) => {

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import useCurrentUser from 'shared/hooks/currentUser'; import useCurrentUser from '../../../shared/hooks/currentUser';
import api from 'shared/utils/api'; import api from '../../../shared/utils/api';
import { insertItemIntoArray, moveItemWithinArray } from 'shared/utils/javascript'; import { insertItemIntoArray, moveItemWithinArray } from '../../../shared/utils/javascript';
import { IssueStatus } from 'shared/constants/issues'; import { IssueStatus } from '../../../shared/constants/issues';
import List from './List'; import List from './List';
import { Lists } from './Styles'; import { Lists } from './Styles';
const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => { const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {

View File

@ -1,20 +1,20 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Route, useHistory, useRouteMatch } from 'react-router-dom'; import { Route, useHistory, useRouteMatch } from 'react-router-dom';
import useMergeState from 'shared/hooks/mergeState'; import useMergeState from '../../shared/hooks/mergeState';
import { Breadcrumbs, Modal } from 'shared/components'; import { Breadcrumbs, Modal } from '../../shared/components';
import Header from './Header'; import Header from './Header';
import Filters from './Filters'; import Filters from './Filters';
import Lists from './Lists'; import Lists from './Lists';
import IssueDetails from './IssueDetails'; import IssueDetails from './IssueDetails';
const defaultFilters = { const defaultFilters = {
searchTerm: '', searchTerm: '',
userIds: [], userIds: [],
myOnly: false, myOnly: false,
recent: false, recent: false,
}; };
const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => { const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => {

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color } from '../../shared/utils/styles';
import { Button } from 'shared/components'; import { Button } from '../../shared/components';
export const SignIn = styled.article` export const SignIn = styled.article`
margin: 24px auto; margin: 24px auto;
@ -30,7 +30,7 @@ export const FormElement = styled.div`
export const FormHeading = styled.div` export const FormHeading = styled.div`
padding-bottom: 15px; padding-bottom: 15px;
${ font.size(21) } font-size: 21px;
`; `;
export const SelectItem = styled.div` export const SelectItem = styled.div`

View File

@ -1,20 +1,26 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { IssuePriority, IssuePriorityCopy, IssueStatus, IssueType, IssueTypeCopy, } from 'shared/constants/issues'; import {
import toast from 'shared/utils/toast'; IssuePriority,
import api from 'shared/utils/api'; IssuePriorityCopy,
import { Avatar, Form, Icon, IssuePriorityIcon, IssueTypeIcon } from 'shared/components'; IssueStatus,
IssueType,
IssueTypeCopy,
} from '../../shared/constants/issues';
import toast from '../../shared/utils/toast';
import api from '../../shared/utils/api';
import { Avatar, Form, Icon, IssuePriorityIcon, IssueTypeIcon } from '../../shared/components';
import { ActionButton, Actions, Divider, FormElement, FormHeading, SelectItem, SelectItemLabel, } from './Styles'; import { ActionButton, Actions, Divider, FormElement, FormHeading, SelectItem, SelectItemLabel, } from './Styles';
class ProjectIssueCreate extends React.Component { class ProjectIssueCreate extends React.Component {
state = { state = {
isCreating: false, form: { isCreating: false, form: {
type: IssueType.TASK, type: IssueType.TASK,
title: '', title: '',
description: '', description: '',
reporterId: null, reporterId: null,
userIds: [], userIds: [],
priority: IssuePriority.MEDIUM, priority: IssuePriority.MEDIUM,
} }

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color, font } from '../../shared/utils/styles';
import { Icon, InputDebounced, Spinner } from 'shared/components'; import { Icon, InputDebounced, Spinner } from '../../shared/components';
export const IssueSearch = styled.div` export const IssueSearch = styled.div`
padding: 25px 35px 60px; padding: 25px 35px 60px;

View File

@ -1,32 +1,28 @@
import React, { Fragment, useState } from 'react'; import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { get } from 'lodash'; import { get } from 'lodash';
import useApi from 'shared/hooks/api'; import useApi from '../../shared/hooks/api';
import { sortByNewest } from 'shared/utils/javascript'; import { sortByNewest } from '../../shared/utils/javascript';
import { IssueTypeIcon } from 'shared/components'; import { IssueTypeIcon } from '../../shared/components';
import NoResultsSVG from './NoResultsSvg'; import NoResultsSVG from './NoResultsSvg';
import { import {
IssueSearch,
SearchInputCont,
SearchInputDebounced,
SearchIcon,
SearchSpinner,
Issue, Issue,
IssueData, IssueData,
IssueSearch,
IssueTitle, IssueTitle,
IssueTypeId, IssueTypeId,
SectionTitle,
NoResults, NoResults,
NoResultsTitle,
NoResultsTip, NoResultsTip,
} from './Styles'; NoResultsTitle,
SearchIcon,
const propTypes = { SearchInputCont,
project: PropTypes.object.isRequired, SearchInputDebounced,
}; SearchSpinner,
SectionTitle,
} from './Styles';
const ProjectIssueSearch = ({ project }) => { const ProjectIssueSearch = ({ project }) => {
const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true); const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true);
@ -96,6 +92,8 @@ const renderIssue = issue => (
</Link> </Link>
); );
ProjectIssueSearch.propTypes = propTypes; ProjectIssueSearch.propTypes = {
project: PropTypes.object.isRequired,
};
export default ProjectIssueSearch; export default ProjectIssueSearch;

View File

@ -1,8 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { color, mixin, sizes, zIndexValues } from 'shared/utils/styles'; import { color, sizes, zIndexValues } from '../../shared/utils/styles';
import { Logo } from 'shared/components'; import { Logo } from '../../shared/components';
export const NavLeft = styled.aside` export const NavLeft = styled.aside`
z-index: ${zIndexValues.navLeft}; z-index: ${zIndexValues.navLeft};
@ -14,7 +14,7 @@ export const NavLeft = styled.aside`
width: ${sizes.appNavBarLeftWidth}px; width: ${sizes.appNavBarLeftWidth}px;
background: ${color.backgroundDarkPrimary}; background: ${color.backgroundDarkPrimary};
transition: all 0.1s; transition: all 0.1s;
${mixin.hardwareAccelerate} transform: translateZ(0);
&:hover { &:hover {
width: 200px; width: 200px;
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6); box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);

View File

@ -1,15 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { AboutTooltip, Icon } from 'shared/components'; import { AboutTooltip, Icon } from '../../shared/components';
import { Bottom, Item, ItemText, LogoLink, NavLeft, StyledLogo } from './Styles'; import { Bottom, Item, ItemText, LogoLink, NavLeft, StyledLogo } from './Styles';
const propTypes = {
issueSearchModalOpen: PropTypes.func.isRequired,
issueCreateModalOpen: PropTypes.func.isRequired,
};
const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => ( const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
<NavLeft class={ 'NavLeft' }> <NavLeft class={ 'NavLeft' }>
<LogoLink to="/"> <LogoLink to="/">
@ -41,6 +36,9 @@ const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
</NavLeft> </NavLeft>
); );
ProjectNavbarLeft.propTypes = propTypes; ProjectNavbarLeft.propTypes = {
issueSearchModalOpen: PropTypes.func.isRequired,
issueCreateModalOpen: PropTypes.func.isRequired,
};
export default ProjectNavbarLeft; export default ProjectNavbarLeft;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { font } from 'shared/utils/styles'; import { font } from '../../shared/utils/styles';
import { Button, Form } from 'shared/components'; import { Button, Form } from '../../shared/components';
export const FormCont = styled.div` export const FormCont = styled.div`
display: flex; display: flex;
@ -16,7 +16,7 @@ export const FormElement = styled(Form.Element)`
export const FormHeading = styled.h1` export const FormHeading = styled.h1`
padding: 6px 0 15px; padding: 6px 0 15px;
font-size: 24px font-size: 24px
${font.medium} ${font.medium}; font-weight: normal;
`; `;
export const ActionButton = styled(Button)` export const ActionButton = styled(Button)`

View File

@ -1,21 +1,28 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from "react-redux"; import { connect } from "react-redux";
import { ProjectCategory, ProjectCategoryCopy } from 'shared/constants/projects'; import { ProjectCategory, ProjectCategoryCopy } from '../../shared/constants/projects';
import toast from 'shared/utils/toast'; import toast from '../../shared/utils/toast';
import api from 'shared/utils/api'; import api from '../../shared/utils/api';
import { Breadcrumbs, Form } from 'shared/components'; import {
import { updateProjectFormFieldChanged, updateProjectFormRequest, updateProjectFormSuccess, } from 'actions/forms'; Breadcrumbs,
Form
} from '../../shared/components';
import {
updateProjectFormFieldChanged,
updateProjectFormRequest,
updateProjectFormSuccess,
} from '../../actions/forms';
import { ActionButton, FormCont, FormElement, FormHeading } from './Styles'; import { ActionButton, FormCont, FormElement, FormHeading } from './Styles';
class ProjectSettings extends React.Component { class ProjectSettings extends React.Component {
state = { state = {
isUpdating: false, form: { isUpdating: false, form: {
name: '', name: '',
url: '', url: '',
category: '', category: '',
description: '', description: '',
} }
}; };

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin, sizes, zIndexValues } from 'shared/utils/styles'; import { color, font, sizes, zIndexValues } from '../../shared/utils/styles';
export const Sidebar = styled.div` export const Sidebar = styled.div`
position: fixed; position: fixed;
@ -15,7 +15,17 @@ export const Sidebar = styled.div`
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
${mixin.customScrollbar()} &::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: none;
}
&::-webkit-scrollbar-thumb {
border-radius: 99px;
background: ${color.backgroundMedium};
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
width: ${sizes.secondarySideBarWidth - 10}px; width: ${sizes.secondarySideBarWidth - 10}px;
} }

View File

@ -1,26 +1,22 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { NavLink, useRouteMatch } from 'react-router-dom'; import { NavLink, useRouteMatch } from 'react-router-dom';
import { ProjectCategoryCopy } from 'shared/constants/projects'; import { ProjectCategoryCopy } from '../../shared/constants/projects';
import { Icon, ProjectAvatar } from 'shared/components'; import { Icon, ProjectAvatar } from '../../shared/components';
import { import {
Sidebar,
ProjectInfo,
ProjectTexts,
ProjectName,
ProjectCategory,
Divider, Divider,
LinkItem, LinkItem,
LinkText, LinkText,
NotImplemented, NotImplemented,
ProjectCategory,
ProjectInfo,
ProjectName,
ProjectTexts,
Sidebar,
} from './Styles'; } from './Styles';
const propTypes = {
project: PropTypes.object.isRequired,
};
const ProjectSidebar = ({ project }) => { const ProjectSidebar = ({ project }) => {
const match = useRouteMatch(); const match = useRouteMatch();
@ -62,6 +58,8 @@ const renderLinkItem = (match, text, iconType, path) => {
); );
}; };
ProjectSidebar.propTypes = propTypes; ProjectSidebar.propTypes = {
project: PropTypes.object.isRequired,
};
export default ProjectSidebar; export default ProjectSidebar;

View File

@ -1,25 +1,25 @@
import React from 'react'; import React from 'react';
import { Redirect, Route, useHistory, useRouteMatch } from 'react-router-dom'; import { Redirect, Route, useHistory, useRouteMatch } from 'react-router-dom';
import useApi from 'shared/hooks/api'; import useApi from '../shared/hooks/api';
import { updateArrayItemById } from 'shared/utils/javascript'; import { updateArrayItemById } from '../shared/utils/javascript';
import { createQueryParamModalHelpers } from 'shared/utils/queryParamModal'; import { createQueryParamModalHelpers } from '../shared/utils/queryParamModal';
import { Modal, PageError, PageLoader } from 'shared/components'; import { Modal, PageError, PageLoader } from '../shared/components';
import NavbarLeft from './NavbarLeft'; import NavbarLeft from './NavbarLeft';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import Board from './Board'; import Board from './Board';
import IssueSearch from './IssueSearch'; import IssueSearch from './IssueSearch';
import IssueCreate from './IssueCreate'; import IssueCreate from './IssueCreate';
import ProjectSettings from './ProjectSettings'; import ProjectSettings from './ProjectSettings';
import { ProjectPage } from './Styles'; import { ProjectPage } from './Styles';
const Project = () => { const Project = () => {
const match = useRouteMatch(); const match = useRouteMatch();
const history = useHistory(); const history = useHistory();
const issueSearchModalHelpers = createQueryParamModalHelpers('issue-search'); const issueSearchModalHelpers = createQueryParamModalHelpers('issue-search');
const issueCreateModalHelpers = createQueryParamModalHelpers('issue-create'); const issueCreateModalHelpers = createQueryParamModalHelpers('issue-create');
const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project'); const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');

View File

@ -1,16 +1,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { copyToClipboard } from 'shared/utils/browser'; import { copyToClipboard } from '../../shared/utils/browser';
import { Button } from 'shared/components'; import { Button } from '../../shared/components';
const CopyLinkButton = ({ ...buttonProps }) => { const CopyLinkButton = ({ ...buttonProps }) => {
const [isLinkCopied, setLinkCopied] = useState(false); const [isLinkCopied, setLinkCopied] = useState(false);
const handleLinkCopy = () => { const handleLinkCopy = () => {
setLinkCopied(true); setLinkCopied(true);
setTimeout(() => setLinkCopied(false), 2000); setTimeout(() => setLinkCopied(false), 2000);
copyToClipboard(window.location.href); copyToClipboard(window.location.href);
}; };
return ( return (
<Button icon="link" onClick={handleLinkCopy} {...buttonProps}> <Button icon="link" onClick={handleLinkCopy} {...buttonProps}>

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { zIndexValues, mixin } from 'shared/utils/styles'; import { mixin, zIndexValues } from 'shared/utils/styles';
export const StyledTooltip = styled.div` export const StyledTooltip = styled.div`
z-index: ${zIndexValues.modal + 1}; z-index: ${zIndexValues.modal + 1};
@ -8,6 +8,6 @@ export const StyledTooltip = styled.div`
width: ${props => props.width}px; width: ${props => props.width}px;
border-radius: 3px; border-radius: 3px;
background: #fff; background: #fff;
${mixin.hardwareAccelerate} transform: translateZ(0);
${mixin.boxShadowDropdown} ${mixin.boxShadowDropdown}
`; `;