Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
8d724fa3cf | |||
76e398488f | |||
d1b867db35 | |||
aeb97a30d8 | |||
56e925a48d | |||
65cd431c1a | |||
a188c4b0ca |
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [0.4.0] - 2021-09-04
|
## [0.3.5] - 2021-09-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Project visibility can now be set to public - meaning anyone can view the project board
|
- Project visibility can now be set to public - meaning anyone can view the project board
|
||||||
|
@ -12,7 +12,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- taskcafe-postgres:/var/lib/postgresql/data
|
- taskcafe-postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 8855:5432
|
- 8865:5432
|
||||||
mailhog:
|
mailhog:
|
||||||
image: mailhog/mailhog:latest
|
image: mailhog/mailhog:latest
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "warn",
|
"prettier/prettier": "warn",
|
||||||
|
"no-shadow": "off",
|
||||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
@ -34,6 +35,7 @@
|
|||||||
"no-case-declarations": "off",
|
"no-case-declarations": "off",
|
||||||
"no-plusplus": "off",
|
"no-plusplus": "off",
|
||||||
"react/prop-types": 0,
|
"react/prop-types": 0,
|
||||||
|
"react/no-unused-prop-types": "off",
|
||||||
"no-continue": "off",
|
"no-continue": "off",
|
||||||
"react/jsx-props-no-spreading": "off",
|
"react/jsx-props-no-spreading": "off",
|
||||||
"no-param-reassign": "off",
|
"no-param-reassign": "off",
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@types/node": "^15.0.1",
|
"@types/node": "^15.0.1",
|
||||||
"@types/react": "^17.0.4",
|
"@types/react": "^17.0.20",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-datepicker": "^3.1.8",
|
"@types/react-datepicker": "^3.1.8",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router": "^5.1.13",
|
"@types/react-router": "^5.1.13",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-select": "^4.0.15",
|
"@types/react-select": "^4.0.15",
|
||||||
@ -62,7 +62,8 @@
|
|||||||
"react-toastify": "^7.0.4",
|
"react-toastify": "^7.0.4",
|
||||||
"rich-markdown-editor": "^11.17.4-0",
|
"rich-markdown-editor": "^11.17.4-0",
|
||||||
"styled-components": "^5.2.3",
|
"styled-components": "^5.2.3",
|
||||||
"typescript": "~4.2.4"
|
"typescript": "~4.2.4",
|
||||||
|
"unist-util-visit": "^4.0.0"
|
||||||
},
|
},
|
||||||
"proxy": "http://localhost:3333",
|
"proxy": "http://localhost:3333",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -223,7 +223,7 @@ TODO: add permision check
|
|||||||
users={data.users}
|
users={data.users}
|
||||||
invitedUsers={data.invitedUsers}
|
invitedUsers={data.invitedUsers}
|
||||||
// canInviteUser={user.roles.org === 'admin'} TODO: add permision check
|
// canInviteUser={user.roles.org === 'admin'} TODO: add permision check
|
||||||
canInviteUser={true}
|
canInviteUser
|
||||||
onInviteUser={NOOP}
|
onInviteUser={NOOP}
|
||||||
onUpdateUserPassword={() => {
|
onUpdateUserPassword={() => {
|
||||||
hidePopup();
|
hidePopup();
|
||||||
|
@ -26,10 +26,10 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
|||||||
import DueDateManager from 'shared/components/DueDateManager';
|
import DueDateManager from 'shared/components/DueDateManager';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import useStickyState from 'shared/hooks/useStickyState';
|
import useStickyState from 'shared/hooks/useStickyState';
|
||||||
|
import { StaticContext } from 'react-router';
|
||||||
import MyTasksSortPopup from './MyTasksSort';
|
import MyTasksSortPopup from './MyTasksSort';
|
||||||
import MyTasksStatusPopup from './MyTasksStatus';
|
import MyTasksStatusPopup from './MyTasksStatus';
|
||||||
import TaskEntry from './TaskEntry';
|
import TaskEntry from './TaskEntry';
|
||||||
import { StaticContext } from 'react-router';
|
|
||||||
|
|
||||||
type TaskRouteProps = {
|
type TaskRouteProps = {
|
||||||
taskID: string;
|
taskID: string;
|
||||||
|
@ -44,7 +44,7 @@ const Projects = () => {
|
|||||||
name="file"
|
name="file"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
ref={$fileUpload}
|
ref={$fileUpload}
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
const fileData = new FormData();
|
const fileData = new FormData();
|
||||||
fileData.append('file', e.target.files[0]);
|
fileData.append('file', e.target.files[0]);
|
||||||
@ -52,7 +52,7 @@ const Projects = () => {
|
|||||||
.post('/users/me/avatar', fileData, {
|
.post('/users/me/avatar', fileData, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
if ($fileUpload && $fileUpload.current) {
|
if ($fileUpload && $fileUpload.current) {
|
||||||
$fileUpload.current.value = '';
|
$fileUpload.current.value = '';
|
||||||
refetch();
|
refetch();
|
||||||
@ -77,7 +77,7 @@ const Projects = () => {
|
|||||||
}}
|
}}
|
||||||
onChangeUserInfo={(d, done) => {
|
onChangeUserInfo={(d, done) => {
|
||||||
updateUserInfo({
|
updateUserInfo({
|
||||||
variables: { name: d.full_name, bio: d.bio, email: d.email, initials: d.initials },
|
variables: { name: d.fullName, bio: d.bio, email: d.email, initials: d.initials },
|
||||||
});
|
});
|
||||||
toast('User info was saved!');
|
toast('User info was saved!');
|
||||||
done();
|
done();
|
||||||
|
@ -7,12 +7,13 @@ import { Popup, usePopup } from 'shared/components/PopupMenu';
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import Member from 'shared/components/Member';
|
import Member from 'shared/components/Member';
|
||||||
|
import { useLabelsQuery } from 'shared/generated/graphql';
|
||||||
|
|
||||||
const FilterMember = styled(Member)`
|
const FilterMember = styled(Member)`
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: ${props => props.theme.colors.primary};
|
background: ${(props) => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ export const Label = styled.li`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
||||||
${props =>
|
${(props) =>
|
||||||
props.active &&
|
props.active &&
|
||||||
css`
|
css`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
@ -43,7 +44,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: padding 85ms, margin 85ms, box-shadow 85ms;
|
transition: padding 85ms, margin 85ms, box-shadow 85ms;
|
||||||
background-color: ${props => props.color};
|
background-color: ${(props) => props.color};
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -71,7 +72,7 @@ export const ActionItem = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${props => props.theme.colors.primary};
|
background: ${(props) => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ export const ActionTitle = styled.span`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ActionItemSeparator = styled.li`
|
const ActionItemSeparator = styled.li`
|
||||||
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
@ -110,15 +111,16 @@ const ActionItemLine = styled.div`
|
|||||||
type FilterMetaProps = {
|
type FilterMetaProps = {
|
||||||
filters: TaskMetaFilters;
|
filters: TaskMetaFilters;
|
||||||
userID: string;
|
userID: string;
|
||||||
labels: React.RefObject<Array<ProjectLabel>>;
|
projectID: string;
|
||||||
members: React.RefObject<Array<TaskUser>>;
|
members: React.RefObject<Array<TaskUser>>;
|
||||||
onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void;
|
onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter, userID, labels, members }) => {
|
const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter, userID, projectID, members }) => {
|
||||||
const [currentFilters, setFilters] = useState(filters);
|
const [currentFilters, setFilters] = useState(filters);
|
||||||
const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : '');
|
const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : '');
|
||||||
const [currentLabel, setCurrentLabel] = useState('');
|
const [currentLabel, setCurrentLabel] = useState('');
|
||||||
|
const { data } = useLabelsQuery({ variables: { projectID } });
|
||||||
|
|
||||||
const handleSetFilters = (f: TaskMetaFilters) => {
|
const handleSetFilters = (f: TaskMetaFilters) => {
|
||||||
setFilters(f);
|
setFilters(f);
|
||||||
@ -127,7 +129,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
|
|
||||||
const handleNameChange = (nFilter: string) => {
|
const handleNameChange = (nFilter: string) => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, draftFilters => {
|
produce(currentFilters, (draftFilters) => {
|
||||||
draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null;
|
draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -138,7 +140,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
|
|
||||||
const handleSetDueDate = (filterType: DueDateFilterType, label: string) => {
|
const handleSetDueDate = (filterType: DueDateFilterType, label: string) => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, draftFilters => {
|
produce(currentFilters, (draftFilters) => {
|
||||||
if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) {
|
if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) {
|
||||||
draftFilters.dueDate = null;
|
draftFilters.dueDate = null;
|
||||||
} else {
|
} else {
|
||||||
@ -157,7 +159,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
<ActionsList>
|
<ActionsList>
|
||||||
<TaskNameInput
|
<TaskNameInput
|
||||||
width="100%"
|
width="100%"
|
||||||
onChange={e => handleNameChange(e.currentTarget.value)}
|
onChange={(e) => handleNameChange(e.currentTarget.value)}
|
||||||
value={nameFilter}
|
value={nameFilter}
|
||||||
autoFocus
|
autoFocus
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
@ -167,14 +169,14 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, draftFilters => {
|
produce(currentFilters, (draftFilters) => {
|
||||||
if (members.current) {
|
if (members.current) {
|
||||||
const member = members.current.find(m => m.id === userID);
|
const member = members.current.find((m) => m.id === userID);
|
||||||
const draftMember = draftFilters.members.find(m => m.id === userID);
|
const draftMember = draftFilters.members.find((m) => m.id === userID);
|
||||||
if (member && !draftMember) {
|
if (member && !draftMember) {
|
||||||
draftFilters.members.push({ id: userID, username: member.username ? member.username : '' });
|
draftFilters.members.push({ id: userID, username: member.username ? member.username : '' });
|
||||||
} else {
|
} else {
|
||||||
draftFilters.members = draftFilters.members.filter(m => m.id !== userID);
|
draftFilters.members = draftFilters.members.filter((m) => m.id !== userID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -185,7 +187,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
<User width={12} height={12} />
|
<User width={12} height={12} />
|
||||||
</ItemIcon>
|
</ItemIcon>
|
||||||
<ActionTitle>Just my tasks</ActionTitle>
|
<ActionTitle>Just my tasks</ActionTitle>
|
||||||
{currentFilters.members.find(m => m.id === userID) && <ActiveIcon width={12} height={12} />}
|
{currentFilters.members.find((m) => m.id === userID) && <ActiveIcon width={12} height={12} />}
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}>
|
<ActionItem onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}>
|
||||||
<ItemIcon>
|
<ItemIcon>
|
||||||
@ -228,10 +230,10 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
</Popup>
|
</Popup>
|
||||||
<Popup tab={1} title="By Labels">
|
<Popup tab={1} title="By Labels">
|
||||||
<Labels>
|
<Labels>
|
||||||
{labels.current &&
|
{data &&
|
||||||
labels.current
|
data.findProject.labels
|
||||||
// .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase())))
|
// .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase())))
|
||||||
.map(label => (
|
.map((label) => (
|
||||||
<Label key={label.id}>
|
<Label key={label.id}>
|
||||||
<CardLabel
|
<CardLabel
|
||||||
key={label.id}
|
key={label.id}
|
||||||
@ -242,9 +244,9 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, draftFilters => {
|
produce(currentFilters, (draftFilters) => {
|
||||||
if (draftFilters.labels.find(l => l.id === label.id)) {
|
if (draftFilters.labels.find((l) => l.id === label.id)) {
|
||||||
draftFilters.labels = draftFilters.labels.filter(l => l.id !== label.id);
|
draftFilters.labels = draftFilters.labels.filter((l) => l.id !== label.id);
|
||||||
} else {
|
} else {
|
||||||
draftFilters.labels.push({
|
draftFilters.labels.push({
|
||||||
id: label.id,
|
id: label.id,
|
||||||
@ -265,16 +267,16 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
|
|||||||
<Popup tab={2} title="By Member">
|
<Popup tab={2} title="By Member">
|
||||||
<ActionsList>
|
<ActionsList>
|
||||||
{members.current &&
|
{members.current &&
|
||||||
members.current.map(member => (
|
members.current.map((member) => (
|
||||||
<FilterMember
|
<FilterMember
|
||||||
key={member.id}
|
key={member.id}
|
||||||
member={member}
|
member={member}
|
||||||
showName
|
showName
|
||||||
onCardMemberClick={() => {
|
onCardMemberClick={() => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, draftFilters => {
|
produce(currentFilters, (draftFilters) => {
|
||||||
if (draftFilters.members.find(m => m.id === member.id)) {
|
if (draftFilters.members.find((m) => m.id === member.id)) {
|
||||||
draftFilters.members = draftFilters.members.filter(m => m.id !== member.id);
|
draftFilters.members = draftFilters.members.filter((m) => m.id !== member.id);
|
||||||
} else {
|
} else {
|
||||||
draftFilters.members.push({ id: member.id, username: member.username ?? '' });
|
draftFilters.members.push({ id: member.id, username: member.username ?? '' });
|
||||||
}
|
}
|
||||||
|
@ -136,16 +136,16 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: ${props => props.theme.colors.text.primary};
|
color: ${(props) => props.theme.colors.text.primary};
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${props => props.theme.colors.text.secondary};
|
color: ${(props) => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
${props =>
|
${(props) =>
|
||||||
props.disabled &&
|
props.disabled &&
|
||||||
css`
|
css`
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -280,8 +280,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
||||||
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
|
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
|
||||||
);
|
);
|
||||||
@ -296,10 +296,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
const { taskGroups } = cache.findProject;
|
const { taskGroups } = cache.findProject;
|
||||||
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
|
const idx = taskGroups.findIndex((taskGroup) => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
if (newTaskData.data) {
|
if (newTaskData.data) {
|
||||||
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
||||||
@ -316,8 +316,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
if (newTaskGroupData.data) {
|
if (newTaskGroupData.data) {
|
||||||
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
||||||
}
|
}
|
||||||
@ -336,10 +336,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
const idx = cache.findProject.taskGroups.findIndex(
|
const idx = cache.findProject.taskGroups.findIndex(
|
||||||
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
|
(t) => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
|
||||||
);
|
);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
draftCache.findProject.taskGroups[idx].tasks = [];
|
draftCache.findProject.taskGroups[idx].tasks = [];
|
||||||
@ -353,8 +353,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
if (resp.data) {
|
if (resp.data) {
|
||||||
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
||||||
}
|
}
|
||||||
@ -371,8 +371,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
if (newTask.data) {
|
if (newTask.data) {
|
||||||
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
||||||
if (previousTaskGroupID !== task.taskGroup.id) {
|
if (previousTaskGroupID !== task.taskGroup.id) {
|
||||||
@ -380,7 +380,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
||||||
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
||||||
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
||||||
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
|
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(
|
||||||
|
(t) => t.id === task.id,
|
||||||
|
);
|
||||||
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
||||||
(t: Task) => t.id !== task.id,
|
(t: Task) => t.id !== task.id,
|
||||||
);
|
);
|
||||||
@ -401,14 +403,14 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [deleteTask] = useDeleteTaskMutation();
|
const [deleteTask] = useDeleteTaskMutation();
|
||||||
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
||||||
onCompleted: newTaskLabel => {
|
onCompleted: (newTaskLabel) => {
|
||||||
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onCreateTask = (taskGroupID: string, name: string) => {
|
const onCreateTask = (taskGroupID: string, name: string) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
|
||||||
if (taskGroup) {
|
if (taskGroup) {
|
||||||
let position = 65535;
|
let position = 65535;
|
||||||
if (taskGroup.tasks.length !== 0) {
|
if (taskGroup.tasks.length !== 0) {
|
||||||
@ -472,12 +474,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
}
|
}
|
||||||
return 'All Tasks';
|
return 'All Tasks';
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
labelsRef.current = data.findProject.labels;
|
labelsRef.current = data.findProject.labels;
|
||||||
membersRef.current = data.findProject.members;
|
membersRef.current = data.findProject.members;
|
||||||
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
|
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
|
||||||
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
|
||||||
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null;
|
const currentTask = taskGroup ? taskGroup.tasks.find((t) => t.id === taskID) : null;
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
setQuickCardEditor({
|
setQuickCardEditor({
|
||||||
target: $target,
|
target: $target,
|
||||||
@ -489,9 +492,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
};
|
};
|
||||||
let currentQuickTask = null;
|
let currentQuickTask = null;
|
||||||
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
|
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
|
||||||
const targetGroup = data.findProject.taskGroups.find(t => t.id === quickCardEditor.taskGroupID);
|
const targetGroup = data.findProject.taskGroups.find((t) => t.id === quickCardEditor.taskGroupID);
|
||||||
if (targetGroup) {
|
if (targetGroup) {
|
||||||
currentQuickTask = targetGroup.tasks.find(t => t.id === quickCardEditor.taskID);
|
currentQuickTask = targetGroup.tasks.find((t) => t.id === quickCardEditor.taskID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -499,13 +502,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectBar>
|
<ProjectBar>
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={target => {
|
onClick={(target) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
target,
|
target,
|
||||||
<Popup tab={0} title={null}>
|
<Popup tab={0} title={null}>
|
||||||
<FilterStatus
|
<FilterStatus
|
||||||
filter={taskStatusFilter}
|
filter={taskStatusFilter}
|
||||||
onChangeTaskStatusFilter={filter => {
|
onChangeTaskStatusFilter={(filter) => {
|
||||||
setTaskStatusFilter(filter);
|
setTaskStatusFilter(filter);
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
@ -519,13 +522,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText>
|
<ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={target => {
|
onClick={(target) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
target,
|
target,
|
||||||
<Popup tab={0} title={null}>
|
<Popup tab={0} title={null}>
|
||||||
<SortPopup
|
<SortPopup
|
||||||
sorting={taskSorting}
|
sorting={taskSorting}
|
||||||
onChangeTaskSorting={sorting => {
|
onChangeTaskSorting={(sorting) => {
|
||||||
setTaskSorting(sorting);
|
setTaskSorting(sorting);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -538,16 +541,16 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText>
|
<ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={target => {
|
onClick={(target) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
target,
|
target,
|
||||||
<FilterMeta
|
<FilterMeta
|
||||||
filters={taskMetaFilters}
|
filters={taskMetaFilters}
|
||||||
onChangeTaskMetaFilter={filter => {
|
onChangeTaskMetaFilter={(filter) => {
|
||||||
setTaskMetaFilters(filter);
|
setTaskMetaFilters(filter);
|
||||||
}}
|
}}
|
||||||
userID={user ?? ''}
|
userID={user ?? ''}
|
||||||
labels={labelsRef}
|
projectID={projectID}
|
||||||
members={membersRef}
|
members={membersRef}
|
||||||
/>,
|
/>,
|
||||||
{ width: 200 },
|
{ width: 200 },
|
||||||
@ -559,11 +562,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
{renderMetaFilters(taskMetaFilters, (meta, id) => {
|
{renderMetaFilters(taskMetaFilters, (meta, id) => {
|
||||||
setTaskMetaFilters(
|
setTaskMetaFilters(
|
||||||
produce(taskMetaFilters, draftFilters => {
|
produce(taskMetaFilters, (draftFilters) => {
|
||||||
if (meta === TaskMeta.MEMBER) {
|
if (meta === TaskMeta.MEMBER) {
|
||||||
draftFilters.members = draftFilters.members.filter(m => m.id !== id);
|
draftFilters.members = draftFilters.members.filter((m) => m.id !== id);
|
||||||
} else if (meta === TaskMeta.LABEL) {
|
} else if (meta === TaskMeta.LABEL) {
|
||||||
draftFilters.labels = draftFilters.labels.filter(m => m.id !== id);
|
draftFilters.labels = draftFilters.labels.filter((m) => m.id !== id);
|
||||||
} else if (meta === TaskMeta.TITLE) {
|
} else if (meta === TaskMeta.TITLE) {
|
||||||
draftFilters.taskName = null;
|
draftFilters.taskName = null;
|
||||||
} else if (meta === TaskMeta.DUE_DATE) {
|
} else if (meta === TaskMeta.DUE_DATE) {
|
||||||
@ -576,15 +579,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
{user && (
|
{user && (
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={$labelsRef => {
|
onClick={($labelsRef) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$labelsRef,
|
$labelsRef,
|
||||||
<LabelManagerEditor
|
<LabelManagerEditor taskLabels={null} labelColors={data.labelColors} projectID={projectID ?? ''} />,
|
||||||
taskLabels={null}
|
|
||||||
labelColors={data.labelColors}
|
|
||||||
labels={labelsRef}
|
|
||||||
projectID={projectID ?? ''}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -604,7 +602,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
</ProjectBar>
|
</ProjectBar>
|
||||||
<SimpleLists
|
<SimpleLists
|
||||||
isPublic={user === null}
|
isPublic={user === null}
|
||||||
onTaskClick={task => {
|
onTaskClick={(task) => {
|
||||||
history.push(`${match.url}/c/${task.id}`);
|
history.push(`${match.url}/c/${task.id}`);
|
||||||
}}
|
}}
|
||||||
onCardLabelClick={onCardLabelClick ?? NOOP}
|
onCardLabelClick={onCardLabelClick ?? NOOP}
|
||||||
@ -637,7 +635,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onTaskGroupDrop={droppedTaskGroup => {
|
onTaskGroupDrop={(droppedTaskGroup) => {
|
||||||
updateTaskGroupLocation({
|
updateTaskGroupLocation({
|
||||||
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
|
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
@ -657,7 +655,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
onCreateTask={onCreateTask}
|
onCreateTask={onCreateTask}
|
||||||
onCreateTaskGroup={onCreateList}
|
onCreateTaskGroup={onCreateList}
|
||||||
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
||||||
const member = data.findProject.members.find(m => m.id === memberID);
|
const member = data.findProject.members.find((m) => m.id === memberID);
|
||||||
if (member) {
|
if (member) {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -684,8 +682,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
deleteTaskGroupTasks({ variables: { taskGroupID } });
|
deleteTaskGroupTasks({ variables: { taskGroupID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onSortTaskGroup={taskSort => {
|
onSortTaskGroup={(taskSort) => {
|
||||||
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
|
||||||
if (taskGroup) {
|
if (taskGroup) {
|
||||||
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
|
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
|
||||||
.sort((a, b) => sortTasks(a, b, taskSort))
|
.sort((a, b) => sortTasks(a, b, taskSort))
|
||||||
@ -697,8 +695,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDuplicateTaskGroup={newName => {
|
onDuplicateTaskGroup={(newName) => {
|
||||||
const idx = data.findProject.taskGroups.findIndex(t => t.id === taskGroupID);
|
const idx = data.findProject.taskGroups.findIndex((t) => t.id === taskGroupID);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
|
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
|
||||||
const prevPos = taskGroups[idx].position;
|
const prevPos = taskGroups[idx].position;
|
||||||
@ -711,7 +709,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onArchiveTaskGroup={tgID => {
|
onArchiveTaskGroup={(tgID) => {
|
||||||
deleteTaskGroup({ variables: { taskGroupID: tgID } });
|
deleteTaskGroup({ variables: { taskGroupID: tgID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
@ -745,7 +743,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
||||||
const member = data.findProject.members.find(m => m.id === memberID);
|
const member = data.findProject.members.find((m) => m.id === memberID);
|
||||||
if (member) {
|
if (member) {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -764,12 +762,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
<LabelManagerEditor
|
<LabelManagerEditor
|
||||||
onLabelToggle={labelID => {
|
onLabelToggle={(labelID) => {
|
||||||
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
|
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
|
||||||
}}
|
}}
|
||||||
taskID={task.id}
|
taskID={task.id}
|
||||||
labelColors={data.labelColors}
|
labelColors={data.labelColors}
|
||||||
labels={labelsRef}
|
|
||||||
taskLabels={taskLabelsRef}
|
taskLabels={taskLabelsRef}
|
||||||
projectID={projectID ?? ''}
|
projectID={projectID ?? ''}
|
||||||
/>,
|
/>,
|
||||||
@ -778,15 +775,15 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
onArchiveCard={(_listId: string, cardId: string) => {
|
onArchiveCard={(_listId: string, cardId: string) => {
|
||||||
return deleteTask({
|
return deleteTask({
|
||||||
variables: { taskID: cardId },
|
variables: { taskID: cardId },
|
||||||
update: client => {
|
update: (client) => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map(taskGroup => ({
|
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map((taskGroup) => ({
|
||||||
...taskGroup,
|
...taskGroup,
|
||||||
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
|
tasks: taskGroup.tasks.filter((t) => t.id !== cardId),
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
@ -800,7 +797,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
|
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
|
||||||
<DueDateManager
|
<DueDateManager
|
||||||
task={task}
|
task={task}
|
||||||
onRemoveDueDate={t => {
|
onRemoveDueDate={(t) => {
|
||||||
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
|
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
|
||||||
// hidePopup();
|
// hidePopup();
|
||||||
}}
|
}}
|
||||||
@ -813,7 +810,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
</Popup>,
|
</Popup>,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onToggleComplete={task => {
|
onToggleComplete={(task) => {
|
||||||
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
||||||
}}
|
}}
|
||||||
target={quickCardEditor.target}
|
target={quickCardEditor.target}
|
||||||
|
@ -9,13 +9,13 @@ import {
|
|||||||
useCreateProjectLabelMutation,
|
useCreateProjectLabelMutation,
|
||||||
FindProjectQuery,
|
FindProjectQuery,
|
||||||
useToggleTaskLabelMutation,
|
useToggleTaskLabelMutation,
|
||||||
|
useLabelsQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
||||||
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
||||||
|
|
||||||
type LabelManagerEditorProps = {
|
type LabelManagerEditorProps = {
|
||||||
taskID?: string;
|
taskID?: string;
|
||||||
labels: React.RefObject<Array<ProjectLabel>>;
|
|
||||||
taskLabels: null | React.RefObject<Array<TaskLabel>>;
|
taskLabels: null | React.RefObject<Array<TaskLabel>>;
|
||||||
projectID: string;
|
projectID: string;
|
||||||
labelColors: Array<LabelColor>;
|
labelColors: Array<LabelColor>;
|
||||||
@ -24,7 +24,6 @@ type LabelManagerEditorProps = {
|
|||||||
|
|
||||||
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||||
taskID,
|
taskID,
|
||||||
labels: labelsRef,
|
|
||||||
projectID,
|
projectID,
|
||||||
labelColors,
|
labelColors,
|
||||||
onLabelToggle,
|
onLabelToggle,
|
||||||
@ -34,7 +33,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
const { setTab, hidePopup } = usePopup();
|
const { setTab, hidePopup } = usePopup();
|
||||||
const [toggleTaskLabel] = useToggleTaskLabelMutation();
|
const [toggleTaskLabel] = useToggleTaskLabelMutation();
|
||||||
const [createProjectLabel] = useCreateProjectLabelMutation({
|
const [createProjectLabel] = useCreateProjectLabelMutation({
|
||||||
onCompleted: data => {
|
onCompleted: (data) => {
|
||||||
if (taskID) {
|
if (taskID) {
|
||||||
toggleTaskLabel({ variables: { taskID, projectLabelID: data.createProjectLabel.id } });
|
toggleTaskLabel({ variables: { taskID, projectLabelID: data.createProjectLabel.id } });
|
||||||
}
|
}
|
||||||
@ -43,8 +42,8 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
if (newLabelData.data) {
|
if (newLabelData.data) {
|
||||||
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
||||||
}
|
}
|
||||||
@ -61,38 +60,39 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
cache =>
|
(cache) =>
|
||||||
produce(cache, draftCache => {
|
produce(cache, (draftCache) => {
|
||||||
draftCache.findProject.labels = cache.findProject.labels.filter(
|
draftCache.findProject.labels = cache.findProject.labels.filter(
|
||||||
label => label.id !== newLabelData.data?.deleteProjectLabel.id,
|
(label) => label.id !== newLabelData.data?.deleteProjectLabel.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const labels = labelsRef.current ? labelsRef.current : [];
|
const { data } = useLabelsQuery({ variables: { projectID } });
|
||||||
|
const labels = data ? data.findProject.labels : [];
|
||||||
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
|
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
|
||||||
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
|
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
|
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
|
||||||
<LabelManager
|
<LabelManager
|
||||||
labels={labels}
|
labels={data ? data.findProject.labels : []}
|
||||||
taskLabels={currentTaskLabels}
|
taskLabels={currentTaskLabels}
|
||||||
onLabelCreate={() => {
|
onLabelCreate={() => {
|
||||||
setTab(2);
|
setTab(2);
|
||||||
}}
|
}}
|
||||||
onLabelEdit={labelId => {
|
onLabelEdit={(labelId) => {
|
||||||
setCurrentLabel(labelId);
|
setCurrentLabel(labelId);
|
||||||
setTab(1);
|
setTab(1);
|
||||||
}}
|
}}
|
||||||
onLabelToggle={labelId => {
|
onLabelToggle={(labelId) => {
|
||||||
if (onLabelToggle) {
|
if (onLabelToggle) {
|
||||||
if (currentTaskLabels.find(t => t.projectLabel.id === labelId)) {
|
if (currentTaskLabels.find((t) => t.projectLabel.id === labelId)) {
|
||||||
setCurrentTaskLabels(currentTaskLabels.filter(t => t.projectLabel.id !== labelId));
|
setCurrentTaskLabels(currentTaskLabels.filter((t) => t.projectLabel.id !== labelId));
|
||||||
} else {
|
} else if (data) {
|
||||||
const newProjectLabel = labels.find(l => l.id === labelId);
|
const newProjectLabel = data.findProject.labels.find((l) => l.id === labelId);
|
||||||
if (newProjectLabel) {
|
if (newProjectLabel) {
|
||||||
setCurrentTaskLabels([
|
setCurrentTaskLabels([
|
||||||
...currentTaskLabels,
|
...currentTaskLabels,
|
||||||
@ -112,14 +112,14 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
|
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
|
||||||
<LabelEditor
|
<LabelEditor
|
||||||
labelColors={labelColors}
|
labelColors={labelColors}
|
||||||
label={labels.find(label => label.id === currentLabel) ?? null}
|
label={labels.find((label) => label.id === currentLabel) ?? null}
|
||||||
onLabelEdit={(projectLabelID, name, color) => {
|
onLabelEdit={(projectLabelID, name, color) => {
|
||||||
if (projectLabelID) {
|
if (projectLabelID) {
|
||||||
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
||||||
}
|
}
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
onLabelDelete={labelID => {
|
onLabelDelete={(labelID) => {
|
||||||
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
|
@ -31,11 +31,11 @@ import produce from 'immer';
|
|||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import useStateWithLocalStorage from 'shared/hooks/useStateWithLocalStorage';
|
import useStateWithLocalStorage from 'shared/hooks/useStateWithLocalStorage';
|
||||||
import localStorage from 'shared/utils/localStorage';
|
import localStorage from 'shared/utils/localStorage';
|
||||||
|
import polling from 'shared/utils/polling';
|
||||||
import Board, { BoardLoading } from './Board';
|
import Board, { BoardLoading } from './Board';
|
||||||
import Details from './Details';
|
import Details from './Details';
|
||||||
import LabelManagerEditor from './LabelManagerEditor';
|
import LabelManagerEditor from './LabelManagerEditor';
|
||||||
import UserManagementPopup from './UserManagementPopup';
|
import UserManagementPopup from './UserManagementPopup';
|
||||||
import polling from 'shared/utils/polling';
|
|
||||||
|
|
||||||
type TaskRouteProps = {
|
type TaskRouteProps = {
|
||||||
taskID: string;
|
taskID: string;
|
||||||
@ -269,7 +269,6 @@ const Project = () => {
|
|||||||
}}
|
}}
|
||||||
taskID={task.id}
|
taskID={task.id}
|
||||||
labelColors={data.labelColors}
|
labelColors={data.labelColors}
|
||||||
labels={labelsRef}
|
|
||||||
taskLabels={taskLabelsRef}
|
taskLabels={taskLabelsRef}
|
||||||
projectID={projectID}
|
projectID={projectID}
|
||||||
/>,
|
/>,
|
||||||
|
@ -524,7 +524,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
members={data.findTeam.members}
|
members={data.findTeam.members}
|
||||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||||
// canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} TODO: add permission check
|
// canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} TODO: add permission check
|
||||||
canChangeRole={true}
|
canChangeRole
|
||||||
onChangeRole={(roleCode) => {
|
onChangeRole={(roleCode) => {
|
||||||
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||||
}}
|
}}
|
||||||
|
@ -10,6 +10,215 @@ import Button from 'shared/components/Button';
|
|||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
const UserSelect = styled(Select)`
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NewUserPassInput = styled(Input)`
|
||||||
|
margin: 8px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InviteMemberButton = styled(Button)`
|
||||||
|
padding: 7px 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserPassBar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding-top: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserPassConfirmButton = styled(Button)`
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserPassButton = styled(Button)`
|
||||||
|
width: 50%;
|
||||||
|
padding: 7px 12px;
|
||||||
|
& ~ & {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberItemOptions = styled.div``;
|
||||||
|
|
||||||
|
const MemberItemOption = styled(Button)`
|
||||||
|
padding: 7px 9px;
|
||||||
|
margin: 4px 0 4px 8px;
|
||||||
|
float: left;
|
||||||
|
min-width: 95px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberList = styled.div`
|
||||||
|
border-top: 1px solid ${(props) => props.theme.colors.border};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid ${(props) => props.theme.colors.border};
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 12px 0 12px 40px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListItemDetails = styled.div`
|
||||||
|
float: left;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InviteIcon = styled(UserPlus)`
|
||||||
|
padding-right: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberProfile = styled(TaskAssignee)`
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberItemName = styled.p`
|
||||||
|
color: ${(props) => props.theme.colors.text.secondary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberItemUsername = styled.p`
|
||||||
|
color: ${(props) => props.theme.colors.text.primary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
const ListTitle = styled.h3`
|
||||||
|
font-size: 18px;
|
||||||
|
color: ${(props) => props.theme.colors.text.secondary};
|
||||||
|
margin-bottom: 12px;
|
||||||
|
`;
|
||||||
|
const ListDesc = styled.span`
|
||||||
|
font-size: 16px;
|
||||||
|
color: ${(props) => props.theme.colors.text.primary};
|
||||||
|
`;
|
||||||
|
const FilterSearch = styled(Input)`
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListActions = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListWrapper = styled.div`
|
||||||
|
flex: 1 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
padding: 2.2rem;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNav = styled.div`
|
||||||
|
float: left;
|
||||||
|
width: 220px;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavContent = styled.ul`
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavItem = styled.li`
|
||||||
|
padding: 0.35rem 0.3rem;
|
||||||
|
height: 48px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding-top: 10px !important;
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
padding-left: 12px !important;
|
||||||
|
padding-right: 8px !important;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
color: ${(props) => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
|
||||||
|
&:hover {
|
||||||
|
color: ${(props) => `${props.theme.colors.primary}`};
|
||||||
|
}
|
||||||
|
&:hover svg {
|
||||||
|
fill: ${(props) => props.theme.colors.primary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const TabItemUser = styled(User)<{ active: boolean }>`
|
||||||
|
fill: ${(props) => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
||||||
|
stroke: ${(props) => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavItemSpan = styled.span`
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavLine = styled.span<{ top: number }>`
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: 48px;
|
||||||
|
transform: scaleX(1);
|
||||||
|
top: ${(props) => props.top}px;
|
||||||
|
|
||||||
|
background: linear-gradient(
|
||||||
|
30deg,
|
||||||
|
${(props) => props.theme.colors.primary},
|
||||||
|
${(props) => props.theme.colors.primary}
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 8px 0 ${(props) => props.theme.colors.primary};
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabContentWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabContent = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #10163a;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const items = [{ name: 'Members' }];
|
||||||
|
|
||||||
export const RoleCheckmark = styled(Checkmark)`
|
export const RoleCheckmark = styled(Checkmark)`
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
`;
|
`;
|
||||||
@ -54,7 +263,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
${props =>
|
${(props) =>
|
||||||
props.disabled
|
props.disabled
|
||||||
? css`
|
? css`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -75,7 +284,7 @@ export const Content = styled.div`
|
|||||||
|
|
||||||
export const CurrentPermission = styled.span`
|
export const CurrentPermission = styled.span`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
|
color: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Separator = styled.div`
|
export const Separator = styled.div`
|
||||||
@ -86,13 +295,13 @@ export const Separator = styled.div`
|
|||||||
|
|
||||||
export const WarningText = styled.span`
|
export const WarningText = styled.span`
|
||||||
display: flex;
|
display: flex;
|
||||||
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DeleteDescription = styled.div`
|
export const DeleteDescription = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${props => props.theme.colors.text.primary};
|
color: ${(props) => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RemoveMemberButton = styled(Button)`
|
export const RemoveMemberButton = styled(Button)`
|
||||||
@ -161,8 +370,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
<MiniProfileActions>
|
<MiniProfileActions>
|
||||||
<MiniProfileActionWrapper>
|
<MiniProfileActionWrapper>
|
||||||
{permissions
|
{permissions
|
||||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
.filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||||
.map(perm => (
|
.map((perm) => (
|
||||||
<MiniProfileActionItem
|
<MiniProfileActionItem
|
||||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||||
key={perm.code}
|
key={perm.code}
|
||||||
@ -213,9 +422,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
Choose a new user to take over ownership of the users teams & projects.
|
Choose a new user to take over ownership of the users teams & projects.
|
||||||
</DeleteDescription>
|
</DeleteDescription>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
onChange={v => setDeleteUser(v)}
|
onChange={(v) => setDeleteUser(v)}
|
||||||
value={deleteUser}
|
value={deleteUser}
|
||||||
options={users.map(u => ({ label: u.fullName, value: u.id }))}
|
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -240,7 +449,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||||
</DeleteDescription>
|
</DeleteDescription>
|
||||||
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
||||||
<UserSelect onChange={NOOP} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} />
|
<UserSelect onChange={NOOP} value={null} options={users.map((u) => ({ label: u.fullName, value: u.id }))} />
|
||||||
<UserPassConfirmButton
|
<UserPassConfirmButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// onDeleteUser();
|
// onDeleteUser();
|
||||||
@ -293,211 +502,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserSelect = styled(Select)`
|
|
||||||
margin: 8px 0;
|
|
||||||
padding: 8px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NewUserPassInput = styled(Input)`
|
|
||||||
margin: 8px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InviteMemberButton = styled(Button)`
|
|
||||||
padding: 7px 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserPassBar = styled.div`
|
|
||||||
display: flex;
|
|
||||||
padding-top: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserPassConfirmButton = styled(Button)`
|
|
||||||
width: 100%;
|
|
||||||
padding: 7px 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserPassButton = styled(Button)`
|
|
||||||
width: 50%;
|
|
||||||
padding: 7px 12px;
|
|
||||||
& ~ & {
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberItemOptions = styled.div``;
|
|
||||||
|
|
||||||
const MemberItemOption = styled(Button)`
|
|
||||||
padding: 7px 9px;
|
|
||||||
margin: 4px 0 4px 8px;
|
|
||||||
float: left;
|
|
||||||
min-width: 95px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberList = styled.div`
|
|
||||||
border-top: 1px solid ${props => props.theme.colors.border};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListItem = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid ${props => props.theme.colors.border};
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 12px 0 12px 40px;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListItemDetails = styled.div`
|
|
||||||
float: left;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
padding-left: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InviteIcon = styled(UserPlus)`
|
|
||||||
padding-right: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberProfile = styled(TaskAssignee)`
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 0;
|
|
||||||
margin: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberItemName = styled.p`
|
|
||||||
color: ${props => props.theme.colors.text.secondary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberItemUsername = styled.p`
|
|
||||||
color: ${props => props.theme.colors.text.primary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
const ListTitle = styled.h3`
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${props => props.theme.colors.text.secondary};
|
|
||||||
margin-bottom: 12px;
|
|
||||||
`;
|
|
||||||
const ListDesc = styled.span`
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${props => props.theme.colors.text.primary};
|
|
||||||
`;
|
|
||||||
const FilterSearch = styled(Input)`
|
|
||||||
margin: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ListActions = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListWrapper = styled.div`
|
|
||||||
flex: 1 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
padding: 2.2rem;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNav = styled.div`
|
|
||||||
float: left;
|
|
||||||
width: 220px;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavContent = styled.ul`
|
|
||||||
display: block;
|
|
||||||
width: auto;
|
|
||||||
border-bottom: 0 !important;
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavItem = styled.li`
|
|
||||||
padding: 0.35rem 0.3rem;
|
|
||||||
height: 48px;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavItemButton = styled.button<{ active: boolean }>`
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
padding-top: 10px !important;
|
|
||||||
padding-bottom: 10px !important;
|
|
||||||
padding-left: 12px !important;
|
|
||||||
padding-right: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
color: ${props => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
|
|
||||||
&:hover {
|
|
||||||
color: ${props => `${props.theme.colors.primary}`};
|
|
||||||
}
|
|
||||||
&:hover svg {
|
|
||||||
fill: ${props => props.theme.colors.primary};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const TabItemUser = styled(User)<{ active: boolean }>`
|
|
||||||
fill: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
|
||||||
stroke: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavItemSpan = styled.span`
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 9px;
|
|
||||||
font-size: 14px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavLine = styled.span<{ top: number }>`
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
width: 2px;
|
|
||||||
height: 48px;
|
|
||||||
transform: scaleX(1);
|
|
||||||
top: ${props => props.top}px;
|
|
||||||
|
|
||||||
background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
|
|
||||||
box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary};
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabContentWrapper = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabContent = styled.div`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
padding: 1.5rem;
|
|
||||||
background-color: #10163a;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const items = [{ name: 'Members' }];
|
|
||||||
|
|
||||||
type NavItemProps = {
|
type NavItemProps = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
@ -591,7 +595,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||||
{canInviteUser && (
|
{canInviteUser && (
|
||||||
<InviteMemberButton
|
<InviteMemberButton
|
||||||
onClick={$target => {
|
onClick={($target) => {
|
||||||
onAddUser($target);
|
onAddUser($target);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -602,7 +606,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
</ListActions>
|
</ListActions>
|
||||||
</MemberListHeader>
|
</MemberListHeader>
|
||||||
<MemberList>
|
<MemberList>
|
||||||
{users.map(member => {
|
{users.map((member) => {
|
||||||
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
||||||
return (
|
return (
|
||||||
<MemberListItem>
|
<MemberListItem>
|
||||||
@ -615,7 +619,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
||||||
<MemberItemOption
|
<MemberItemOption
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={$target => {
|
onClick={($target) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
@ -626,7 +630,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
onUpdateUserPassword(user, password);
|
onUpdateUserPassword(user, password);
|
||||||
}}
|
}}
|
||||||
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
||||||
onChangeRole={roleCode => {
|
onChangeRole={(roleCode) => {
|
||||||
updateUserRole({ variables: { userID: member.id, roleCode } });
|
updateUserRole({ variables: { userID: member.id, roleCode } });
|
||||||
}}
|
}}
|
||||||
onDeleteUser={onDeleteUser}
|
onDeleteUser={onDeleteUser}
|
||||||
@ -640,7 +644,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
</MemberListItem>
|
</MemberListItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{invitedUsers.map(member => {
|
{invitedUsers.map((member) => {
|
||||||
return (
|
return (
|
||||||
<MemberListItem>
|
<MemberListItem>
|
||||||
<MemberProfile
|
<MemberProfile
|
||||||
@ -664,7 +668,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<MemberItemOptions>
|
<MemberItemOptions>
|
||||||
<MemberItemOption
|
<MemberItemOption
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={$target => {
|
onClick={($target) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
|
@ -7,6 +7,8 @@ import 'react-datepicker/dist/react-datepicker.css';
|
|||||||
import { getYear, getMonth } from 'date-fns';
|
import { getYear, getMonth } from 'date-fns';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
|
import { Clock, Cross } from 'shared/icons';
|
||||||
|
import Select from 'react-select/src/Select';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
@ -23,8 +25,6 @@ import {
|
|||||||
ActionClock,
|
ActionClock,
|
||||||
ActionLabel,
|
ActionLabel,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
import { Clock, Cross } from 'shared/icons';
|
|
||||||
import Select from 'react-select/src/Select';
|
|
||||||
|
|
||||||
type DueDateManagerProps = {
|
type DueDateManagerProps = {
|
||||||
task: Task;
|
task: Task;
|
||||||
@ -190,21 +190,6 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
};
|
};
|
||||||
const [isRange, setIsRange] = useState(false);
|
const [isRange, setIsRange] = useState(false);
|
||||||
|
|
||||||
const CustomTimeInput = forwardRef(({ value, onClick, onChange, onBlur, onFocus }: any, $ref: any) => {
|
|
||||||
return (
|
|
||||||
<DueDateInput
|
|
||||||
id="endTime"
|
|
||||||
value={value}
|
|
||||||
name="endTime"
|
|
||||||
onChange={onChange}
|
|
||||||
width="100%"
|
|
||||||
variant="alternate"
|
|
||||||
label="Time"
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<DateRangeInputs>
|
<DateRangeInputs>
|
||||||
|
@ -100,7 +100,7 @@ const List = React.forwardRef(
|
|||||||
/>
|
/>
|
||||||
{!isPublic && (
|
{!isPublic && (
|
||||||
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
|
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
|
||||||
<Ellipsis size={16} color="#c2c6dc" />
|
<Ellipsis vertical={false} size={16} color="#c2c6dc" />
|
||||||
</ListExtraMenuButtonWrapper>
|
</ListExtraMenuButtonWrapper>
|
||||||
)}
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
|
@ -98,8 +98,8 @@ const ProjectName = styled.input`
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
||||||
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
|
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const ProjectNameLabel = styled.label`
|
const ProjectNameLabel = styled.label`
|
||||||
@ -210,8 +210,8 @@ const CreateButton = styled.button`
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: ${props => props.theme.colors.primary};
|
background: ${(props) => props.theme.colors.primary};
|
||||||
border-color: ${props => props.theme.colors.primary};
|
border-color: ${(props) => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
type NewProjectProps = {
|
type NewProjectProps = {
|
||||||
@ -224,7 +224,7 @@ type NewProjectProps = {
|
|||||||
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
|
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
|
||||||
const [projectName, setProjectName] = useState('');
|
const [projectName, setProjectName] = useState('');
|
||||||
const [team, setTeam] = useState<null | string>(initialTeamID);
|
const [team, setTeam] = useState<null | string>(initialTeamID);
|
||||||
const options = [{ label: 'No team', value: 'no-team' }, ...teams.map(t => ({ label: t.name, value: t.id }))];
|
const options = [{ label: 'No team', value: 'no-team' }, ...teams.map((t) => ({ label: t.name, value: t.id }))];
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay>
|
||||||
<Content>
|
<Content>
|
||||||
@ -234,7 +234,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft color="#c2c6dc" />
|
<ArrowLeft width={16} height={16} color="#c2c6dc" />
|
||||||
</HeaderLeft>
|
</HeaderLeft>
|
||||||
<HeaderRight
|
<HeaderRight
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
|
|||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setTeam(e.value);
|
setTeam(e.value);
|
||||||
}}
|
}}
|
||||||
value={options.find(d => d.value === team)}
|
value={options.find((d) => d.value === team)}
|
||||||
styles={colourStyles}
|
styles={colourStyles}
|
||||||
classNamePrefix="teamSelect"
|
classNamePrefix="teamSelect"
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -218,7 +218,7 @@ export const PopupProvider: React.FC = ({ children }) => {
|
|||||||
|
|
||||||
const setTab = (newTab: number, options?: PopupOptions) => {
|
const setTab = (newTab: number, options?: PopupOptions) => {
|
||||||
setState((prevState: PopupState) =>
|
setState((prevState: PopupState) =>
|
||||||
produce(prevState, draftState => {
|
produce(prevState, (draftState) => {
|
||||||
draftState.previousTab = currentState.currentTab;
|
draftState.previousTab = currentState.currentTab;
|
||||||
draftState.currentTab = newTab;
|
draftState.currentTab = newTab;
|
||||||
if (options) {
|
if (options) {
|
||||||
@ -296,7 +296,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
|
|||||||
<Wrapper padding borders>
|
<Wrapper padding borders>
|
||||||
{onPrevious && (
|
{onPrevious && (
|
||||||
<PreviousButton onClick={onPrevious}>
|
<PreviousButton onClick={onPrevious}>
|
||||||
<AngleLeft color="#c2c6dc" />
|
<AngleLeft size={16} color="#c2c6dc" />
|
||||||
</PreviousButton>
|
</PreviousButton>
|
||||||
)}
|
)}
|
||||||
{noHeader ? (
|
{noHeader ? (
|
||||||
@ -332,7 +332,7 @@ export const Popup: React.FC<PopupProps> = ({ borders = true, padding = true, ti
|
|||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AngleLeft color="#c2c6dc" />
|
<AngleLeft size={16} color="#c2c6dc" />
|
||||||
</PreviousButton>
|
</PreviousButton>
|
||||||
)}
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
|
@ -2,15 +2,15 @@ import React, { useRef } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
||||||
width: ${props => props.size}px;
|
width: ${(props) => props.size}px;
|
||||||
height: ${props => props.size}px;
|
height: ${(props) => props.size}px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
background: ${(props) => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
`;
|
`;
|
||||||
@ -22,6 +22,10 @@ type ProfileIconProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
|
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
|
||||||
|
let realSize = size;
|
||||||
|
if (size === null) {
|
||||||
|
realSize = 28;
|
||||||
|
}
|
||||||
const $profileRef = useRef<HTMLDivElement>(null);
|
const $profileRef = useRef<HTMLDivElement>(null);
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
@ -29,7 +33,7 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onProfileClick($profileRef, user);
|
onProfileClick($profileRef, user);
|
||||||
}}
|
}}
|
||||||
size={size}
|
size={realSize}
|
||||||
backgroundURL={user.profileIcon.url ?? null}
|
backgroundURL={user.profileIcon.url ?? null}
|
||||||
bgColor={user.profileIcon.bgColor ?? null}
|
bgColor={user.profileIcon.bgColor ?? null}
|
||||||
>
|
>
|
||||||
@ -38,8 +42,4 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ProfileIcon.defaultProps = {
|
|
||||||
size: 28,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileIcon;
|
export default ProfileIcon;
|
||||||
|
@ -311,7 +311,7 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UserInfoData = {
|
type UserInfoData = {
|
||||||
full_name: string;
|
fullName: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
initials: string;
|
initials: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -355,12 +355,12 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<UserInfoInput
|
<UserInfoInput
|
||||||
{...register('full_name', { required: 'Full name is required' })}
|
{...register('fullName', { required: 'Full name is required' })}
|
||||||
defaultValue={profile.fullName}
|
defaultValue={profile.fullName}
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Name"
|
label="Name"
|
||||||
/>
|
/>
|
||||||
{errors.full_name && <FormError>{errors.full_name.message}</FormError>}
|
{errors.fullName && <FormError>{errors.fullName.message}</FormError>}
|
||||||
<UserInfoInput
|
<UserInfoInput
|
||||||
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
||||||
{...register('initials', {
|
{...register('initials', {
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
Smile,
|
Smile,
|
||||||
} from 'shared/icons';
|
} from 'shared/icons';
|
||||||
import { toArray } from 'react-emoji-render';
|
import { toArray } from 'react-emoji-render';
|
||||||
|
import { useCurrentUser } from 'App/context';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
@ -80,11 +81,8 @@ import {
|
|||||||
ActivityItemHeaderTitleName,
|
ActivityItemHeaderTitleName,
|
||||||
ActivityItemComment,
|
ActivityItemComment,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
import { useCurrentUser } from 'App/context';
|
|
||||||
|
|
||||||
type TaskDetailsProps = {};
|
const TaskDetailsLoading: React.FC = () => {
|
||||||
|
|
||||||
const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
|
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { useCurrentUser } from 'App/context';
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
User,
|
User,
|
||||||
@ -81,9 +82,8 @@ import {
|
|||||||
} from './Styles';
|
} from './Styles';
|
||||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||||
import onDragEnd from './onDragEnd';
|
import onDragEnd from './onDragEnd';
|
||||||
import { plugin as em } from './remark';
|
import plugin from './remark';
|
||||||
import ActivityMessage from './ActivityMessage';
|
import ActivityMessage from './ActivityMessage';
|
||||||
import { useCurrentUser } from 'App/context';
|
|
||||||
|
|
||||||
const parseEmojis = (value: string) => {
|
const parseEmojis = (value: string) => {
|
||||||
const emojisArray = toArray(value);
|
const emojisArray = toArray(value);
|
||||||
@ -136,7 +136,7 @@ const StreamComment: React.FC<StreamCommentProps> = ({
|
|||||||
onCreateComment={onUpdateComment}
|
onCreateComment={onUpdateComment}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ReactMarkdown skipHtml plugins={[em]}>
|
<ReactMarkdown skipHtml plugins={[plugin]}>
|
||||||
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
|
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
)}
|
)}
|
||||||
@ -514,6 +514,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<Editor
|
<Editor
|
||||||
defaultValue={task.description ?? ''}
|
defaultValue={task.description ?? ''}
|
||||||
readOnly={user === null || !editTaskDescription}
|
readOnly={user === null || !editTaskDescription}
|
||||||
|
theme={dark}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSaveTimeout(() => {
|
setSaveTimeout(() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import visit from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
import emoji from 'node-emoji';
|
import emoji from 'node-emoji';
|
||||||
import { emoticon } from 'emoticon';
|
import { emoticon } from 'emoticon';
|
||||||
import { Emoji } from 'emoji-mart';
|
import { Emoji } from 'emoji-mart';
|
||||||
@ -15,17 +15,17 @@ const DEFAULT_SETTINGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function plugin(options) {
|
function plugin(options) {
|
||||||
const settings = Object.assign({}, DEFAULT_SETTINGS, options);
|
const settings = { ...DEFAULT_SETTINGS, ...options };
|
||||||
const pad = !!settings.padSpaceAfter;
|
const pad = !!settings.padSpaceAfter;
|
||||||
const emoticonEnable = !!settings.emoticon;
|
const emoticonEnable = !!settings.emoticon;
|
||||||
|
|
||||||
function getEmojiByShortCode(match) {
|
function getEmojiByShortCode(match) {
|
||||||
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
|
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
|
||||||
const iconFull = emoticon.find(e => e.emoticons.includes(match)); // full match
|
const iconFull = emoticon.find((e) => e.emoticons.includes(match)); // full match
|
||||||
const iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern
|
const iconPart = emoticon.find((e) => e.emoticons.includes(match.slice(0, -1))); // second search pattern
|
||||||
const trimmedChar = iconPart ? match.slice(-1) : '';
|
const trimmedChar = iconPart ? match.slice(-1) : '';
|
||||||
const addPad = pad ? ' ' : '';
|
const addPad = pad ? ' ' : '';
|
||||||
let icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
|
const icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
|
||||||
return icon || match;
|
return icon || match;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ function plugin(options) {
|
|||||||
console.log(match);
|
console.log(match);
|
||||||
const got = emoji.get(match);
|
const got = emoji.get(match);
|
||||||
if (pad && got !== match) {
|
if (pad && got !== match) {
|
||||||
return got + ' ';
|
return `${got} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(got);
|
console.log(got);
|
||||||
@ -41,7 +41,7 @@ function plugin(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformer(tree) {
|
function transformer(tree) {
|
||||||
visit(tree, 'paragraph', function(node) {
|
visit(tree, 'paragraph', function (node) {
|
||||||
console.log(tree);
|
console.log(tree);
|
||||||
// node.value = node.value.replace(RE_EMOJI, getEmoji);
|
// node.value = node.value.replace(RE_EMOJI, getEmoji);
|
||||||
// jnode.type = 'html';
|
// jnode.type = 'html';
|
||||||
@ -65,4 +65,4 @@ function plugin(options) {
|
|||||||
return transformer;
|
return transformer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { plugin };
|
export default plugin;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
|
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { RoleCode } from 'shared/generated/graphql';
|
import { RoleCode } from 'shared/generated/graphql';
|
||||||
import * as S from './Styles';
|
import * as S from './Styles';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export type MenuItem = {
|
export type MenuItem = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -144,7 +144,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
|||||||
</ProjectSettingsButton>
|
</ProjectSettingsButton>
|
||||||
{onFavorite && (
|
{onFavorite && (
|
||||||
<ProjectSettingsButton onClick={() => onFavorite()}>
|
<ProjectSettingsButton onClick={() => onFavorite()}>
|
||||||
<Star width={16} height={16} color="#c2c6dc" />
|
<Star filled width={16} height={16} color="#c2c6dc" />
|
||||||
</ProjectSettingsButton>
|
</ProjectSettingsButton>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -228,7 +228,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
<NavbarWrapper>
|
<NavbarWrapper>
|
||||||
<NavbarHeader>
|
<NavbarHeader>
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectSwitch ref={$finder} onClick={e => onOpenProjectFinder($finder)}>
|
<ProjectSwitch ref={$finder} onClick={(e) => onOpenProjectFinder($finder)}>
|
||||||
<ProjectSwitchInner>
|
<ProjectSwitchInner>
|
||||||
<TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
|
<TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
|
||||||
</ProjectSwitchInner>
|
</ProjectSwitchInner>
|
||||||
@ -304,7 +304,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
))}
|
))}
|
||||||
{canInviteUser && (
|
{canInviteUser && (
|
||||||
<InviteButton
|
<InviteButton
|
||||||
onClick={$target => {
|
onClick={($target) => {
|
||||||
if (onInviteUser) {
|
if (onInviteUser) {
|
||||||
onInviteUser($target);
|
onInviteUser($target);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
26
frontend/src/shared/graphql/labels.ts
Normal file
26
frontend/src/shared/graphql/labels.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
import TASK_FRAGMENT from './fragments/task';
|
||||||
|
|
||||||
|
const FIND_PROJECT_QUERY = gql`
|
||||||
|
query labels($projectID: UUID!) {
|
||||||
|
findProject(input: { projectID: $projectID }) {
|
||||||
|
labels {
|
||||||
|
id
|
||||||
|
createdDate
|
||||||
|
name
|
||||||
|
labelColor {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
colorHex
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labelColors {
|
||||||
|
id
|
||||||
|
position
|
||||||
|
colorHex
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -16,9 +16,4 @@ const AngleLeft = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AngleLeft.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AngleLeft;
|
export default AngleLeft;
|
||||||
|
@ -17,10 +17,4 @@ const ArrowLeft = ({ width, height, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ArrowLeft.defaultProps = {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArrowLeft;
|
export default ArrowLeft;
|
||||||
|
@ -13,9 +13,4 @@ const Bell = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Bell.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Bell;
|
export default Bell;
|
||||||
|
@ -14,9 +14,4 @@ const Bin = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Bin.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Bin;
|
export default Bin;
|
||||||
|
@ -16,9 +16,4 @@ const Cog = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Cog.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Cog;
|
export default Cog;
|
||||||
|
@ -21,10 +21,4 @@ const Ellipsis = ({ size, color, vertical }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Ellipsis.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
vertical: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Ellipsis;
|
export default Ellipsis;
|
||||||
|
@ -13,9 +13,4 @@ const Exit = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Exit.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Exit;
|
export default Exit;
|
||||||
|
@ -13,9 +13,4 @@ const Question = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Question.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Question;
|
export default Question;
|
||||||
|
@ -13,9 +13,4 @@ const Stack = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Stack.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Stack;
|
export default Stack;
|
||||||
|
@ -25,11 +25,4 @@ const Star = ({ width, height, color, filled }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Star.defaultProps = {
|
|
||||||
width: 24,
|
|
||||||
height: 16,
|
|
||||||
color: '#000',
|
|
||||||
filled: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Star;
|
export default Star;
|
||||||
|
@ -14,9 +14,4 @@ const Users = ({ size, color }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Users.defaultProps = {
|
|
||||||
size: 16,
|
|
||||||
color: '#000',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Users;
|
export default Users;
|
||||||
|
@ -7,7 +7,7 @@ export function updateApolloCache<T>(
|
|||||||
client: DataProxy,
|
client: DataProxy,
|
||||||
document: DocumentNode,
|
document: DocumentNode,
|
||||||
update: UpdateCacheFn<T>,
|
update: UpdateCacheFn<T>,
|
||||||
variables?: object,
|
variables?: any,
|
||||||
) {
|
) {
|
||||||
let queryArgs: DataProxy.Query<any, any>;
|
let queryArgs: DataProxy.Query<any, any>;
|
||||||
if (variables) {
|
if (variables) {
|
||||||
|
@ -45,6 +45,56 @@ export const base = {
|
|||||||
codeInserted: '#202746',
|
codeInserted: '#202746',
|
||||||
codeImportant: '#c94922',
|
codeImportant: '#c94922',
|
||||||
|
|
||||||
|
blockToolbarBackground: colors.bgPrimary,
|
||||||
|
blockToolbarTrigger: colors.primary,
|
||||||
|
blockToolbarTriggerIcon: colors.white,
|
||||||
|
blockToolbarItem: colors.white,
|
||||||
|
blockToolbarText: colors.white,
|
||||||
|
blockToolbarHoverBackground: colors.primary,
|
||||||
|
blockToolbarDivider: colors.almostWhite,
|
||||||
|
|
||||||
|
blockToolbarIcon: undefined,
|
||||||
|
blockToolbarIconSelected: colors.white,
|
||||||
|
blockToolbarTextSelected: colors.white,
|
||||||
|
|
||||||
|
noticeInfoBackground: '#F5BE31',
|
||||||
|
noticeInfoText: colors.almostBlack,
|
||||||
|
noticeTipBackground: '#9E5CF7',
|
||||||
|
noticeTipText: colors.white,
|
||||||
|
noticeWarningBackground: '#FF5C80',
|
||||||
|
noticeWarningText: colors.white,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BASE_TWO = {
|
||||||
|
...colors,
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen, Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif",
|
||||||
|
fontFamilyMono: "'SFMono-Regular',Consolas,'Liberation Mono', Menlo, Courier,monospace",
|
||||||
|
fontWeight: 400,
|
||||||
|
zIndex: 1000000,
|
||||||
|
link: colors.primary,
|
||||||
|
placeholder: '#B1BECC',
|
||||||
|
textSecondary: '#fff',
|
||||||
|
textLight: colors.white,
|
||||||
|
textHighlight: '#b3e7ff',
|
||||||
|
textHighlightForeground: colors.white,
|
||||||
|
selected: colors.primary,
|
||||||
|
codeComment: '#6a737d',
|
||||||
|
codePunctuation: '#5e6687',
|
||||||
|
codeNumber: '#d73a49',
|
||||||
|
codeProperty: '#c08b30',
|
||||||
|
codeTag: '#3d8fd1',
|
||||||
|
codeString: '#032f62',
|
||||||
|
codeSelector: '#6679cc',
|
||||||
|
codeAttr: '#c76b29',
|
||||||
|
codeEntity: '#22a2c9',
|
||||||
|
codeKeyword: '#d73a49',
|
||||||
|
codeFunction: '#6f42c1',
|
||||||
|
codeStatement: '#22a2c9',
|
||||||
|
codePlaceholder: '#3d8fd1',
|
||||||
|
codeInserted: '#202746',
|
||||||
|
codeImportant: '#c94922',
|
||||||
|
|
||||||
blockToolbarBackground: colors.bgPrimary,
|
blockToolbarBackground: colors.bgPrimary,
|
||||||
blockToolbarTrigger: colors.white,
|
blockToolbarTrigger: colors.white,
|
||||||
blockToolbarTriggerIcon: colors.white,
|
blockToolbarTriggerIcon: colors.white,
|
||||||
@ -53,6 +103,10 @@ export const base = {
|
|||||||
blockToolbarHoverBackground: colors.primary,
|
blockToolbarHoverBackground: colors.primary,
|
||||||
blockToolbarDivider: colors.almostWhite,
|
blockToolbarDivider: colors.almostWhite,
|
||||||
|
|
||||||
|
blockToolbarIcon: undefined,
|
||||||
|
blockToolbarIconSelected: colors.black,
|
||||||
|
blockToolbarTextSelected: colors.black,
|
||||||
|
|
||||||
noticeInfoBackground: '#F5BE31',
|
noticeInfoBackground: '#F5BE31',
|
||||||
noticeInfoText: colors.almostBlack,
|
noticeInfoText: colors.almostBlack,
|
||||||
noticeTipBackground: '#9E5CF7',
|
noticeTipBackground: '#9E5CF7',
|
||||||
|
4072
frontend/yarn.lock
4072
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -334,7 +334,7 @@ func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
Active: false,
|
Active: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("issue registering user account")
|
log.WithError(err).Error("issue registering user account")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
61
magefile.go
61
magefile.go
@ -3,10 +3,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -19,6 +21,8 @@ const (
|
|||||||
packageName = "github.com/jordanknott/taskcafe"
|
packageName = "github.com/jordanknott/taskcafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var semverRegex = regexp.MustCompile(`^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
|
||||||
|
|
||||||
var ldflags = "-X $PACKAGE/internal/utils.commitHash=$COMMIT_HASH -X $PACKAGE/internal/utils.buildDate=$BUILD_DATE -X $PACKAGE/internal/utils.version=$VERSION"
|
var ldflags = "-X $PACKAGE/internal/utils.commitHash=$COMMIT_HASH -X $PACKAGE/internal/utils.buildDate=$BUILD_DATE -X $PACKAGE/internal/utils.version=$VERSION"
|
||||||
|
|
||||||
func runWith(env map[string]string, cmd string, inArgs ...interface{}) error {
|
func runWith(env map[string]string, cmd string, inArgs ...interface{}) error {
|
||||||
@ -98,8 +102,10 @@ func (Backend) GenFrontend() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func flagEnv() map[string]string {
|
func flagEnv() map[string]string {
|
||||||
hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD")
|
hash, err := sh.Output("git", "rev-parse", "--short", "HEAD")
|
||||||
|
if err != nil {
|
||||||
fmt.Println("[ignore] fatal: no tag matches")
|
fmt.Println("[ignore] fatal: no tag matches")
|
||||||
|
}
|
||||||
tag, err := sh.Output("git", "describe", "--exact-match", "--tags")
|
tag, err := sh.Output("git", "describe", "--exact-match", "--tags")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tag = "nightly"
|
tag = "nightly"
|
||||||
@ -145,6 +151,7 @@ func (Backend) Schema() error {
|
|||||||
return sh.Run("gqlgen")
|
return sh.Run("gqlgen")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test run golang unit tests
|
||||||
func (Backend) Test() error {
|
func (Backend) Test() error {
|
||||||
fmt.Println("running taskcafe backend unit tests")
|
fmt.Println("running taskcafe backend unit tests")
|
||||||
return sh.RunV("go", "test", "./...")
|
return sh.RunV("go", "test", "./...")
|
||||||
@ -160,6 +167,58 @@ func Build() {
|
|||||||
mg.SerialDeps(Frontend.Build, Backend.GenMigrations, Backend.GenFrontend, Backend.Build)
|
mg.SerialDeps(Frontend.Build, Backend.GenMigrations, Backend.GenFrontend, Backend.Build)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release tags, builds, and upload a new release docker image
|
||||||
|
func Release() error {
|
||||||
|
// mg.SerialDeps(Frontend.Eslint, Frontend.Tsc, Backend.Test)
|
||||||
|
version, ok := os.LookupEnv("TASKCAFE_RELEASE_VERSION")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("TASKCAFE_RELEASE_VERSION must be set")
|
||||||
|
}
|
||||||
|
if !semverRegex.MatchString(version) {
|
||||||
|
return errors.New("TASKCAFE_RELEASE_VERSION must be a valid SemVer")
|
||||||
|
}
|
||||||
|
fmt.Println("Preparing " + version + " release...")
|
||||||
|
|
||||||
|
err := sh.RunV("git", "tag", version, "-m", "v"+version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = sh.RunV("git", "push", "origin", version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = sh.RunV("docker", "build", ".", "-t", "taskcafe/taskcafe:latest", "-t", "taskcafe/taskcafe:"+version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = sh.RunV("docker", "push", "taskcafe/latest:latest")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = sh.RunV("docker", "push", "taskcafe/latest:"+version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Released version " + version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest is namespace for commands interacting with docker test setups
|
||||||
|
type Latest mg.Namespace
|
||||||
|
|
||||||
|
// Up starts the docker-compose file using the `latest` taskcafe image
|
||||||
|
func (Latest) Up() error {
|
||||||
|
return sh.RunV("docker-compose", "-p", "taskcafe-latest", "-f", "testing/docker-compose.latest.yml", "up")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev is namespace for commands interacting with docker test setups
|
||||||
|
type Dev mg.Namespace
|
||||||
|
|
||||||
|
// Up starts the docker-compose file using the current files
|
||||||
|
func (Dev) Up() error {
|
||||||
|
return sh.RunV("docker-compose", "-p", "taskcafe-dev", "-f", "testing/docker-compose.dev.yml", "up")
|
||||||
|
}
|
||||||
|
|
||||||
// Docker is namespace for commands interacting with docker
|
// Docker is namespace for commands interacting with docker
|
||||||
type Docker mg.Namespace
|
type Docker mg.Namespace
|
||||||
|
|
||||||
|
26
testing/docker-compose.dev.yml
Normal file
26
testing/docker-compose.dev.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: ../
|
||||||
|
ports:
|
||||||
|
- "6677:3333"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- taskcafe-dev-test
|
||||||
|
environment:
|
||||||
|
TASKCAFE_DATABASE_HOST: postgres
|
||||||
|
TASKCAFE_MIGRATE: "true"
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.3-alpine
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- taskcafe-dev-test
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: taskcafe
|
||||||
|
POSTGRES_PASSWORD: taskcafe_test
|
||||||
|
POSTGRES_DB: taskcafe
|
||||||
|
|
||||||
|
networks:
|
||||||
|
taskcafe-dev-test:
|
||||||
|
driver: bridge
|
33
testing/docker-compose.latest.yml
Normal file
33
testing/docker-compose.latest.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: taskcafe/taskcafe:latest
|
||||||
|
# build: .
|
||||||
|
ports:
|
||||||
|
- "6688:3333"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- taskcafe-latest-test
|
||||||
|
environment:
|
||||||
|
TASKCAFE_DATABASE_HOST: postgres
|
||||||
|
TASKCAFE_MIGRATE: "true"
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.3-alpine
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- taskcafe-latest-test
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: taskcafe
|
||||||
|
POSTGRES_PASSWORD: taskcafe_test
|
||||||
|
POSTGRES_DB: taskcafe
|
||||||
|
volumes:
|
||||||
|
- taskcafe-latest-postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
taskcafe-latest-postgres:
|
||||||
|
external: false
|
||||||
|
|
||||||
|
networks:
|
||||||
|
taskcafe-latest-test:
|
||||||
|
driver: bridge
|
Reference in New Issue
Block a user