Implemented issue create modal, further polish
This commit is contained in:
@@ -19,7 +19,8 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
'/issues',
|
'/issues',
|
||||||
catchErrors(async (req, res) => {
|
catchErrors(async (req, res) => {
|
||||||
const issue = await createEntity(Issue, req.body);
|
const listPosition = await calculateListPosition(req.body);
|
||||||
|
const issue = await createEntity(Issue, { ...req.body, listPosition });
|
||||||
res.respond({ issue });
|
res.respond({ issue });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -40,4 +41,15 @@ router.delete(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const calculateListPosition = async (newIssue: Issue): Promise<number> => {
|
||||||
|
const issues = await Issue.find({
|
||||||
|
where: { projectId: newIssue.projectId, status: newIssue.status },
|
||||||
|
});
|
||||||
|
const listPositions = issues.map(({ listPosition }) => listPosition);
|
||||||
|
if (listPositions.length > 0) {
|
||||||
|
return Math.min(...listPositions) - 1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Issue extends BaseEntity {
|
|||||||
type: [is.required(), is.oneOf(Object.values(IssueType))],
|
type: [is.required(), is.oneOf(Object.values(IssueType))],
|
||||||
status: [is.required(), is.oneOf(Object.values(IssueStatus))],
|
status: [is.required(), is.oneOf(Object.values(IssueStatus))],
|
||||||
priority: [is.required(), is.oneOf(Object.values(IssuePriority))],
|
priority: [is.required(), is.oneOf(Object.values(IssuePriority))],
|
||||||
|
listPosition: is.required(),
|
||||||
reporterId: is.required(),
|
reporterId: is.required(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,6 +72,9 @@ class Issue extends BaseEntity {
|
|||||||
)
|
)
|
||||||
project: Project;
|
project: Project;
|
||||||
|
|
||||||
|
@Column('integer')
|
||||||
|
projectId: number;
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => Comment,
|
() => Comment,
|
||||||
comment => comment.issue,
|
comment => comment.issue,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"import/no-cycle": 0,
|
"import/no-cycle": 0,
|
||||||
"react/no-array-index-key": 0,
|
"react/no-array-index-key": 0,
|
||||||
"react/forbid-prop-types": 0,
|
"react/forbid-prop-types": 0,
|
||||||
|
"react/prop-types": [2, { "skipUndeclared": true }],
|
||||||
"react/state-in-constructor": 0,
|
"react/state-in-constructor": 0,
|
||||||
"react/jsx-props-no-spreading": 0,
|
"react/jsx-props-no-spreading": 0,
|
||||||
"jsx-a11y/click-events-have-key-events": 0
|
"jsx-a11y/click-events-have-key-events": 0
|
||||||
|
|||||||
44
client/package-lock.json
generated
44
client/package-lock.json
generated
@@ -2733,6 +2733,11 @@
|
|||||||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
|
||||||
|
},
|
||||||
"default-gateway": {
|
"default-gateway": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
|
||||||
@@ -4179,6 +4184,32 @@
|
|||||||
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
|
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"formik": {
|
||||||
|
"version": "2.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/formik/-/formik-2.0.8.tgz",
|
||||||
|
"integrity": "sha512-PxC9G6EvLdJzMv7z+bsvpI/Euplv2vVgTQebD5yNza4t3fiMBB+iD90VOVTLCyH5Pnv3bljsZiKsIqGp/UNKKg==",
|
||||||
|
"requires": {
|
||||||
|
"deepmerge": "^2.1.1",
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"lodash": "^4.17.14",
|
||||||
|
"lodash-es": "^4.17.14",
|
||||||
|
"react-fast-compare": "^2.0.1",
|
||||||
|
"scheduler": "^0.17.0",
|
||||||
|
"tiny-warning": "^1.0.2",
|
||||||
|
"tslib": "^1.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"scheduler": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"object-assign": "^4.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
@@ -6309,6 +6340,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||||
},
|
},
|
||||||
|
"lodash-es": {
|
||||||
|
"version": "4.17.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
|
||||||
|
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
|
||||||
|
},
|
||||||
"log-symbols": {
|
"log-symbols": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
|
||||||
@@ -7845,6 +7881,11 @@
|
|||||||
"scheduler": "^0.18.0"
|
"scheduler": "^0.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-fast-compare": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.12.0",
|
"version": "16.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
||||||
@@ -9459,8 +9500,7 @@
|
|||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
|
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"tty-browserify": {
|
"tty-browserify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"core-js": "^3.4.7",
|
"core-js": "^3.4.7",
|
||||||
|
"formik": "^2.0.8",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Icon } from 'shared/components';
|
|
||||||
import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';
|
|
||||||
|
|
||||||
const NavbarLeft = () => (
|
|
||||||
<NavLeft>
|
|
||||||
<LogoLink to="/">
|
|
||||||
<StyledLogo color="#fff" />
|
|
||||||
</LogoLink>
|
|
||||||
<Item>
|
|
||||||
<Icon type="search" size={22} top={1} left={3} />
|
|
||||||
<ItemText>Search</ItemText>
|
|
||||||
</Item>
|
|
||||||
<Item>
|
|
||||||
<Icon type="plus" size={27} />
|
|
||||||
<ItemText>Create</ItemText>
|
|
||||||
</Item>
|
|
||||||
<Bottom>
|
|
||||||
<Item>
|
|
||||||
<Icon type="help" size={25} />
|
|
||||||
<ItemText>Help</ItemText>
|
|
||||||
</Item>
|
|
||||||
</Bottom>
|
|
||||||
</NavLeft>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default NavbarLeft;
|
|
||||||
@@ -3,24 +3,17 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom';
|
|||||||
import history from 'browserHistory';
|
import history from 'browserHistory';
|
||||||
|
|
||||||
import PageError from 'shared/components/PageError';
|
import PageError from 'shared/components/PageError';
|
||||||
|
import Project from 'Project';
|
||||||
import Project from 'components/Project';
|
|
||||||
|
|
||||||
import NavbarLeft from './NavbarLeft';
|
|
||||||
import Authenticate from './Authenticate';
|
import Authenticate from './Authenticate';
|
||||||
import { Main } from './AppStyles';
|
|
||||||
|
|
||||||
const Routes = () => (
|
const Routes = () => (
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Main>
|
|
||||||
<NavbarLeft />
|
|
||||||
<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>
|
||||||
</Main>
|
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { color, font, mixin } from 'shared/utils/styles';
|
|
||||||
import { Button } from 'shared/components';
|
|
||||||
|
|
||||||
export const TypeButton = styled(Button)`
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
color: ${color.textMedium};
|
|
||||||
${font.size(13)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TypeDropdown = styled.div`
|
|
||||||
padding-bottom: 6px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TypeTitle = styled.div`
|
|
||||||
padding: 10px 0 7px 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: ${color.textMedium};
|
|
||||||
${font.size(12)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Type = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 7px 12px;
|
|
||||||
${mixin.clickable}
|
|
||||||
&:hover {
|
|
||||||
background: ${color.backgroundLight};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TypeLabel = styled.div`
|
|
||||||
padding: 0 5px 0 7px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
${font.size(15)}
|
|
||||||
`;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { IssueType } from 'shared/constants/issues';
|
|
||||||
import { IssueTypeIcon, Tooltip } from 'shared/components';
|
|
||||||
import { TypeButton, TypeDropdown, TypeTitle, Type, TypeLabel } from './Styles';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
issue: PropTypes.object.isRequired,
|
|
||||||
updateIssue: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
|
|
||||||
<Tooltip
|
|
||||||
width={150}
|
|
||||||
offset={{ top: -15 }}
|
|
||||||
renderLink={linkProps => (
|
|
||||||
<TypeButton {...linkProps} color="empty" icon={<IssueTypeIcon type={issue.type} />}>
|
|
||||||
{`${issue.type}-${issue.id}`}
|
|
||||||
</TypeButton>
|
|
||||||
)}
|
|
||||||
renderContent={() => (
|
|
||||||
<TypeDropdown>
|
|
||||||
<TypeTitle>Change issue type</TypeTitle>
|
|
||||||
{Object.values(IssueType).map(type => (
|
|
||||||
<Type key={type} onClick={() => updateIssue({ type })}>
|
|
||||||
<IssueTypeIcon type={type} top={1} />
|
|
||||||
<TypeLabel>{type}</TypeLabel>
|
|
||||||
</Type>
|
|
||||||
))}
|
|
||||||
</TypeDropdown>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
ProjectBoardIssueDetailsType.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default ProjectBoardIssueDetailsType;
|
|
||||||
@@ -22,7 +22,7 @@ const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
|||||||
<Draggable draggableId={issue.id.toString()} index={index}>
|
<Draggable draggableId={issue.id.toString()} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<IssueLink
|
<IssueLink
|
||||||
to={`${match.url}/${issue.id}`}
|
to={`${match.url}/issue/${issue.id}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Route, useRouteMatch, useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { 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';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
project: PropTypes.object.isRequired,
|
project: PropTypes.object.isRequired,
|
||||||
fetchProject: PropTypes.func.isRequired,
|
|
||||||
updateLocalIssuesArray: PropTypes.func.isRequired,
|
updateLocalIssuesArray: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,11 +17,8 @@ const defaultFilters = {
|
|||||||
recent: false,
|
recent: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoard = ({ project, fetchProject, updateLocalIssuesArray }) => {
|
const ProjectBoard = ({ project, updateLocalIssuesArray }) => {
|
||||||
const match = useRouteMatch();
|
|
||||||
const history = useHistory();
|
|
||||||
const [filters, setFilters] = useState(defaultFilters);
|
const [filters, setFilters] = useState(defaultFilters);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header projectName={project.name} />
|
<Header projectName={project.name} />
|
||||||
@@ -36,26 +29,6 @@ const ProjectBoard = ({ project, fetchProject, updateLocalIssuesArray }) => {
|
|||||||
setFilters={setFilters}
|
setFilters={setFilters}
|
||||||
/>
|
/>
|
||||||
<Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} />
|
<Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} />
|
||||||
<Route
|
|
||||||
path={`${match.path}/:issueId`}
|
|
||||||
render={({ match: { params } }) => (
|
|
||||||
<Modal
|
|
||||||
isOpen
|
|
||||||
width={1040}
|
|
||||||
withCloseIcon={false}
|
|
||||||
onClose={() => history.push(match.url)}
|
|
||||||
renderContent={modal => (
|
|
||||||
<IssueDetails
|
|
||||||
issueId={params.issueId}
|
|
||||||
projectUsers={project.users}
|
|
||||||
fetchProject={fetchProject}
|
|
||||||
updateLocalIssuesArray={updateLocalIssuesArray}
|
|
||||||
modalClose={modal.close}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
39
client/src/Project/IssueCreateForm/Styles.js
Normal file
39
client/src/Project/IssueCreateForm/Styles.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { color, font } from 'shared/utils/styles';
|
||||||
|
import { Button, Form } from 'shared/components';
|
||||||
|
|
||||||
|
export const FormElement = styled(Form.Element)`
|
||||||
|
padding: 20px 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FormHeading = styled.div`
|
||||||
|
padding-bottom: 15px;
|
||||||
|
${font.size(20)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
${props => props.withBottomMargin && `margin-bottom: 5px;`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectItemLabel = styled.div`
|
||||||
|
padding: 0 3px 0 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Divider = styled.div`
|
||||||
|
margin-top: 22px;
|
||||||
|
border-top: 1px solid ${color.borderLightest};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Actions = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 30px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActionButton = styled(Button)`
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
169
client/src/Project/IssueCreateForm/index.jsx
Normal file
169
client/src/Project/IssueCreateForm/index.jsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IssueType,
|
||||||
|
IssueStatus,
|
||||||
|
IssuePriority,
|
||||||
|
IssueTypeCopy,
|
||||||
|
IssuePriorityCopy,
|
||||||
|
} from 'shared/constants/issues';
|
||||||
|
import toast from 'shared/utils/toast';
|
||||||
|
import useApi from 'shared/hooks/api';
|
||||||
|
import { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components';
|
||||||
|
import {
|
||||||
|
FormHeading,
|
||||||
|
FormElement,
|
||||||
|
SelectItem,
|
||||||
|
SelectItemLabel,
|
||||||
|
Divider,
|
||||||
|
Actions,
|
||||||
|
ActionButton,
|
||||||
|
} from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
project: PropTypes.object.isRequired,
|
||||||
|
fetchProject: PropTypes.func.isRequired,
|
||||||
|
modalClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectIssueCreateForm = ({ project, fetchProject, modalClose }) => {
|
||||||
|
const [{ isCreating }, createIssue] = useApi.post('/issues');
|
||||||
|
|
||||||
|
const typeOptions = Object.values(IssueType).map(type => ({
|
||||||
|
value: type,
|
||||||
|
label: IssueTypeCopy[type],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const priorityOptions = Object.values(IssuePriority).map(priority => ({
|
||||||
|
value: priority,
|
||||||
|
label: IssuePriorityCopy[priority],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userOptions = project.users.map(user => ({ value: user.id, label: user.name }));
|
||||||
|
|
||||||
|
const renderType = ({ value: type }) => (
|
||||||
|
<SelectItem>
|
||||||
|
<IssueTypeIcon type={type} top={1} />
|
||||||
|
<SelectItemLabel>{IssueTypeCopy[type]}</SelectItemLabel>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPriority = ({ value: priority }) => (
|
||||||
|
<SelectItem>
|
||||||
|
<IssuePriorityIcon priority={priority} top={1} />
|
||||||
|
<SelectItemLabel>{IssuePriorityCopy[priority]}</SelectItemLabel>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUser = ({ value: userId, removeOptionValue }) => {
|
||||||
|
const user = project.users.find(({ id }) => id === userId);
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={user.id}
|
||||||
|
withBottomMargin={!!removeOptionValue}
|
||||||
|
onClick={() => removeOptionValue && removeOptionValue()}
|
||||||
|
>
|
||||||
|
<Avatar size={20} avatarUrl={user.avatarUrl} name={user.name} />
|
||||||
|
<SelectItemLabel>{user.name}</SelectItemLabel>
|
||||||
|
{removeOptionValue && <Icon type="close" top={2} />}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initialValues={{
|
||||||
|
status: IssueStatus.BACKLOG,
|
||||||
|
type: IssueType.TASK,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
reporterId: null,
|
||||||
|
userIds: [],
|
||||||
|
priority: null,
|
||||||
|
}}
|
||||||
|
validations={{
|
||||||
|
type: Form.is.required(),
|
||||||
|
title: [Form.is.required(), Form.is.maxLength(200)],
|
||||||
|
reporterId: Form.is.required(),
|
||||||
|
priority: Form.is.required(),
|
||||||
|
}}
|
||||||
|
onSubmit={async (values, form) => {
|
||||||
|
try {
|
||||||
|
await createIssue({
|
||||||
|
...values,
|
||||||
|
projectId: project.id,
|
||||||
|
users: values.userIds.map(id => ({ id })),
|
||||||
|
});
|
||||||
|
await fetchProject();
|
||||||
|
modalClose();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.data.fields) {
|
||||||
|
form.setErrors(error.data.fields);
|
||||||
|
} else {
|
||||||
|
toast.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormElement>
|
||||||
|
<FormHeading>Create issue</FormHeading>
|
||||||
|
<Form.Field.Select
|
||||||
|
name="type"
|
||||||
|
label="Issue Type"
|
||||||
|
tip="Start typing to get a list of possible matches."
|
||||||
|
options={typeOptions}
|
||||||
|
renderOption={renderType}
|
||||||
|
renderValue={renderType}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<Form.Field.Input
|
||||||
|
name="title"
|
||||||
|
label="Short Summary"
|
||||||
|
tip="Concisely summarize the issue in one or two sentences."
|
||||||
|
/>
|
||||||
|
<Form.Field.TextEditor
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
tip="Describe the issue in as much detail as you'd like."
|
||||||
|
/>
|
||||||
|
<Form.Field.Select
|
||||||
|
name="reporterId"
|
||||||
|
label="Reporter"
|
||||||
|
options={userOptions}
|
||||||
|
renderOption={renderUser}
|
||||||
|
renderValue={renderUser}
|
||||||
|
/>
|
||||||
|
<Form.Field.Select
|
||||||
|
isMulti
|
||||||
|
name="userIds"
|
||||||
|
label="Assignees"
|
||||||
|
tio="People who are responsible for dealing with this issue."
|
||||||
|
options={userOptions}
|
||||||
|
renderOption={renderUser}
|
||||||
|
renderValue={renderUser}
|
||||||
|
/>
|
||||||
|
<Form.Field.Select
|
||||||
|
name="priority"
|
||||||
|
label="Priority"
|
||||||
|
tip="Priority in relation to other issues."
|
||||||
|
options={priorityOptions}
|
||||||
|
renderOption={renderPriority}
|
||||||
|
renderValue={renderPriority}
|
||||||
|
/>
|
||||||
|
<Actions>
|
||||||
|
<ActionButton type="submit" color="primary" working={isCreating}>
|
||||||
|
Create Issue
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton color="empty" onClick={modalClose}>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
|
</Actions>
|
||||||
|
</FormElement>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectIssueCreateForm.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectIssueCreateForm;
|
||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||||
import { isFocusedElementEditable } from 'shared/utils/dom';
|
import { isFocusedElementEditable } from 'shared/utils/dom';
|
||||||
import { Tip, TipLetter } from './Style';
|
import { Tip, TipLetter } from './Styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
setFormOpen: PropTypes.func.isRequired,
|
setFormOpen: PropTypes.func.isRequired,
|
||||||
@@ -6,7 +6,7 @@ import useApi from 'shared/hooks/api';
|
|||||||
import toast from 'shared/utils/toast';
|
import toast from 'shared/utils/toast';
|
||||||
import BodyForm from '../BodyForm';
|
import BodyForm from '../BodyForm';
|
||||||
import ProTip from './ProTip';
|
import ProTip from './ProTip';
|
||||||
import { Create, UserAvatar, Right, FakeTextarea } from './Style';
|
import { Create, UserAvatar, Right, FakeTextarea } from './Styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
issueId: PropTypes.number.isRequired,
|
issueId: PropTypes.number.isRequired,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { getTextContentsFromHtmlString } from 'shared/utils/html';
|
import { getTextContentsFromHtmlString } from 'shared/utils/html';
|
||||||
@@ -11,34 +11,30 @@ const propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||||
const $editorRef = useRef();
|
const [value, setValue] = useState(issue.description);
|
||||||
const [isPresenting, setPresenting] = useState(true);
|
const [isEditing, setEditing] = useState(false);
|
||||||
|
|
||||||
const renderPresentingMode = () =>
|
const renderPresentingMode = () =>
|
||||||
isDescriptionEmpty(issue.description) ? (
|
isDescriptionEmpty(issue.description) ? (
|
||||||
<EmptyLabel onClick={() => setPresenting(false)}>Add a description...</EmptyLabel>
|
<EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>
|
||||||
) : (
|
) : (
|
||||||
<TextEditedContent content={issue.description} onClick={() => setPresenting(false)} />
|
<TextEditedContent content={issue.description} onClick={() => setEditing(true)} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderEditingMode = () => (
|
const renderEditingMode = () => (
|
||||||
<>
|
<>
|
||||||
<TextEditor
|
<TextEditor placeholder="Describe the issue" defaultValue={value} onChange={setValue} />
|
||||||
placeholder="Describe the issue"
|
|
||||||
defaultValue={issue.description}
|
|
||||||
getEditor={editor => ($editorRef.current = editor)}
|
|
||||||
/>
|
|
||||||
<Actions>
|
<Actions>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPresenting(true);
|
setEditing(false);
|
||||||
updateIssue({ description: $editorRef.current.getHTML() });
|
updateIssue({ description: value });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="empty" onClick={() => setPresenting(true)}>
|
<Button color="empty" onClick={() => setEditing(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</Actions>
|
</Actions>
|
||||||
@@ -47,7 +43,7 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Description</Title>
|
<Title>Description</Title>
|
||||||
{isPresenting ? renderPresentingMode() : renderEditingMode()}
|
{isEditing ? renderEditingMode() : renderPresentingMode()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -18,7 +18,6 @@ export const Priority = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Label = styled.div`
|
export const Label = styled.div`
|
||||||
text-transform: capitalize;
|
|
||||||
padding: 0 3px 0 8px;
|
padding: 0 3px 0 8px;
|
||||||
${font.size(14.5)}
|
${font.size(14.5)}
|
||||||
`;
|
`;
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { invert } from 'lodash';
|
|
||||||
|
|
||||||
import { IssuePriority } from 'shared/constants/issues';
|
import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';
|
||||||
import { Select, IssuePriorityIcon } from 'shared/components';
|
import { Select, IssuePriorityIcon } from 'shared/components';
|
||||||
import { Priority, Label } from './Styles';
|
import { Priority, Label } from './Styles';
|
||||||
import { SectionTitle } from '../Styles';
|
import { SectionTitle } from '../Styles';
|
||||||
|
|
||||||
const IssuePriorityCopy = invert(IssuePriority);
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
issue: PropTypes.object.isRequired,
|
issue: PropTypes.object.isRequired,
|
||||||
updateIssue: PropTypes.func.isRequired,
|
updateIssue: PropTypes.func.isRequired,
|
||||||
@@ -18,13 +15,14 @@ const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => {
|
|||||||
const renderPriorityItem = (priority, isValue) => (
|
const renderPriorityItem = (priority, isValue) => (
|
||||||
<Priority isValue={isValue}>
|
<Priority isValue={isValue}>
|
||||||
<IssuePriorityIcon priority={priority} />
|
<IssuePriorityIcon priority={priority} />
|
||||||
<Label>{IssuePriorityCopy[priority].toLowerCase()}</Label>
|
<Label>{IssuePriorityCopy[priority]}</Label>
|
||||||
</Priority>
|
</Priority>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionTitle>Priority</SectionTitle>
|
<SectionTitle>Priority</SectionTitle>
|
||||||
<Select
|
<Select
|
||||||
|
variant="empty"
|
||||||
dropdownWidth={343}
|
dropdownWidth={343}
|
||||||
value={issue.priority}
|
value={issue.priority}
|
||||||
options={Object.values(IssuePriority).map(priority => ({
|
options={Object.values(IssuePriority).map(priority => ({
|
||||||
@@ -15,6 +15,7 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
|
|||||||
<>
|
<>
|
||||||
<SectionTitle>Status</SectionTitle>
|
<SectionTitle>Status</SectionTitle>
|
||||||
<Select
|
<Select
|
||||||
|
variant="empty"
|
||||||
dropdownWidth={343}
|
dropdownWidth={343}
|
||||||
value={issue.status}
|
value={issue.status}
|
||||||
options={Object.values(IssueStatus).map(status => ({
|
options={Object.values(IssueStatus).map(status => ({
|
||||||
21
client/src/Project/IssueDetails/Type/Styles.js
Normal file
21
client/src/Project/IssueDetails/Type/Styles.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { color, font } from 'shared/utils/styles';
|
||||||
|
import { Button } from 'shared/components';
|
||||||
|
|
||||||
|
export const TypeButton = styled(Button)`
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: ${color.textMedium};
|
||||||
|
${font.size(13)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Type = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TypeLabel = styled.div`
|
||||||
|
padding: 0 5px 0 7px;
|
||||||
|
${font.size(15)}
|
||||||
|
`;
|
||||||
39
client/src/Project/IssueDetails/Type/index.jsx
Normal file
39
client/src/Project/IssueDetails/Type/index.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { IssueType, IssueTypeCopy } from 'shared/constants/issues';
|
||||||
|
import { IssueTypeIcon, Select } from 'shared/components';
|
||||||
|
import { TypeButton, Type, TypeLabel } from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
issue: PropTypes.object.isRequired,
|
||||||
|
updateIssue: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
|
||||||
|
<Select
|
||||||
|
variant="empty"
|
||||||
|
dropdownWidth={150}
|
||||||
|
value={issue.type}
|
||||||
|
options={Object.values(IssueType).map(type => ({
|
||||||
|
value: type,
|
||||||
|
label: IssueTypeCopy[type],
|
||||||
|
}))}
|
||||||
|
onChange={type => updateIssue({ type })}
|
||||||
|
renderValue={({ value: type }) => (
|
||||||
|
<TypeButton color="empty" icon={<IssueTypeIcon type={type} />}>
|
||||||
|
{`${type}-${issue.id}`}
|
||||||
|
</TypeButton>
|
||||||
|
)}
|
||||||
|
renderOption={({ value: type }) => (
|
||||||
|
<Type key={type} onClick={() => updateIssue({ type })}>
|
||||||
|
<IssueTypeIcon type={type} top={1} />
|
||||||
|
<TypeLabel>{IssueTypeCopy[type]}</TypeLabel>
|
||||||
|
</Type>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
ProjectBoardIssueDetailsType.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardIssueDetailsType;
|
||||||
@@ -3,7 +3,7 @@ import styled, { css } from 'styled-components';
|
|||||||
import { color, font, mixin } from 'shared/utils/styles';
|
import { color, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const User = styled.div`
|
export const User = styled.div`
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
${mixin.clickable}
|
${mixin.clickable}
|
||||||
${props =>
|
${props =>
|
||||||
@@ -16,12 +16,12 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
|||||||
|
|
||||||
const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));
|
const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));
|
||||||
|
|
||||||
const renderUserValue = (user, withBottomMargin, removeOptionValue) => (
|
const renderUser = (user, isSelectValue, removeOptionValue) => (
|
||||||
<User
|
<User
|
||||||
key={user.id}
|
key={user.id}
|
||||||
isSelectValue
|
isSelectValue={isSelectValue}
|
||||||
withBottomMargin={withBottomMargin}
|
withBottomMargin={!!removeOptionValue}
|
||||||
onClick={() => removeOptionValue && removeOptionValue(user.id)}
|
onClick={() => removeOptionValue && removeOptionValue()}
|
||||||
>
|
>
|
||||||
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />
|
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />
|
||||||
<Username>{user.name}</Username>
|
<Username>{user.name}</Username>
|
||||||
@@ -29,18 +29,12 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
|||||||
</User>
|
</User>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderUserOption = user => (
|
|
||||||
<User key={user.id}>
|
|
||||||
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={32} />
|
|
||||||
<Username>{user.name}</Username>
|
|
||||||
</User>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderAssignees = () => (
|
const renderAssignees = () => (
|
||||||
<>
|
<>
|
||||||
<SectionTitle>Assignees</SectionTitle>
|
<SectionTitle>Assignees</SectionTitle>
|
||||||
<Select
|
<Select
|
||||||
isMulti
|
isMulti
|
||||||
|
variant="empty"
|
||||||
dropdownWidth={343}
|
dropdownWidth={343}
|
||||||
placeholder="Unassigned"
|
placeholder="Unassigned"
|
||||||
value={issue.userIds}
|
value={issue.userIds}
|
||||||
@@ -49,9 +43,9 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
|||||||
updateIssue({ userIds, users: userIds.map(getUserById) });
|
updateIssue({ userIds, users: userIds.map(getUserById) });
|
||||||
}}
|
}}
|
||||||
renderValue={({ value, removeOptionValue }) =>
|
renderValue={({ value, removeOptionValue }) =>
|
||||||
renderUserValue(getUserById(value), true, removeOptionValue)
|
renderUser(getUserById(value), true, removeOptionValue)
|
||||||
}
|
}
|
||||||
renderOption={({ value }) => renderUserOption(getUserById(value))}
|
renderOption={({ value }) => renderUser(getUserById(value), false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -60,12 +54,13 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
|||||||
<>
|
<>
|
||||||
<SectionTitle>Reporter</SectionTitle>
|
<SectionTitle>Reporter</SectionTitle>
|
||||||
<Select
|
<Select
|
||||||
|
variant="empty"
|
||||||
dropdownWidth={343}
|
dropdownWidth={343}
|
||||||
value={issue.reporterId}
|
value={issue.reporterId}
|
||||||
options={userOptions}
|
options={userOptions}
|
||||||
onChange={userId => updateIssue({ reporterId: userId })}
|
onChange={userId => updateIssue({ reporterId: userId })}
|
||||||
renderValue={({ value }) => renderUserValue(getUserById(value), false)}
|
renderValue={({ value }) => renderUser(getUserById(value), true)}
|
||||||
renderOption={({ value }) => renderUserOption(getUserById(value))}
|
renderOption={({ value }) => renderUser(getUserById(value))}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -16,7 +16,7 @@ export const NavLeft = styled.aside`
|
|||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
${mixin.hardwareAccelerate}
|
${mixin.hardwareAccelerate}
|
||||||
&:hover {
|
&:hover {
|
||||||
width: 180px;
|
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);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -48,7 +48,7 @@ export const Item = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
line-height: 42px;
|
line-height: 42px;
|
||||||
padding-left: 67px;
|
padding-left: 64px;
|
||||||
color: #deebff;
|
color: #deebff;
|
||||||
transition: color 0.1s;
|
transition: color 0.1s;
|
||||||
${mixin.clickable}
|
${mixin.clickable}
|
||||||
34
client/src/Project/NavbarLeft/index.jsx
Normal file
34
client/src/Project/NavbarLeft/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Icon } from 'shared/components';
|
||||||
|
import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';
|
||||||
|
|
||||||
|
const ProjectNavbarLeft = () => {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
return (
|
||||||
|
<NavLeft>
|
||||||
|
<LogoLink to="/">
|
||||||
|
<StyledLogo color="#fff" />
|
||||||
|
</LogoLink>
|
||||||
|
<Item>
|
||||||
|
<Icon type="search" size={22} top={1} left={3} />
|
||||||
|
<ItemText>Search issues</ItemText>
|
||||||
|
</Item>
|
||||||
|
<Link to={`${match.path}/board/create-issue`}>
|
||||||
|
<Item>
|
||||||
|
<Icon type="plus" size={27} />
|
||||||
|
<ItemText>Create Issue</ItemText>
|
||||||
|
</Item>
|
||||||
|
</Link>
|
||||||
|
<Bottom>
|
||||||
|
<Item>
|
||||||
|
<Icon type="help" size={25} />
|
||||||
|
<ItemText>Help</ItemText>
|
||||||
|
</Item>
|
||||||
|
</Bottom>
|
||||||
|
</NavLeft>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectNavbarLeft;
|
||||||
@@ -79,7 +79,7 @@ export const NotImplemented = styled.div`
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 9px;
|
top: 9px;
|
||||||
left: 104%;
|
left: 101%;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
padding: 3px 0 3px 8px;
|
padding: 3px 0 3px 8px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import styled from 'styled-components';
|
|||||||
import { sizes } from 'shared/utils/styles';
|
import { sizes } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const ProjectPage = styled.div`
|
export const ProjectPage = styled.div`
|
||||||
padding: 25px 32px 0 ${sizes.secondarySideBarWidth + 40}px;
|
padding: 25px 32px 0 ${sizes.appNavBarLeftWidth + sizes.secondarySideBarWidth + 40}px;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, Redirect, useRouteMatch } from 'react-router-dom';
|
import { Route, Redirect, useRouteMatch, useHistory } 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 { PageLoader, PageError } from 'shared/components';
|
import { PageLoader, PageError, Modal } from 'shared/components';
|
||||||
|
import NavbarLeft from './NavbarLeft';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Board from './Board';
|
import Board from './Board';
|
||||||
|
import IssueDetails from './IssueDetails';
|
||||||
|
import IssueCreateForm from './IssueCreateForm';
|
||||||
import { ProjectPage } from './Styles';
|
import { ProjectPage } from './Styles';
|
||||||
|
|
||||||
const Project = () => {
|
const Project = () => {
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');
|
const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');
|
||||||
|
|
||||||
const updateLocalIssuesArray = (issueId, updatedFields) => {
|
const updateLocalIssuesArray = (issueId, updatedFields) => {
|
||||||
@@ -33,15 +38,40 @@ const Project = () => {
|
|||||||
updateLocalIssuesArray={updateLocalIssuesArray}
|
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const renderSettings = () => <h1>SETTINGS</h1>;
|
const renderIssueDetailsModal = routeProps => (
|
||||||
const renderIssues = () => <h1>ISSUES</h1>;
|
<Modal
|
||||||
|
isOpen
|
||||||
|
width={1040}
|
||||||
|
withCloseIcon={false}
|
||||||
|
onClose={() => history.push(match.url)}
|
||||||
|
renderContent={modal => (
|
||||||
|
<IssueDetails
|
||||||
|
issueId={routeProps.match.params.issueId}
|
||||||
|
projectUsers={project.users}
|
||||||
|
fetchProject={fetchProject}
|
||||||
|
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||||
|
modalClose={modal.close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const renderIssueCreateModal = () => (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
width={800}
|
||||||
|
onClose={() => history.push(match.url)}
|
||||||
|
renderContent={modal => (
|
||||||
|
<IssueCreateForm project={project} fetchProject={fetchProject} modalClose={modal.close} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<ProjectPage>
|
<ProjectPage>
|
||||||
|
<NavbarLeft />
|
||||||
<Sidebar projectName={project.name} matchPath={match.path} />
|
<Sidebar projectName={project.name} matchPath={match.path} />
|
||||||
<Route path={`${match.path}/board`} render={renderBoard} />
|
<Route path={`${match.path}/board`} render={renderBoard} />
|
||||||
<Route path={`${match.path}/settings`} render={renderSettings} />
|
<Route path={`${match.path}/board/create-issue`} render={renderIssueCreateModal} />
|
||||||
<Route path={`${match.path}/issues`} render={renderIssues} />
|
<Route path={`${match.path}/board/issue/:issueId`} render={renderIssueDetailsModal} />
|
||||||
{match.isExact && <Redirect to={`${match.url}/board`} />}
|
{match.isExact && <Redirect to={`${match.url}/board`} />}
|
||||||
</ProjectPage>
|
</ProjectPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'regenerator-runtime/runtime';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import App from 'components/App/App';
|
import App from 'App';
|
||||||
|
|
||||||
|
// TODO: UPDATE FORMIK TO FIX SETFIELDVALUE TO EMPTY ARRAY ISSUE https://github.com/jaredpalmer/formik/pull/2144
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
|||||||
51
client/src/shared/components/Form/Field.jsx
Normal file
51
client/src/shared/components/Form/Field.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
|
||||||
|
import Input from 'shared/components/Input';
|
||||||
|
import Select from 'shared/components/Select';
|
||||||
|
import Textarea from 'shared/components/Textarea';
|
||||||
|
import TextEditor from 'shared/components/TextEditor';
|
||||||
|
import DatePicker from 'shared/components/DatePicker';
|
||||||
|
import { StyledField, FieldLabel, FieldTip, FieldError } from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
tip: PropTypes.string,
|
||||||
|
error: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
label: null,
|
||||||
|
tip: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateField = FormComponent => {
|
||||||
|
const FieldComponent = ({ className, label, tip, error, ...props }) => {
|
||||||
|
const fieldId = uniqueId('form-field-');
|
||||||
|
return (
|
||||||
|
<StyledField className={className} hasLabel={!!label}>
|
||||||
|
{label && <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>}
|
||||||
|
<FormComponent id={fieldId} invalid={!!error} {...props} />
|
||||||
|
{tip && <FieldTip>{tip}</FieldTip>}
|
||||||
|
{error && <FieldError>{error}</FieldError>}
|
||||||
|
</StyledField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FieldComponent.propTypes = propTypes;
|
||||||
|
FieldComponent.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
return FieldComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Input: generateField(Input),
|
||||||
|
Select: generateField(Select),
|
||||||
|
Textarea: generateField(Textarea),
|
||||||
|
TextEditor: generateField(TextEditor),
|
||||||
|
DatePicker: generateField(DatePicker),
|
||||||
|
};
|
||||||
29
client/src/shared/components/Form/Styles.js
Normal file
29
client/src/shared/components/Form/Styles.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { color, font } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const StyledField = styled.div`
|
||||||
|
margin-top: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FieldLabel = styled.label`
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
color: ${color.textMedium};
|
||||||
|
${font.medium}
|
||||||
|
${font.size(13)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FieldTip = styled.div`
|
||||||
|
padding-top: 6px;
|
||||||
|
color: ${color.textMedium};
|
||||||
|
${font.size(12.5)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FieldError = styled.div`
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1;
|
||||||
|
color: ${color.danger};
|
||||||
|
${font.medium}
|
||||||
|
${font.size(12.5)}
|
||||||
|
`;
|
||||||
57
client/src/shared/components/Form/index.jsx
Normal file
57
client/src/shared/components/Form/index.jsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Formik, Form as FormikForm, Field as FormikField } from 'formik';
|
||||||
|
import { get, mapValues } from 'lodash';
|
||||||
|
|
||||||
|
import { is, generateErrors } from 'shared/utils/validation';
|
||||||
|
import Field from './Field';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
validate: PropTypes.func,
|
||||||
|
validations: PropTypes.object,
|
||||||
|
validateOnBlur: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
validate: undefined,
|
||||||
|
validations: undefined,
|
||||||
|
validateOnBlur: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Form = ({ validate, validations, ...otherProps }) => (
|
||||||
|
<Formik
|
||||||
|
{...otherProps}
|
||||||
|
validate={values => {
|
||||||
|
if (validate) {
|
||||||
|
return validate(values);
|
||||||
|
}
|
||||||
|
if (validations) {
|
||||||
|
return generateErrors(values, validations);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
Form.Element = props => <FormikForm noValidate {...props} />;
|
||||||
|
|
||||||
|
Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) => (
|
||||||
|
<FormikField name={name} validate={validate}>
|
||||||
|
{({ field, form: { touched, errors, setFieldValue } }) => (
|
||||||
|
<FieldComponent
|
||||||
|
{...field}
|
||||||
|
{...props}
|
||||||
|
name={name}
|
||||||
|
error={get(touched, name) && get(errors, name)}
|
||||||
|
onChange={value => setFieldValue(name, value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormikField>
|
||||||
|
));
|
||||||
|
|
||||||
|
Form.is = is;
|
||||||
|
|
||||||
|
Form.propTypes = propTypes;
|
||||||
|
Form.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default Form;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
import { color, font } from 'shared/utils/styles';
|
import { color, font } from 'shared/utils/styles';
|
||||||
|
|
||||||
@@ -25,8 +25,16 @@ export default styled.div`
|
|||||||
border: 1px solid ${color.borderInputFocus};
|
border: 1px solid ${color.borderInputFocus};
|
||||||
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||||
}
|
}
|
||||||
${props => (props.icon ? 'padding-left: 32px;' : '')}
|
${props => props.icon && 'padding-left: 32px;'}
|
||||||
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
${props =>
|
||||||
|
props.invalid &&
|
||||||
|
css`
|
||||||
|
&,
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid ${color.danger};
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
`}
|
||||||
}
|
}
|
||||||
i {
|
i {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const modalStyles = {
|
|||||||
max-width: ${props => props.width}px;
|
max-width: ${props => props.width}px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
border-radius: 3px;
|
||||||
${mixin.boxShadowMedium}
|
${mixin.boxShadowMedium}
|
||||||
`,
|
`,
|
||||||
aside: css`
|
aside: css`
|
||||||
|
|||||||
@@ -7,27 +7,35 @@ export const StyledSelect = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: #fff;
|
|
||||||
${font.size(14)}
|
${font.size(14)}
|
||||||
${props => props.variant === 'empty' && `display: inline-block;`}
|
${props => props.variant === 'empty' && `display: inline-block;`}
|
||||||
${props =>
|
${props =>
|
||||||
props.variant === 'border' &&
|
props.variant === 'normal' &&
|
||||||
css`
|
css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid ${color.borderLightest};
|
border: 1px solid ${color.borderLightest};
|
||||||
|
background: ${color.backgroundLightest};
|
||||||
`}
|
`}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
${props =>
|
${props =>
|
||||||
props.variant === 'border' &&
|
props.variant === 'normal' &&
|
||||||
css`
|
css`
|
||||||
background: #fff;
|
|
||||||
border: 1px solid ${color.borderInputFocus};
|
border: 1px solid ${color.borderInputFocus};
|
||||||
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
}
|
}
|
||||||
${props => props.invalid && `&, &:focus { border: 1px solid ${color.danger}; }`}
|
${props =>
|
||||||
|
props.invalid &&
|
||||||
|
css`
|
||||||
|
&,
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid ${color.danger};
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ValueContainer = styled.div`
|
export const ValueContainer = styled.div`
|
||||||
@@ -35,10 +43,10 @@ export const ValueContainer = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
${props =>
|
${props =>
|
||||||
props.variant === 'border' &&
|
props.variant === 'normal' &&
|
||||||
css`
|
css`
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding: 8px 5px 8px 10px;
|
padding: 5px 5px 5px 10px;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -56,7 +64,7 @@ export const ValueMulti = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
${props => props.variant === 'border' && `padding-top: 5px;`}
|
${props => props.variant === 'normal' && `padding-top: 5px;`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ValueMultiItem = styled.div`
|
export const ValueMultiItem = styled.div`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
variant: PropTypes.oneOf(['border', 'empty']),
|
variant: PropTypes.oneOf(['normal', 'empty']),
|
||||||
dropdownWidth: PropTypes.number,
|
dropdownWidth: PropTypes.number,
|
||||||
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||||
defaultValue: PropTypes.any,
|
defaultValue: PropTypes.any,
|
||||||
@@ -33,7 +33,7 @@ const propTypes = {
|
|||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: undefined,
|
className: undefined,
|
||||||
variant: 'empty',
|
variant: 'normal',
|
||||||
dropdownWidth: undefined,
|
dropdownWidth: undefined,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
@@ -134,7 +134,10 @@ const Select = ({
|
|||||||
<ValueMulti variant={variant}>
|
<ValueMulti variant={variant}>
|
||||||
{value.map(optionValue =>
|
{value.map(optionValue =>
|
||||||
propsRenderValue ? (
|
propsRenderValue ? (
|
||||||
propsRenderValue({ value: optionValue, removeOptionValue })
|
propsRenderValue({
|
||||||
|
value: optionValue,
|
||||||
|
removeOptionValue: () => removeOptionValue(optionValue),
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>
|
<ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>
|
||||||
{getOptionLabel(optionValue)}
|
{getOptionLabel(optionValue)}
|
||||||
|
|||||||
@@ -10,47 +10,68 @@ const propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
defaultValue: PropTypes.string,
|
defaultValue: PropTypes.string,
|
||||||
getEditor: PropTypes.func.isRequired,
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
getEditor: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: undefined,
|
className: undefined,
|
||||||
placeholder: undefined,
|
placeholder: undefined,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
|
value: undefined,
|
||||||
|
onChange: () => {},
|
||||||
|
getEditor: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextEditor = ({ className, placeholder, defaultValue, getEditor, ...otherProps }) => {
|
const TextEditor = ({
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
defaultValue,
|
||||||
|
// we're not really feeding new value to quill instance on each render because it's too
|
||||||
|
// expensive, but we're still accepting 'value' prop as alias for defaultValue because
|
||||||
|
// other components like <Form.Field> feed their children with data via the 'value' prop
|
||||||
|
value: alsoDefaultValue,
|
||||||
|
onChange,
|
||||||
|
getEditor,
|
||||||
|
}) => {
|
||||||
const $editorContRef = useRef();
|
const $editorContRef = useRef();
|
||||||
const $editorRef = useRef();
|
const $editorRef = useRef();
|
||||||
|
const quillRef = useRef();
|
||||||
|
const initialValueRef = useRef(defaultValue || alsoDefaultValue || '');
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
let editor = null;
|
const setupQuill = () => {
|
||||||
|
quillRef.current = new Quill($editorRef.current, { placeholder, ...quillConfig });
|
||||||
const setup = () => {
|
|
||||||
editor = new Quill($editorRef.current, { placeholder, ...editorConfig });
|
|
||||||
|
|
||||||
editor.clipboard.dangerouslyPasteHTML(0, defaultValue);
|
|
||||||
|
|
||||||
getEditor({
|
|
||||||
getHTML: () => $editorContRef.current.querySelector('.ql-editor').innerHTML,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
setup();
|
const insertInitialValue = () => {
|
||||||
|
quillRef.current.clipboard.dangerouslyPasteHTML(0, initialValueRef.current);
|
||||||
|
};
|
||||||
|
const handleContentsChange = () => {
|
||||||
|
onChange(getHTMLValue());
|
||||||
|
};
|
||||||
|
const getHTMLValue = () => $editorContRef.current.querySelector('.ql-editor').innerHTML;
|
||||||
|
|
||||||
|
setupQuill();
|
||||||
|
insertInitialValue();
|
||||||
|
getEditor({ getValue: getHTMLValue });
|
||||||
|
|
||||||
|
quillRef.current.on('text-change', handleContentsChange);
|
||||||
return () => {
|
return () => {
|
||||||
editor = null;
|
quillRef.current.off('text-change', handleContentsChange);
|
||||||
|
quillRef.current = null;
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorCont className={className} ref={$editorContRef}>
|
<EditorCont className={className} ref={$editorContRef}>
|
||||||
<div ref={$editorRef} {...otherProps} />
|
<div ref={$editorRef} />
|
||||||
</EditorCont>
|
</EditorCont>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editorConfig = {
|
const quillConfig = {
|
||||||
theme: 'snow',
|
theme: 'snow',
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: [
|
toolbar: [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export { default as Button } from './Button';
|
|||||||
export { default as ConfirmModal } from './ConfirmModal';
|
export { default as ConfirmModal } from './ConfirmModal';
|
||||||
export { default as CopyLinkButton } from './CopyLinkButton';
|
export { default as CopyLinkButton } from './CopyLinkButton';
|
||||||
export { default as DatePicker } from './DatePicker';
|
export { default as DatePicker } from './DatePicker';
|
||||||
export { default as Tooltip } from './Tooltip';
|
export { default as Form } from './Form';
|
||||||
export { default as Icon } from './Icon';
|
export { default as Icon } from './Icon';
|
||||||
export { default as Input } from './Input';
|
export { default as Input } from './Input';
|
||||||
export { default as InputDebounced } from './InputDebounced';
|
export { default as InputDebounced } from './InputDebounced';
|
||||||
@@ -19,3 +19,4 @@ export { default as Spinner } from './Spinner';
|
|||||||
export { default as Textarea } from './Textarea';
|
export { default as Textarea } from './Textarea';
|
||||||
export { default as TextEditedContent } from './TextEditedContent';
|
export { default as TextEditedContent } from './TextEditedContent';
|
||||||
export { default as TextEditor } from './TextEditor';
|
export { default as TextEditor } from './TextEditor';
|
||||||
|
export { default as Tooltip } from './Tooltip';
|
||||||
|
|||||||
@@ -19,9 +19,23 @@ export const IssuePriority = {
|
|||||||
LOWEST: '1',
|
LOWEST: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IssueTypeCopy = {
|
||||||
|
[IssueType.TASK]: 'Task',
|
||||||
|
[IssueType.BUG]: 'Bug',
|
||||||
|
[IssueType.STORY]: 'Story',
|
||||||
|
};
|
||||||
|
|
||||||
export const IssueStatusCopy = {
|
export const IssueStatusCopy = {
|
||||||
[IssueStatus.BACKLOG]: 'Backlog',
|
[IssueStatus.BACKLOG]: 'Backlog',
|
||||||
[IssueStatus.SELECTED]: 'Selected for development',
|
[IssueStatus.SELECTED]: 'Selected for development',
|
||||||
[IssueStatus.INPROGRESS]: 'In progress',
|
[IssueStatus.INPROGRESS]: 'In progress',
|
||||||
[IssueStatus.DONE]: 'Done',
|
[IssueStatus.DONE]: 'Done',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IssuePriorityCopy = {
|
||||||
|
[IssuePriority.HIGHEST]: 'Highest',
|
||||||
|
[IssuePriority.HIGH]: 'High',
|
||||||
|
[IssuePriority.MEDIUM]: 'Medium',
|
||||||
|
[IssuePriority.LOW]: 'Low',
|
||||||
|
[IssuePriority.LOWEST]: 'Lowest',
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const useApi = (method, url, variables = {}, { lazy = false } = {}) => {
|
|||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
data: null,
|
data: null,
|
||||||
error: null,
|
error: null,
|
||||||
isLoading: isCalledAutomatically,
|
isWorking: isCalledAutomatically,
|
||||||
additionalVariables: {},
|
additionalVariables: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,17 +27,17 @@ const useApi = (method, url, variables = {}, { lazy = false } = {}) => {
|
|||||||
const additionalVariables = { ...stateRef.current.additionalVariables, ...newVariables };
|
const additionalVariables = { ...stateRef.current.additionalVariables, ...newVariables };
|
||||||
|
|
||||||
if (!isCalledAutomatically || wasCalledRef.current) {
|
if (!isCalledAutomatically || wasCalledRef.current) {
|
||||||
setStateMerge({ additionalVariables, isLoading: true });
|
setStateMerge({ additionalVariables, isWorking: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
api[method](url, { ...variablesMemoized, ...additionalVariables }).then(
|
api[method](url, { ...variablesMemoized, ...additionalVariables }).then(
|
||||||
data => {
|
data => {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
setStateMerge({ data, error: null, isLoading: false });
|
setStateMerge({ data, error: null, isWorking: false });
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
setStateMerge({ error, data: null, isLoading: false });
|
setStateMerge({ error, data: null, isWorking: false });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ const useApi = (method, url, variables = {}, { lazy = false } = {}) => {
|
|||||||
const result = [
|
const result = [
|
||||||
{
|
{
|
||||||
...state,
|
...state,
|
||||||
|
[isWorkingAlias[method]]: state.isWorking,
|
||||||
wasCalled: wasCalledRef.current,
|
wasCalled: wasCalledRef.current,
|
||||||
variables: { ...variablesMemoized, ...state.additionalVariables },
|
variables: { ...variablesMemoized, ...state.additionalVariables },
|
||||||
setLocalData,
|
setLocalData,
|
||||||
@@ -71,6 +72,14 @@ const useApi = (method, url, variables = {}, { lazy = false } = {}) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWorkingAlias = {
|
||||||
|
get: 'isLoading',
|
||||||
|
post: 'isCreating',
|
||||||
|
put: 'isUpdating',
|
||||||
|
patch: 'isUpdating',
|
||||||
|
delete: 'isDeleting',
|
||||||
|
};
|
||||||
|
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
export default {
|
export default {
|
||||||
get: (...args) => useApi('get', ...args),
|
get: (...args) => useApi('get', ...args),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const defaults = {
|
|||||||
code: 'INTERNAL_ERROR',
|
code: 'INTERNAL_ERROR',
|
||||||
message: 'Something went wrong. Please check your internet connection or contact our support.',
|
message: 'Something went wrong. Please check your internet connection or contact our support.',
|
||||||
status: 503,
|
status: 503,
|
||||||
|
data: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export const mixin = {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
tag: (background = color.backgroundLight, colorValue = color.textDarkest) => `
|
tag: (background = color.backgroundMedium, colorValue = color.textDarkest) => `
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|||||||
@@ -16,5 +16,6 @@
|
|||||||
"hooks": {
|
"hooks": {
|
||||||
"pre-commit": "npm run pre-commit"
|
"pre-commit": "npm run pre-commit"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user