Add copy link command

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,11 @@
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';
const propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
projectUsers: PropTypes.array.isRequired,
};
const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, projectUsers }) => {
const getUserById = userId => projectUsers.find(user => user.id === userId);
@ -66,6 +60,10 @@ const renderUser = (user, isSelectValue, removeOptionValue) => (
</User>
);
ProjectBoardIssueDetailsAssigneesReporter.propTypes = propTypes;
ProjectBoardIssueDetailsAssigneesReporter.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
projectUsers: PropTypes.array.isRequired,
};
export default ProjectBoardIssueDetailsAssigneesReporter;

View File

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

View File

@ -1,18 +1,10 @@
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';
const propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
isWorking: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
class ProjectBoardIssueDetailsCommentsBodyForm extends React.Component {
state = { textArea: React.createRef() };
@ -53,6 +45,12 @@ class ProjectBoardIssueDetailsCommentsBodyForm extends React.Component {
}
}
ProjectBoardIssueDetailsCommentsBodyForm.propTypes = propTypes;
ProjectBoardIssueDetailsCommentsBodyForm.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
isWorking: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsCommentsBodyForm;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import PropTypes from 'prop-types';
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 { AssigneeAvatar, Assignees, Bottom, Issue, IssueLink, Title } from './Styles';
const propTypes = {
projectUsers: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
};
const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
const match = useRouteMatch();
@ -54,6 +48,10 @@ const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
);
};
ProjectBoardListIssue.propTypes = propTypes;
ProjectBoardListIssue.propTypes = {
projectUsers: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
};
export default ProjectBoardListIssue;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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