Add copy link command
This commit is contained in:
parent
5e81329378
commit
f778f5a5c5
@ -31,5 +31,8 @@ features = [
|
||||
"DataTransfer",
|
||||
"DragEvent",
|
||||
"HtmlDivElement",
|
||||
"DomRect"
|
||||
"DomRect",
|
||||
"HtmlDocument",
|
||||
"Document",
|
||||
"Selection"
|
||||
]
|
||||
|
@ -30,7 +30,7 @@ pub async fn update_issue(
|
||||
.method(Method::Put)
|
||||
.header("Content-Type", "application/json")
|
||||
.body_json(&payload)
|
||||
.fetch_json(Msg::IssueUpdateResult)
|
||||
.fetch_string(Msg::IssueUpdateResult)
|
||||
.await
|
||||
}
|
||||
Err(e) => return Ok(Msg::InternalFailure(e)),
|
||||
|
@ -1,7 +1,7 @@
|
||||
use seed::fetch::{FetchObject, ResponseWithDataResult};
|
||||
use seed::*;
|
||||
|
||||
use jirs_data::{FullProjectResponse, Issue};
|
||||
use jirs_data::{FullIssue, FullProject, Issue};
|
||||
|
||||
use crate::model::Model;
|
||||
|
||||
@ -42,9 +42,9 @@ pub fn current_project_response(fetched: &FetchObject<String>, model: &mut Model
|
||||
if status.is_error() {
|
||||
return;
|
||||
}
|
||||
match serde_json::from_str::<'_, FullProjectResponse>(body.as_str()) {
|
||||
Ok(project_response) => {
|
||||
model.project = Some(project_response.project);
|
||||
match serde_json::from_str::<'_, FullProject>(body.as_str()) {
|
||||
Ok(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) {
|
||||
log!("update_issue_response");
|
||||
log!(fetched);
|
||||
if let FetchObject {
|
||||
result:
|
||||
Ok(ResponseWithDataResult {
|
||||
@ -68,18 +66,21 @@ pub fn update_issue_response(fetched: &FetchObject<String>, model: &mut Model) {
|
||||
return;
|
||||
}
|
||||
match (
|
||||
serde_json::from_str::<'_, Issue>(body.as_str()),
|
||||
serde_json::from_str::<'_, FullIssue>(body.as_str()),
|
||||
model.project.as_mut(),
|
||||
) {
|
||||
(Ok(issue), Some(project)) => {
|
||||
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 {
|
||||
issues.push(i.clone());
|
||||
project.issues.push(i);
|
||||
}
|
||||
}
|
||||
issues.push(issue);
|
||||
project.issues = issues;
|
||||
project.issues.push(issue.into());
|
||||
}
|
||||
(Err(error), _) => {
|
||||
error!(error);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ pub struct EditIssueModal {
|
||||
pub top_select_opened: bool,
|
||||
pub top_select_filter: String,
|
||||
pub value: IssueType,
|
||||
pub link_copied: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)]
|
||||
|
@ -2,6 +2,7 @@ use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::update_issue;
|
||||
use crate::model::{EditIssueModal, Icon, ModalType, Model, Page};
|
||||
use crate::shared::modal::{Modal, Variant as ModalVariant};
|
||||
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_filter: "".to_string(),
|
||||
value,
|
||||
link_copied: false,
|
||||
},
|
||||
));
|
||||
}
|
||||
@ -97,14 +99,18 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(issue) = found {
|
||||
let issue = match found {
|
||||
Some(i) => i,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
issue.status = status.clone();
|
||||
issue.list_position = position + 1f64;
|
||||
|
||||
let payload = UpdateIssuePayload {
|
||||
title: Some(issue.title.clone()),
|
||||
issue_type: Some(issue.issue_type.clone()),
|
||||
status: Some(status.to_payload().to_string()),
|
||||
status: Some(status),
|
||||
priority: Some(issue.priority.clone()),
|
||||
list_position: Some(issue.list_position),
|
||||
description: Some(issue.description.clone()),
|
||||
@ -113,16 +119,15 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
time_spent: Some(issue.time_spent),
|
||||
time_remaining: Some(issue.time_remaining),
|
||||
project_id: Some(issue.project_id),
|
||||
users: Some(vec![]),
|
||||
user_ids: Some(issue.user_ids.clone()),
|
||||
};
|
||||
model.project_page.dragged_issue_id = None;
|
||||
orders.skip().perform_cmd(crate::api::update_issue(
|
||||
model.host_url.clone(),
|
||||
issue.id,
|
||||
payload,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => 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;
|
||||
}
|
||||
(StyledSelectChange::Changed(value), Some(ModalType::EditIssue(_, modal))) => {
|
||||
(
|
||||
StyledSelectChange::Changed(value),
|
||||
Some(ModalType::EditIssue(issue_id, modal)),
|
||||
) => {
|
||||
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);
|
||||
|
||||
a![
|
||||
drag_started,
|
||||
attrs![At::Class => "issueLink"; At::Href => href],
|
||||
div![
|
||||
attrs![At::Class => class_list.join(" "), At::Draggable => true],
|
||||
drag_started,
|
||||
drag_stopped,
|
||||
p![attrs![At::Class => "title"], issue.title,],
|
||||
div![
|
||||
@ -492,12 +535,48 @@ fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> 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![
|
||||
attrs![At::Class => "issueDetails"],
|
||||
div![
|
||||
attrs![At::Class => "topActions"],
|
||||
issue_type_select,
|
||||
div![attrs![At::Class => "topActionsRight"]],
|
||||
div![attrs![At::Class => "topActionsRight"], copy_button],
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "content"],
|
||||
|
@ -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> {
|
||||
content.add_class("value");
|
||||
content
|
||||
|
@ -12,12 +12,6 @@ pub use sql::*;
|
||||
#[cfg(feature = "backend")]
|
||||
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", sql_type = "IssueTypeType")]
|
||||
#[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)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum IssueStatus {
|
||||
Backlog,
|
||||
Selected,
|
||||
@ -196,20 +193,6 @@ pub struct FullProject {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullIssue {
|
||||
@ -217,7 +200,7 @@ pub struct FullIssue {
|
||||
pub title: String,
|
||||
#[serde(rename = "type")]
|
||||
pub issue_type: IssueType,
|
||||
pub status: String,
|
||||
pub status: IssueStatus,
|
||||
pub priority: IssuePriority,
|
||||
pub list_position: f64,
|
||||
pub description: Option<String>,
|
||||
@ -234,17 +217,26 @@ pub struct FullIssue {
|
||||
pub comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullIssueResponse {
|
||||
pub issue: FullIssue,
|
||||
}
|
||||
|
||||
impl ResponseData for FullIssue {
|
||||
type Response = FullIssueResponse;
|
||||
|
||||
fn into_response(self) -> Self::Response {
|
||||
FullIssueResponse { issue: self }
|
||||
impl Into<Issue> for FullIssue {
|
||||
fn into(self) -> Issue {
|
||||
Issue {
|
||||
id: self.id,
|
||||
title: self.title,
|
||||
issue_type: self.issue_type,
|
||||
status: self.status,
|
||||
priority: self.priority,
|
||||
list_position: self.list_position,
|
||||
description: self.description,
|
||||
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>,
|
||||
#[serde(rename = "type")]
|
||||
pub issue_type: Option<IssueType>,
|
||||
pub status: Option<String>,
|
||||
pub status: Option<IssueStatus>,
|
||||
pub priority: Option<IssuePriority>,
|
||||
pub list_position: Option<f64>,
|
||||
pub description: Option<Option<String>>,
|
||||
@ -334,8 +326,6 @@ pub struct UpdateIssuePayload {
|
||||
pub time_spent: Option<Option<i32>>,
|
||||
pub time_remaining: Option<Option<i32>>,
|
||||
pub project_id: Option<i32>,
|
||||
|
||||
pub users: Option<Vec<User>>,
|
||||
pub user_ids: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
@ -359,7 +349,7 @@ pub struct CreateIssuePayload {
|
||||
pub title: String,
|
||||
#[serde(rename = "type")]
|
||||
pub issue_type: IssueType,
|
||||
pub status: String,
|
||||
pub status: IssueStatus,
|
||||
pub priority: IssuePriority,
|
||||
pub description: Option<String>,
|
||||
pub description_text: Option<String>,
|
||||
|
@ -2,7 +2,7 @@ use std::io::Write;
|
||||
|
||||
use diesel::{deserialize::*, pg::*, serialize::*, *};
|
||||
|
||||
use crate::{IssuePriority, IssueType};
|
||||
use crate::{IssuePriority, IssueStatus, IssueType};
|
||||
|
||||
#[derive(SqlType)]
|
||||
#[postgres(type_name = "IssuePriorityType")]
|
||||
@ -79,3 +79,45 @@ impl ToSql<IssueTypeType, Pg> for IssueType {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
DROP EXTENSION IF EXISTS "uuid-ossp";
|
||||
|
@ -0,0 +1 @@
|
||||
CREATE EXTENSION "uuid-ossp";
|
@ -0,0 +1 @@
|
||||
DROP TYPE IF EXISTS "ProjectCategoryType" CASCADE;
|
@ -0,0 +1,6 @@
|
||||
DROP TYPE IF EXISTS "ProjectCategoryType" CASCADE;
|
||||
CREATE TYPE "ProjectCategoryType" as ENUM (
|
||||
'software',
|
||||
'marketing',
|
||||
'business'
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TYPE IF EXISTS "IssuePriorityType" CASCADE;
|
@ -0,0 +1,8 @@
|
||||
DROP TYPE IF EXISTS "IssuePriorityType" CASCADE;
|
||||
CREATE TYPE "IssuePriorityType" as ENUM (
|
||||
'highest',
|
||||
'high',
|
||||
'medium',
|
||||
'low',
|
||||
'lowest'
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;
|
@ -0,0 +1,6 @@
|
||||
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;
|
||||
CREATE TYPE "IssueTypeType" AS ENUM (
|
||||
'task',
|
||||
'bug',
|
||||
'story'
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TYPE IF EXISTS "IssueStatusType" CASCADE;
|
@ -0,0 +1,7 @@
|
||||
DROP TYPE IF EXISTS "IssueStatusType" CASCADE;
|
||||
CREATE TYPE "IssueStatusType" AS ENUM (
|
||||
'backlog',
|
||||
'selected',
|
||||
'in_progress',
|
||||
'done'
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS projects CASCADE;
|
@ -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()
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
@ -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()
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS issues CASCADE;
|
@ -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()
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS comments CASCADE;
|
@ -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()
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS tokens CASCADE;
|
@ -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()
|
||||
);
|
@ -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";
|
||||
|
@ -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()
|
||||
);
|
@ -4,12 +4,14 @@ use diesel::expression::sql_literal::sql;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use jirs_data::{IssuePriority, IssueType};
|
||||
use jirs_data::{IssuePriority, IssueStatus, IssueType};
|
||||
|
||||
use crate::db::DbExecutor;
|
||||
use crate::errors::ServiceErrors;
|
||||
use crate::models::Issue;
|
||||
|
||||
const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LoadIssue {
|
||||
pub issue_id: i32,
|
||||
@ -69,7 +71,7 @@ pub struct UpdateIssue {
|
||||
pub issue_id: i32,
|
||||
pub title: Option<String>,
|
||||
pub issue_type: Option<IssueType>,
|
||||
pub status: Option<String>,
|
||||
pub status: Option<IssueStatus>,
|
||||
pub priority: Option<IssuePriority>,
|
||||
pub list_position: Option<f64>,
|
||||
pub description: Option<Option<String>>,
|
||||
@ -78,8 +80,6 @@ pub struct UpdateIssue {
|
||||
pub time_spent: Option<Option<i32>>,
|
||||
pub time_remaining: Option<Option<i32>>,
|
||||
pub project_id: Option<i32>,
|
||||
|
||||
pub users: Option<Vec<jirs_data::User>>,
|
||||
pub user_ids: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
@ -121,9 +121,9 @@ impl Handler<UpdateIssue> for DbExecutor {
|
||||
dsl::updated_at.eq(chrono::Utc::now().naive_utc()),
|
||||
));
|
||||
diesel::debug_query::<diesel::pg::Pg, _>(&chain);
|
||||
chain
|
||||
.get_result::<Issue>(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
chain.get_result::<Issue>(conn).map_err(|_| {
|
||||
ServiceErrors::DatabaseQueryFailed("Failed to update issue".to_string())
|
||||
})?;
|
||||
|
||||
if let Some(user_ids) = msg.user_ids.as_ref() {
|
||||
use crate::schema::issue_assignees::dsl;
|
||||
@ -148,7 +148,9 @@ impl Handler<UpdateIssue> for DbExecutor {
|
||||
diesel::insert_into(dsl::issue_assignees)
|
||||
.values(values)
|
||||
.execute(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
.map_err(|_| {
|
||||
ServiceErrors::DatabaseQueryFailed(FAILED_CONNECT_USER_AND_ISSUE.to_string())
|
||||
})?;
|
||||
}
|
||||
|
||||
let row = issues
|
||||
@ -194,7 +196,7 @@ impl Handler<DeleteIssue> for DbExecutor {
|
||||
pub struct CreateIssue {
|
||||
pub title: String,
|
||||
pub issue_type: IssueType,
|
||||
pub status: String,
|
||||
pub status: IssueStatus,
|
||||
pub priority: IssuePriority,
|
||||
pub description: Option<String>,
|
||||
pub description_text: Option<String>,
|
||||
@ -223,7 +225,7 @@ impl Handler<CreateIssue> for DbExecutor {
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
|
||||
let list_position = issues
|
||||
.filter(status.eq("backlog"))
|
||||
.filter(status.eq(IssueStatus::Backlog))
|
||||
.select(sql("max(list_position) + 1.0"))
|
||||
.get_result::<f64>(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
|
@ -1,4 +1,5 @@
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
use jirs_data::ErrorResponse;
|
||||
|
||||
const TOKEN_NOT_FOUND: &str = "Token not found";
|
||||
@ -7,6 +8,7 @@ const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
|
||||
pub enum ServiceErrors {
|
||||
Unauthorized,
|
||||
DatabaseConnectionLost,
|
||||
DatabaseQueryFailed(String),
|
||||
RecordNotFound(String),
|
||||
}
|
||||
|
||||
@ -27,6 +29,11 @@ impl Into<HttpResponse> for ServiceErrors {
|
||||
errors: vec![DATABASE_CONNECTION_FAILED.to_owned()],
|
||||
})
|
||||
}
|
||||
ServiceErrors::DatabaseQueryFailed(error) => {
|
||||
HttpResponse::BadRequest().json(ErrorResponse {
|
||||
errors: vec![error.to_owned()],
|
||||
})
|
||||
}
|
||||
ServiceErrors::RecordNotFound(resource_name) => {
|
||||
HttpResponse::BadRequest().json(ErrorResponse {
|
||||
errors: vec![format!("Resource not found {}", resource_name)],
|
||||
|
@ -49,7 +49,7 @@ pub struct Issue {
|
||||
pub title: String,
|
||||
#[serde(rename = "type")]
|
||||
pub issue_type: IssueType,
|
||||
pub status: String,
|
||||
pub status: IssueStatus,
|
||||
pub priority: IssuePriority,
|
||||
pub list_position: f64,
|
||||
pub description: Option<String>,
|
||||
@ -69,11 +69,7 @@ impl Into<jirs_data::Issue> for Issue {
|
||||
id: self.id,
|
||||
title: self.title,
|
||||
issue_type: self.issue_type,
|
||||
status: self
|
||||
.status
|
||||
.as_str()
|
||||
.parse::<IssueStatus>()
|
||||
.unwrap_or_else(|_| IssueStatus::Backlog),
|
||||
status: self.status,
|
||||
priority: self.priority,
|
||||
list_position: self.list_position,
|
||||
description: self.description,
|
||||
@ -123,7 +119,7 @@ pub struct CreateIssueForm {
|
||||
pub title: String,
|
||||
#[serde(rename = "type")]
|
||||
pub issue_type: IssueType,
|
||||
pub status: String,
|
||||
pub status: IssueStatus,
|
||||
pub priority: IssuePriority,
|
||||
pub list_position: f64,
|
||||
pub description: Option<String>,
|
||||
|
@ -4,8 +4,6 @@ use actix::Addr;
|
||||
use actix_web::web::{Data, Json, Path};
|
||||
use actix_web::{delete, get, post, put, HttpRequest, HttpResponse};
|
||||
|
||||
use jirs_data::ResponseData;
|
||||
|
||||
use crate::db::authorize_user::AuthorizeUser;
|
||||
use crate::db::comments::LoadIssueComments;
|
||||
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 {
|
||||
Ok(full_issue) => HttpResponse::Ok().json(full_issue.into_response()),
|
||||
Ok(full_issue) => HttpResponse::Ok().json(full_issue),
|
||||
Err(e) => e.into_http_response(),
|
||||
}
|
||||
}
|
||||
@ -92,13 +90,13 @@ pub async fn update(
|
||||
Ok(uuid) => uuid,
|
||||
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
|
||||
};
|
||||
let _user = match db
|
||||
match db
|
||||
.send(AuthorizeUser {
|
||||
access_token: token,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => user,
|
||||
Ok(Ok(_)) => (),
|
||||
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
|
||||
};
|
||||
let signal = UpdateIssue {
|
||||
@ -115,7 +113,6 @@ pub async fn update(
|
||||
time_remaining: payload.time_remaining.clone(),
|
||||
project_id: payload.project_id.clone(),
|
||||
user_ids: payload.user_ids.clone(),
|
||||
users: payload.users.clone(),
|
||||
};
|
||||
match db.send(signal).await {
|
||||
Ok(Ok(_)) => (),
|
||||
@ -123,7 +120,7 @@ pub async fn update(
|
||||
_ => return ServiceErrors::DatabaseConnectionLost.into_http_response(),
|
||||
};
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ use actix::Addr;
|
||||
use actix_web::web::{Data, Json, Path};
|
||||
use actix_web::{get, put, HttpRequest, HttpResponse};
|
||||
|
||||
use jirs_data::ResponseData;
|
||||
|
||||
use crate::db::authorize_user::AuthorizeUser;
|
||||
use crate::db::issues::LoadProjectIssues;
|
||||
use crate::db::projects::{LoadCurrentProject, UpdateProject};
|
||||
@ -73,7 +71,7 @@ pub async fn project_with_users_and_issues(
|
||||
.collect(),
|
||||
users: users.into_iter().map(|u| u.into()).collect(),
|
||||
};
|
||||
HttpResponse::Ok().json(res.into_response())
|
||||
HttpResponse::Ok().json(res)
|
||||
}
|
||||
|
||||
#[put("/{id}")]
|
||||
|
@ -114,10 +114,10 @@ table! {
|
||||
issue_type -> IssueTypeType,
|
||||
/// The `status` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
/// Its SQL type is `IssueStatusType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
status -> Text,
|
||||
status -> IssueStatusType,
|
||||
/// The `priority` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `IssuePriorityType`.
|
||||
|
29
react-client/src/App/BaseStyles.js
vendored
29
react-client/src/App/BaseStyles.js
vendored
@ -1,6 +1,6 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { color, font } from '../shared/utils/styles';
|
||||
|
||||
export default createGlobalStyle`
|
||||
html, body, #root {
|
||||
@ -89,7 +89,15 @@ export default createGlobalStyle`
|
||||
p {
|
||||
line-height: 1.4285;
|
||||
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;
|
||||
}
|
||||
|
||||
${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;
|
||||
}
|
||||
`;
|
||||
|
@ -1,18 +1,18 @@
|
||||
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 Project from 'Project';
|
||||
import Authenticate from 'Auth/Authenticate';
|
||||
import PageError from 'shared/components/PageError';
|
||||
import history from '../browserHistory';
|
||||
import Project from '../Project';
|
||||
import Authenticate from '../Auth/Authenticate';
|
||||
import PageError from '../shared/components/PageError';
|
||||
|
||||
const Routes = () => (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Redirect exact from="/" to="/project" />
|
||||
<Route path="/authenticate" component={Authenticate} />
|
||||
<Route path="/project" component={Project} />
|
||||
<Route component={PageError} />
|
||||
<Redirect exact from="/" to="/project"/>
|
||||
<Route path="/authenticate" component={Authenticate}/>
|
||||
<Route path="/project" component={Project}/>
|
||||
<Route component={PageError}/>
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
|
6
react-client/src/App/Toast/Styles.js
vendored
6
react-client/src/App/Toast/Styles.js
vendored
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin, zIndexValues } from 'shared/utils/styles';
|
||||
import { Icon } from 'shared/components';
|
||||
import { color, font, mixin, zIndexValues } from '../../shared/utils/styles';
|
||||
import { Icon } from '../../shared/components';
|
||||
|
||||
export const Container = styled.div`
|
||||
z-index: ${zIndexValues.modal + 1};
|
||||
@ -21,7 +21,7 @@ export const StyledToast = styled.div`
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
${mixin.clearfix}
|
||||
${mixin.hardwareAccelerate}
|
||||
transform: translateZ(0);
|
||||
|
||||
&.jira-toast-enter,
|
||||
&.jira-toast-exit.jira-toast-exit-active {
|
||||
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||
import { connect } from "react-redux";
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import * as formActions from 'actions/forms';
|
||||
import { getStoredAuthToken } from 'shared/utils/authToken';
|
||||
import * as formActions from '../actions/forms';
|
||||
import { getStoredAuthToken } from '../shared/utils/authToken';
|
||||
|
||||
import {
|
||||
ActionButton,
|
||||
@ -13,7 +13,7 @@ import {
|
||||
Header,
|
||||
SignIn,
|
||||
SignInSection,
|
||||
} from 'Project/IssueCreate/Styles';
|
||||
} from '../Project/IssueCreate/Styles';
|
||||
|
||||
const Authenticate = ({
|
||||
onEmailChanged,
|
||||
|
6
react-client/src/Auth/Styles.js
vendored
6
react-client/src/Auth/Styles.js
vendored
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components/dist/styled-components.esm';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Button, Form } from 'shared/components';
|
||||
import { color, font } from '../shared/utils/styles';
|
||||
import { Button, Form } from '../shared/components';
|
||||
|
||||
export const FormElement = styled(Form.Element)`;
|
||||
padding: 25px 40px 35px;
|
||||
@ -9,7 +9,7 @@ export const FormElement = styled(Form.Element)`;
|
||||
|
||||
export const FormHeading = styled.div`
|
||||
padding-bottom: 15px;
|
||||
${ font.size(21) }
|
||||
${font.size(21)}
|
||||
`;
|
||||
|
||||
export const SelectItem = styled.div`
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { InputDebounced, Avatar, Button } from 'shared/components';
|
||||
import { color } from '../../../shared/utils/styles';
|
||||
import { Avatar, Button, InputDebounced } from '../../../shared/components';
|
||||
|
||||
export const Filters = styled.div`
|
||||
display: flex;
|
||||
|
@ -2,22 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { xor } from 'lodash';
|
||||
|
||||
import {
|
||||
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,
|
||||
};
|
||||
import { AvatarIsActiveBorder, Avatars, ClearAll, Filters, SearchInput, StyledAvatar, StyledButton, } from './Styles';
|
||||
|
||||
const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => {
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { font } from 'shared/utils/styles';
|
||||
import { font } from '../../../shared/utils/styles';
|
||||
|
||||
export const Header = styled.div`
|
||||
margin-top: 6px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'shared/components';
|
||||
import { Button } from '../../../shared/components';
|
||||
|
||||
import { BoardName, Header } from './Styles';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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`
|
||||
display: flex;
|
||||
|
@ -1,17 +1,11 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Avatar, Select, Icon } from 'shared/components';
|
||||
import { Avatar, Icon, Select } from '../../../../shared/components';
|
||||
|
||||
import { SectionTitle } 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 getUserById = userId => projectUsers.find(user => user.id === userId);
|
||||
|
||||
@ -66,6 +60,10 @@ const renderUser = (user, isSelectValue, removeOptionValue) => (
|
||||
</User>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsAssigneesReporter.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsAssigneesReporter.propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
projectUsers: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsAssigneesReporter;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Button } from 'shared/components';
|
||||
import { Button } from '../../../../../shared/components';
|
||||
|
||||
export const Actions = styled.div`
|
||||
display: flex;
|
||||
|
@ -1,18 +1,10 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Textarea } from 'shared/components';
|
||||
import { Textarea } from '../../../../../shared/components';
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Avatar } from 'shared/components';
|
||||
import { color, font } from '../../../../../shared/utils/styles';
|
||||
import { Avatar } from '../../../../../shared/components';
|
||||
|
||||
export const Comment = styled.div`
|
||||
position: relative;
|
||||
@ -39,7 +39,8 @@ export const Body = styled.p`
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
const actionLinkStyles = css`
|
||||
export const EditLink = styled.div`
|
||||
margin-right: 12px;
|
||||
display: inline-block;
|
||||
padding: 2px 0;
|
||||
color: ${color.textMedium};
|
||||
@ -51,13 +52,16 @@ const actionLinkStyles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const EditLink = styled.div`
|
||||
margin-right: 12px;
|
||||
${actionLinkStyles}
|
||||
`;
|
||||
|
||||
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 {
|
||||
position: relative;
|
||||
right: 6px;
|
||||
|
@ -1,27 +1,13 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import { formatDateTimeConversational } from 'shared/utils/dateTime';
|
||||
import { ConfirmModal } from 'shared/components';
|
||||
import api from '../../../../../shared/utils/api';
|
||||
import toast from '../../../../../shared/utils/toast';
|
||||
import { formatDateTimeConversational } from '../../../../../shared/utils/dateTime';
|
||||
import { ConfirmModal } from '../../../../../shared/components';
|
||||
|
||||
import BodyForm from '../BodyForm';
|
||||
import {
|
||||
Comment,
|
||||
UserAvatar,
|
||||
Content,
|
||||
Username,
|
||||
CreatedAt,
|
||||
Body,
|
||||
EditLink,
|
||||
DeleteLink,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
comment: PropTypes.object.isRequired,
|
||||
fetchIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
import { Body, Comment, Content, CreatedAt, DeleteLink, EditLink, UserAvatar, Username, } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color } from 'shared/utils/styles';
|
||||
import { color } from '../../../../../../shared/utils/styles';
|
||||
|
||||
export const Tip = styled.div`
|
||||
display: flex;
|
||||
|
@ -1,15 +1,11 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||
import { isFocusedElementEditable } from 'shared/utils/browser';
|
||||
import { KeyCodes } from '../../../../../../shared/constants/keyCodes';
|
||||
import { isFocusedElementEditable } from '../../../../../../shared/utils/browser';
|
||||
|
||||
import { Tip, TipLetter } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
setFormOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
@ -33,6 +29,8 @@ const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
|
||||
);
|
||||
};
|
||||
|
||||
ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = {
|
||||
setFormOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsCommentsCreateProTip;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Avatar } from 'shared/components';
|
||||
import { color } from '../../../../../shared/utils/styles';
|
||||
import { Avatar } from '../../../../../shared/components';
|
||||
|
||||
export const Create = styled.div`
|
||||
position: relative;
|
||||
|
@ -2,9 +2,9 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import { fetchCurrentUser } from "actions/users";
|
||||
import api from '../../../../../shared/utils/api';
|
||||
import toast from '../../../../../shared/utils/toast';
|
||||
import { fetchCurrentUser } from "../../../../../actions/users";
|
||||
|
||||
import BodyForm from '../BodyForm';
|
||||
import ProTip from './ProTip';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { font } from 'shared/utils/styles';
|
||||
import { font } from '../../../../shared/utils/styles';
|
||||
|
||||
export const Comments = styled.div`
|
||||
padding-top: 40px;
|
||||
|
@ -1,28 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { sortByNewest } from 'shared/utils/javascript';
|
||||
import { sortByNewest } from '../../../../shared/utils/javascript';
|
||||
|
||||
import Create from './Create';
|
||||
import Comment from './Comment';
|
||||
import { Comments, Title } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
fetchIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (
|
||||
<Comments>
|
||||
<Title>Comments</Title>
|
||||
<Create issueId={issue.id} fetchIssue={fetchIssue} />
|
||||
<Create issueId={issue.id} fetchIssue={fetchIssue}/>
|
||||
|
||||
{sortByNewest(issue.comments, 'createdAt').map(comment => (
|
||||
<Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} />
|
||||
<Comment key={comment.id} comment={comment} fetchIssue={fetchIssue}/>
|
||||
))}
|
||||
</Comments>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsComments.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsComments.propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
fetchIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsComments;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { color } from '../../../../shared/utils/styles';
|
||||
|
||||
export const Dates = styled.div`
|
||||
margin-top: 11px;
|
||||
|
@ -1,14 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatDateTimeConversational } from 'shared/utils/dateTime';
|
||||
import { formatDateTimeConversational } from '../../../../shared/utils/dateTime';
|
||||
|
||||
import { Dates } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsDates = ({ issue }) => (
|
||||
<Dates>
|
||||
<div>Created at {formatDateTimeConversational(issue.createdAt)}</div>
|
||||
@ -16,6 +12,8 @@ const ProjectBoardIssueDetailsDates = ({ issue }) => (
|
||||
</Dates>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsDates.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsDates.propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsDates;
|
||||
|
@ -1,15 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import { Button, ConfirmModal } from 'shared/components';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
fetchProject: PropTypes.func.isRequired,
|
||||
modalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
import api from '../../../shared/utils/api';
|
||||
import toast from '../../../shared/utils/toast';
|
||||
import { Button, ConfirmModal } from '../../../shared/components';
|
||||
|
||||
const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => {
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
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`
|
||||
padding: 20px 0 6px;
|
||||
|
@ -1,15 +1,10 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getTextContentsFromHtmlString } from 'shared/utils/browser';
|
||||
import { TextEditor, TextEditedContent, Button } from 'shared/components';
|
||||
import { getTextContentsFromHtmlString } from '../../../../shared/utils/browser';
|
||||
import { Button, TextEditedContent, TextEditor } from '../../../../shared/components';
|
||||
|
||||
import { Title, EmptyLabel, Actions } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
import { Actions, EmptyLabel, Title } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
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`
|
||||
padding: 4px 4px 2px 0;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Icon } from 'shared/components';
|
||||
import { color } from '../../../../../shared/utils/styles';
|
||||
import { Icon } from '../../../../../shared/components';
|
||||
|
||||
export const TrackingWidget = styled.div`
|
||||
display: flex;
|
||||
|
@ -2,11 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { TrackingWidget, WatchIcon, Right, BarCont, Bar, Values } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
};
|
||||
import { Bar, BarCont, Right, TrackingWidget, Values, WatchIcon } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsTrackingWidget = ({ issue }) => (
|
||||
<TrackingWidget>
|
||||
@ -50,6 +46,8 @@ const renderRemainingOrEstimate = ({ timeRemaining, estimate }) => {
|
||||
}
|
||||
};
|
||||
|
||||
ProjectBoardIssueDetailsTrackingWidget.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsTrackingWidget.propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsTrackingWidget;
|
||||
|
@ -2,24 +2,11 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { InputDebounced, Modal, Button } from 'shared/components';
|
||||
import { Button, InputDebounced, Modal } from '../../../../shared/components';
|
||||
|
||||
import TrackingWidget from './TrackingWidget';
|
||||
import { SectionTitle } from '../Styles';
|
||||
import {
|
||||
TrackingLink,
|
||||
ModalContents,
|
||||
ModalTitle,
|
||||
Inputs,
|
||||
InputCont,
|
||||
InputLabel,
|
||||
Actions,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
import { Actions, InputCont, InputLabel, Inputs, ModalContents, ModalTitle, TrackingLink, } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => (
|
||||
<Fragment>
|
||||
@ -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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { color } from '../../../../shared/utils/styles';
|
||||
|
||||
export const Priority = styled.div`
|
||||
display: flex;
|
||||
|
@ -1,16 +1,11 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';
|
||||
import { Select, IssuePriorityIcon } from 'shared/components';
|
||||
import { IssuePriority, IssuePriorityCopy } from '../../../../shared/constants/issues';
|
||||
import { IssuePriorityIcon, Select } from '../../../../shared/components';
|
||||
|
||||
import { SectionTitle } from '../Styles';
|
||||
import { Priority, Label } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
import { Label, Priority } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => (
|
||||
<Fragment>
|
||||
@ -39,6 +34,9 @@ const renderPriorityItem = (priority, isValue) => (
|
||||
</Priority>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsPriority.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsPriority.propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsPriority;
|
||||
|
@ -1,11 +1,28 @@
|
||||
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`
|
||||
text-transform: uppercase;
|
||||
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.isValue &&
|
||||
css`
|
||||
|
@ -1,17 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
|
||||
import { Select, Icon } from 'shared/components';
|
||||
import { IssueStatus, IssueStatusCopy } from '../../../../shared/constants/issues';
|
||||
import { Icon, Select } from '../../../../shared/components';
|
||||
|
||||
import { SectionTitle } from '../Styles';
|
||||
import { Status } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
|
||||
<Fragment>
|
||||
<SectionTitle>Status</SectionTitle>
|
||||
@ -39,6 +34,9 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsStatus.propTypes = propTypes;
|
||||
ProjectBoardIssueDetailsStatus.propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBoardIssueDetailsStatus;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color } from 'shared/utils/styles';
|
||||
import { color } from '../../../shared/utils/styles';
|
||||
|
||||
export const Content = styled.div`
|
||||
display: flex;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Textarea } from 'shared/components';
|
||||
import { color, font } from '../../../../shared/utils/styles';
|
||||
import { Textarea } from '../../../../shared/components';
|
||||
|
||||
export const TitleTextarea = styled(Textarea)`
|
||||
margin: 18px 0 0 -8px;
|
||||
|
@ -1,15 +1,10 @@
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||
import { is, generateErrors } from 'shared/utils/validation';
|
||||
import { KeyCodes } from '../../../../shared/constants/keyCodes';
|
||||
import { generateErrors, is } from '../../../../shared/utils/validation';
|
||||
|
||||
import { TitleTextarea, ErrorText } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
import { ErrorText, TitleTextarea } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => {
|
||||
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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Button } from 'shared/components';
|
||||
import { color } from '../../../../shared/utils/styles';
|
||||
import { Button } from '../../../../shared/components';
|
||||
|
||||
export const TypeButton = styled(Button)`
|
||||
text-transform: uppercase;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { PageError, CopyLinkButton, Button, AboutTooltip } from 'shared/components';
|
||||
import api from '../../../shared/utils/api';
|
||||
import useApi from '../../../shared/hooks/api';
|
||||
import { AboutTooltip, Button, CopyLinkButton, PageError } from '../../../shared/components';
|
||||
|
||||
import Loader from './Loader';
|
||||
import Type from './Type';
|
||||
@ -16,15 +16,7 @@ import AssigneesReporter from './AssigneesReporter';
|
||||
import Priority from './Priority';
|
||||
import EstimateTracking from './EstimateTracking';
|
||||
import Dates from './Dates';
|
||||
import { TopActions, TopActionsRight, Content, Left, Right } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issueId: PropTypes.string.isRequired,
|
||||
projectUsers: PropTypes.array.isRequired,
|
||||
fetchProject: PropTypes.func.isRequired,
|
||||
updateLocalProjectIssues: PropTypes.func.isRequired,
|
||||
modalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
import { Content, Left, Right, TopActions, TopActionsRight } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetails = ({
|
||||
issueId,
|
||||
@ -32,7 +24,7 @@ const ProjectBoardIssueDetails = ({
|
||||
fetchProject,
|
||||
updateLocalProjectIssues,
|
||||
modalClose,
|
||||
}) => {
|
||||
}) => {
|
||||
const [{ data, error, setLocalData }, fetchIssue] = useApi.get(`/issues/${issueId}`);
|
||||
|
||||
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;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Avatar } from 'shared/components';
|
||||
import { color } from '../../../../../shared/utils/styles';
|
||||
import { Avatar } from '../../../../../shared/components';
|
||||
|
||||
export const IssueLink = styled(Link)`
|
||||
display: block;
|
||||
|
@ -7,12 +7,6 @@ import { IssuePriorityIcon, IssueTypeIcon } from '../../../../../shared/componen
|
||||
|
||||
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 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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color } from 'shared/utils/styles';
|
||||
import { color } from '../../../../shared/utils/styles';
|
||||
|
||||
export const List = styled.div`
|
||||
display: flex;
|
||||
|
@ -4,7 +4,7 @@ import moment from 'moment';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { intersection } from 'lodash';
|
||||
|
||||
import { IssueStatusCopy } from 'shared/constants/issues';
|
||||
import { IssueStatusCopy } from '../../../../shared/constants/issues';
|
||||
|
||||
import Issue from './Issue';
|
||||
import { Issues, IssuesCount, List, Title } from './Styles';
|
||||
|
@ -2,10 +2,10 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import useCurrentUser from 'shared/hooks/currentUser';
|
||||
import api from 'shared/utils/api';
|
||||
import { insertItemIntoArray, moveItemWithinArray } from 'shared/utils/javascript';
|
||||
import { IssueStatus } from 'shared/constants/issues';
|
||||
import useCurrentUser from '../../../shared/hooks/currentUser';
|
||||
import api from '../../../shared/utils/api';
|
||||
import { insertItemIntoArray, moveItemWithinArray } from '../../../shared/utils/javascript';
|
||||
import { IssueStatus } from '../../../shared/constants/issues';
|
||||
|
||||
import List from './List';
|
||||
import { Lists } from './Styles';
|
||||
|
@ -2,8 +2,8 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import useMergeState from 'shared/hooks/mergeState';
|
||||
import { Breadcrumbs, Modal } from 'shared/components';
|
||||
import useMergeState from '../../shared/hooks/mergeState';
|
||||
import { Breadcrumbs, Modal } from '../../shared/components';
|
||||
|
||||
import Header from './Header';
|
||||
import Filters from './Filters';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Button } from 'shared/components';
|
||||
import { color } from '../../shared/utils/styles';
|
||||
import { Button } from '../../shared/components';
|
||||
|
||||
export const SignIn = styled.article`
|
||||
margin: 24px auto;
|
||||
@ -30,7 +30,7 @@ export const FormElement = styled.div`
|
||||
|
||||
export const FormHeading = styled.div`
|
||||
padding-bottom: 15px;
|
||||
${ font.size(21) }
|
||||
font-size: 21px;
|
||||
`;
|
||||
|
||||
export const SelectItem = styled.div`
|
||||
|
@ -1,10 +1,16 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssuePriority, IssuePriorityCopy, 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 {
|
||||
IssuePriority,
|
||||
IssuePriorityCopy,
|
||||
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';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Icon, InputDebounced, Spinner } from 'shared/components';
|
||||
import { color, font } from '../../shared/utils/styles';
|
||||
import { Icon, InputDebounced, Spinner } from '../../shared/components';
|
||||
|
||||
export const IssueSearch = styled.div`
|
||||
padding: 25px 35px 60px;
|
||||
|
@ -3,31 +3,27 @@ import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { sortByNewest } from 'shared/utils/javascript';
|
||||
import { IssueTypeIcon } from 'shared/components';
|
||||
import useApi from '../../shared/hooks/api';
|
||||
import { sortByNewest } from '../../shared/utils/javascript';
|
||||
import { IssueTypeIcon } from '../../shared/components';
|
||||
|
||||
import NoResultsSVG from './NoResultsSvg';
|
||||
import {
|
||||
IssueSearch,
|
||||
SearchInputCont,
|
||||
SearchInputDebounced,
|
||||
SearchIcon,
|
||||
SearchSpinner,
|
||||
Issue,
|
||||
IssueData,
|
||||
IssueSearch,
|
||||
IssueTitle,
|
||||
IssueTypeId,
|
||||
SectionTitle,
|
||||
NoResults,
|
||||
NoResultsTitle,
|
||||
NoResultsTip,
|
||||
NoResultsTitle,
|
||||
SearchIcon,
|
||||
SearchInputCont,
|
||||
SearchInputDebounced,
|
||||
SearchSpinner,
|
||||
SectionTitle,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
project: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const ProjectIssueSearch = ({ project }) => {
|
||||
const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true);
|
||||
|
||||
@ -96,6 +92,8 @@ const renderIssue = issue => (
|
||||
</Link>
|
||||
);
|
||||
|
||||
ProjectIssueSearch.propTypes = propTypes;
|
||||
ProjectIssueSearch.propTypes = {
|
||||
project: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectIssueSearch;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { color, mixin, sizes, zIndexValues } from 'shared/utils/styles';
|
||||
import { Logo } from 'shared/components';
|
||||
import { color, sizes, zIndexValues } from '../../shared/utils/styles';
|
||||
import { Logo } from '../../shared/components';
|
||||
|
||||
export const NavLeft = styled.aside`
|
||||
z-index: ${zIndexValues.navLeft};
|
||||
@ -14,7 +14,7 @@ export const NavLeft = styled.aside`
|
||||
width: ${sizes.appNavBarLeftWidth}px;
|
||||
background: ${color.backgroundDarkPrimary};
|
||||
transition: all 0.1s;
|
||||
${mixin.hardwareAccelerate}
|
||||
transform: translateZ(0);
|
||||
&:hover {
|
||||
width: 200px;
|
||||
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);
|
||||
|
@ -1,15 +1,10 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
const propTypes = {
|
||||
issueSearchModalOpen: PropTypes.func.isRequired,
|
||||
issueCreateModalOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
|
||||
<NavLeft class={ 'NavLeft' }>
|
||||
<LogoLink to="/">
|
||||
@ -41,6 +36,9 @@ const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
|
||||
</NavLeft>
|
||||
);
|
||||
|
||||
ProjectNavbarLeft.propTypes = propTypes;
|
||||
ProjectNavbarLeft.propTypes = {
|
||||
issueSearchModalOpen: PropTypes.func.isRequired,
|
||||
issueCreateModalOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectNavbarLeft;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { font } from 'shared/utils/styles';
|
||||
import { Button, Form } from 'shared/components';
|
||||
import { font } from '../../shared/utils/styles';
|
||||
import { Button, Form } from '../../shared/components';
|
||||
|
||||
export const FormCont = styled.div`
|
||||
display: flex;
|
||||
@ -16,7 +16,7 @@ export const FormElement = styled(Form.Element)`
|
||||
export const FormHeading = styled.h1`
|
||||
padding: 6px 0 15px;
|
||||
font-size: 24px
|
||||
${font.medium}
|
||||
${font.medium}; font-weight: normal;
|
||||
`;
|
||||
|
||||
export const ActionButton = styled(Button)`
|
||||
|
@ -2,11 +2,18 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { ProjectCategory, ProjectCategoryCopy } from 'shared/constants/projects';
|
||||
import toast from 'shared/utils/toast';
|
||||
import api from 'shared/utils/api';
|
||||
import { Breadcrumbs, Form } from 'shared/components';
|
||||
import { updateProjectFormFieldChanged, updateProjectFormRequest, updateProjectFormSuccess, } from 'actions/forms';
|
||||
import { ProjectCategory, ProjectCategoryCopy } from '../../shared/constants/projects';
|
||||
import toast from '../../shared/utils/toast';
|
||||
import api from '../../shared/utils/api';
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Form
|
||||
} from '../../shared/components';
|
||||
import {
|
||||
updateProjectFormFieldChanged,
|
||||
updateProjectFormRequest,
|
||||
updateProjectFormSuccess,
|
||||
} from '../../actions/forms';
|
||||
|
||||
import { ActionButton, FormCont, FormElement, FormHeading } from './Styles';
|
||||
|
||||
|
14
react-client/src/Project/Sidebar/Styles.js
vendored
14
react-client/src/Project/Sidebar/Styles.js
vendored
@ -1,6 +1,6 @@
|
||||
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`
|
||||
position: fixed;
|
||||
@ -15,7 +15,17 @@ export const Sidebar = styled.div`
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-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) {
|
||||
width: ${sizes.secondarySideBarWidth - 10}px;
|
||||
}
|
||||
|
@ -2,25 +2,21 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { ProjectCategoryCopy } from 'shared/constants/projects';
|
||||
import { Icon, ProjectAvatar } from 'shared/components';
|
||||
import { ProjectCategoryCopy } from '../../shared/constants/projects';
|
||||
import { Icon, ProjectAvatar } from '../../shared/components';
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
ProjectInfo,
|
||||
ProjectTexts,
|
||||
ProjectName,
|
||||
ProjectCategory,
|
||||
Divider,
|
||||
LinkItem,
|
||||
LinkText,
|
||||
NotImplemented,
|
||||
ProjectCategory,
|
||||
ProjectInfo,
|
||||
ProjectName,
|
||||
ProjectTexts,
|
||||
Sidebar,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
project: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const ProjectSidebar = ({ project }) => {
|
||||
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;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { updateArrayItemById } from 'shared/utils/javascript';
|
||||
import { createQueryParamModalHelpers } from 'shared/utils/queryParamModal';
|
||||
import { Modal, PageError, PageLoader } from 'shared/components';
|
||||
import useApi from '../shared/hooks/api';
|
||||
import { updateArrayItemById } from '../shared/utils/javascript';
|
||||
import { createQueryParamModalHelpers } from '../shared/utils/queryParamModal';
|
||||
import { Modal, PageError, PageLoader } from '../shared/components';
|
||||
|
||||
import NavbarLeft from './NavbarLeft';
|
||||
import Sidebar from './Sidebar';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { copyToClipboard } from 'shared/utils/browser';
|
||||
import { Button } from 'shared/components';
|
||||
import { copyToClipboard } from '../../shared/utils/browser';
|
||||
import { Button } from '../../shared/components';
|
||||
|
||||
const CopyLinkButton = ({ ...buttonProps }) => {
|
||||
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { zIndexValues, mixin } from 'shared/utils/styles';
|
||||
import { mixin, zIndexValues } from 'shared/utils/styles';
|
||||
|
||||
export const StyledTooltip = styled.div`
|
||||
z-index: ${zIndexValues.modal + 1};
|
||||
@ -8,6 +8,6 @@ export const StyledTooltip = styled.div`
|
||||
width: ${props => props.width}px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
${mixin.hardwareAccelerate}
|
||||
transform: translateZ(0);
|
||||
${mixin.boxShadowDropdown}
|
||||
`;
|
||||
|
Loading…
Reference in New Issue
Block a user