Add copy link command
This commit is contained in:
parent
5e81329378
commit
f778f5a5c5
@ -31,5 +31,8 @@ features = [
|
|||||||
"DataTransfer",
|
"DataTransfer",
|
||||||
"DragEvent",
|
"DragEvent",
|
||||||
"HtmlDivElement",
|
"HtmlDivElement",
|
||||||
"DomRect"
|
"DomRect",
|
||||||
|
"HtmlDocument",
|
||||||
|
"Document",
|
||||||
|
"Selection"
|
||||||
]
|
]
|
||||||
|
@ -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)),
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -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)]
|
||||||
|
@ -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"],
|
||||||
|
@ -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
|
||||||
|
@ -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>,
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 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)?;
|
||||||
|
@ -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)],
|
||||||
|
@ -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>,
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}")]
|
||||||
|
@ -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`.
|
||||||
|
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 { 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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -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;
|
||||||
|
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 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 {
|
||||||
|
@ -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,
|
||||||
|
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 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`
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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`
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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`
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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)`
|
||||||
|
@ -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: '',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
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 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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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}>
|
||||||
|
@ -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}
|
||||||
`;
|
`;
|
||||||
|
Loading…
Reference in New Issue
Block a user