7 Commits
0.3.4 ... 0.3.6

Author SHA1 Message Date
8d724fa3cf refactor: add release target 2021-09-13 13:07:49 -05:00
76e398488f fix: rewrite the label manager to no longer use useRef
useRef was causing a `readonly` error when trying to overwrite
`ref.current`. Rewrote components to use an Apollo query instead.

fixes #121
2021-09-13 12:44:02 -05:00
d1b867db35 deps: upgrade @types/react & @types/react-dom 2021-09-13 12:43:39 -05:00
aeb97a30d8 refactor: add docker testing targets to magefile 2021-09-13 11:23:09 -05:00
56e925a48d fix: add error to log when user creation fails 2021-09-13 11:22:48 -05:00
65cd431c1a fix: TaskDetails editor theme updated to work with latest version 2021-09-07 11:32:29 -05:00
a188c4b0ca fix: clean up component to fix lint warnings preventing frontend build 2021-09-04 14:08:44 -05:00
44 changed files with 3343 additions and 3231 deletions

View File

@ -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/),
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
- Project visibility can now be set to public - meaning anyone can view the project board

View File

@ -12,7 +12,7 @@ services:
volumes:
- taskcafe-postgres:/var/lib/postgresql/data
ports:
- 8855:5432
- 8865:5432
mailhog:
image: mailhog/mailhog:latest
restart: always

View File

@ -25,6 +25,7 @@
],
"rules": {
"prettier/prettier": "warn",
"no-shadow": "off",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
@ -34,6 +35,7 @@
"no-case-declarations": "off",
"no-plusplus": "off",
"react/prop-types": 0,
"react/no-unused-prop-types": "off",
"no-continue": "off",
"react/jsx-props-no-spreading": "off",
"no-param-reassign": "off",

View File

@ -13,10 +13,10 @@
"@types/jest": "^26.0.23",
"@types/lodash": "^4.14.168",
"@types/node": "^15.0.1",
"@types/react": "^17.0.4",
"@types/react": "^17.0.20",
"@types/react-beautiful-dnd": "^13.0.0",
"@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-dom": "^5.1.7",
"@types/react-select": "^4.0.15",
@ -62,7 +62,8 @@
"react-toastify": "^7.0.4",
"rich-markdown-editor": "^11.17.4-0",
"styled-components": "^5.2.3",
"typescript": "~4.2.4"
"typescript": "~4.2.4",
"unist-util-visit": "^4.0.0"
},
"proxy": "http://localhost:3333",
"scripts": {

View File

@ -223,7 +223,7 @@ TODO: add permision check
users={data.users}
invitedUsers={data.invitedUsers}
// canInviteUser={user.roles.org === 'admin'} TODO: add permision check
canInviteUser={true}
canInviteUser
onInviteUser={NOOP}
onUpdateUserPassword={() => {
hidePopup();

View File

@ -26,10 +26,10 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import DueDateManager from 'shared/components/DueDateManager';
import dayjs from 'dayjs';
import useStickyState from 'shared/hooks/useStickyState';
import { StaticContext } from 'react-router';
import MyTasksSortPopup from './MyTasksSort';
import MyTasksStatusPopup from './MyTasksStatus';
import TaskEntry from './TaskEntry';
import { StaticContext } from 'react-router';
type TaskRouteProps = {
taskID: string;

View File

@ -44,7 +44,7 @@ const Projects = () => {
name="file"
style={{ display: 'none' }}
ref={$fileUpload}
onChange={e => {
onChange={(e) => {
if (e.target.files) {
const fileData = new FormData();
fileData.append('file', e.target.files[0]);
@ -52,7 +52,7 @@ const Projects = () => {
.post('/users/me/avatar', fileData, {
withCredentials: true,
})
.then(res => {
.then((res) => {
if ($fileUpload && $fileUpload.current) {
$fileUpload.current.value = '';
refetch();
@ -77,7 +77,7 @@ const Projects = () => {
}}
onChangeUserInfo={(d, done) => {
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!');
done();

View File

@ -7,12 +7,13 @@ import { Popup, usePopup } from 'shared/components/PopupMenu';
import produce from 'immer';
import { mixin } from 'shared/utils/styles';
import Member from 'shared/components/Member';
import { useLabelsQuery } from 'shared/generated/graphql';
const FilterMember = styled(Member)`
margin: 2px 0;
&:hover {
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 }>`
${props =>
${(props) =>
props.active &&
css`
margin-left: 4px;
@ -43,7 +44,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
padding: 6px 12px;
position: relative;
transition: padding 85ms, margin 85ms, box-shadow 85ms;
background-color: ${props => props.color};
background-color: ${(props) => props.color};
color: #fff;
display: block;
max-width: 100%;
@ -71,7 +72,7 @@ export const ActionItem = styled.li`
align-items: center;
font-size: 14px;
&: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`
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;
padding-left: 4px;
padding-right: 4px;
@ -110,15 +111,16 @@ const ActionItemLine = styled.div`
type FilterMetaProps = {
filters: TaskMetaFilters;
userID: string;
labels: React.RefObject<Array<ProjectLabel>>;
projectID: string;
members: React.RefObject<Array<TaskUser>>;
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 [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : '');
const [currentLabel, setCurrentLabel] = useState('');
const { data } = useLabelsQuery({ variables: { projectID } });
const handleSetFilters = (f: TaskMetaFilters) => {
setFilters(f);
@ -127,7 +129,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
const handleNameChange = (nFilter: string) => {
handleSetFilters(
produce(currentFilters, draftFilters => {
produce(currentFilters, (draftFilters) => {
draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null;
}),
);
@ -138,7 +140,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
const handleSetDueDate = (filterType: DueDateFilterType, label: string) => {
handleSetFilters(
produce(currentFilters, draftFilters => {
produce(currentFilters, (draftFilters) => {
if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) {
draftFilters.dueDate = null;
} else {
@ -157,7 +159,7 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<ActionsList>
<TaskNameInput
width="100%"
onChange={e => handleNameChange(e.currentTarget.value)}
onChange={(e) => handleNameChange(e.currentTarget.value)}
value={nameFilter}
autoFocus
variant="alternate"
@ -167,14 +169,14 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<ActionItem
onClick={() => {
handleSetFilters(
produce(currentFilters, draftFilters => {
produce(currentFilters, (draftFilters) => {
if (members.current) {
const member = members.current.find(m => m.id === userID);
const draftMember = draftFilters.members.find(m => m.id === userID);
const member = members.current.find((m) => m.id === userID);
const draftMember = draftFilters.members.find((m) => m.id === userID);
if (member && !draftMember) {
draftFilters.members.push({ id: userID, username: member.username ? member.username : '' });
} 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} />
</ItemIcon>
<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 onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}>
<ItemIcon>
@ -228,10 +230,10 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
</Popup>
<Popup tab={1} title="By Labels">
<Labels>
{labels.current &&
labels.current
{data &&
data.findProject.labels
// .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase())))
.map(label => (
.map((label) => (
<Label key={label.id}>
<CardLabel
key={label.id}
@ -242,9 +244,9 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
}}
onClick={() => {
handleSetFilters(
produce(currentFilters, draftFilters => {
if (draftFilters.labels.find(l => l.id === label.id)) {
draftFilters.labels = draftFilters.labels.filter(l => l.id !== label.id);
produce(currentFilters, (draftFilters) => {
if (draftFilters.labels.find((l) => l.id === label.id)) {
draftFilters.labels = draftFilters.labels.filter((l) => l.id !== label.id);
} else {
draftFilters.labels.push({
id: label.id,
@ -265,16 +267,16 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
<Popup tab={2} title="By Member">
<ActionsList>
{members.current &&
members.current.map(member => (
members.current.map((member) => (
<FilterMember
key={member.id}
member={member}
showName
onCardMemberClick={() => {
handleSetFilters(
produce(currentFilters, draftFilters => {
if (draftFilters.members.find(m => m.id === member.id)) {
draftFilters.members = draftFilters.members.filter(m => m.id !== member.id);
produce(currentFilters, (draftFilters) => {
if (draftFilters.members.find((m) => m.id === member.id)) {
draftFilters.members = draftFilters.members.filter((m) => m.id !== member.id);
} else {
draftFilters.members.push({ id: member.id, username: member.username ?? '' });
}

View File

@ -136,16 +136,16 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
display: flex;
align-items: center;
font-size: 15px;
color: ${props => props.theme.colors.text.primary};
color: ${(props) => props.theme.colors.text.primary};
&:not(:last-of-type) {
margin-right: 16px;
}
&:hover {
color: ${props => props.theme.colors.text.secondary};
color: ${(props) => props.theme.colors.text.secondary};
}
${props =>
${(props) =>
props.disabled &&
css`
opacity: 0.5;
@ -280,8 +280,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
);
@ -296,10 +296,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
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 (newTaskData.data) {
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
@ -316,8 +316,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
if (newTaskGroupData.data) {
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
}
@ -336,10 +336,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
const idx = cache.findProject.taskGroups.findIndex(
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
(t) => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
);
if (idx !== -1) {
draftCache.findProject.taskGroups[idx].tasks = [];
@ -353,8 +353,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
if (resp.data) {
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
}
@ -371,8 +371,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
if (newTask.data) {
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
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 newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
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(
(t: Task) => t.id !== task.id,
);
@ -401,14 +403,14 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
const { user } = useCurrentUser();
const [deleteTask] = useDeleteTaskMutation();
const [toggleTaskLabel] = useToggleTaskLabelMutation({
onCompleted: newTaskLabel => {
onCompleted: (newTaskLabel) => {
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
},
});
const onCreateTask = (taskGroupID: string, name: string) => {
if (data) {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
if (taskGroup) {
let position = 65535;
if (taskGroup.tasks.length !== 0) {
@ -472,12 +474,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
}
return 'All Tasks';
};
if (data) {
labelsRef.current = data.findProject.labels;
membersRef.current = data.findProject.members;
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null;
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
const currentTask = taskGroup ? taskGroup.tasks.find((t) => t.id === taskID) : null;
if (currentTask) {
setQuickCardEditor({
target: $target,
@ -489,9 +492,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
};
let currentQuickTask = null;
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) {
currentQuickTask = targetGroup.tasks.find(t => t.id === quickCardEditor.taskID);
currentQuickTask = targetGroup.tasks.find((t) => t.id === quickCardEditor.taskID);
}
}
return (
@ -499,13 +502,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectBar>
<ProjectActions>
<ProjectAction
onClick={target => {
onClick={(target) => {
showPopup(
target,
<Popup tab={0} title={null}>
<FilterStatus
filter={taskStatusFilter}
onChangeTaskStatusFilter={filter => {
onChangeTaskStatusFilter={(filter) => {
setTaskStatusFilter(filter);
hidePopup();
}}
@ -519,13 +522,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText>
</ProjectAction>
<ProjectAction
onClick={target => {
onClick={(target) => {
showPopup(
target,
<Popup tab={0} title={null}>
<SortPopup
sorting={taskSorting}
onChangeTaskSorting={sorting => {
onChangeTaskSorting={(sorting) => {
setTaskSorting(sorting);
}}
/>
@ -538,16 +541,16 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText>
</ProjectAction>
<ProjectAction
onClick={target => {
onClick={(target) => {
showPopup(
target,
<FilterMeta
filters={taskMetaFilters}
onChangeTaskMetaFilter={filter => {
onChangeTaskMetaFilter={(filter) => {
setTaskMetaFilters(filter);
}}
userID={user ?? ''}
labels={labelsRef}
projectID={projectID}
members={membersRef}
/>,
{ width: 200 },
@ -559,11 +562,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
</ProjectAction>
{renderMetaFilters(taskMetaFilters, (meta, id) => {
setTaskMetaFilters(
produce(taskMetaFilters, draftFilters => {
produce(taskMetaFilters, (draftFilters) => {
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) {
draftFilters.labels = draftFilters.labels.filter(m => m.id !== id);
draftFilters.labels = draftFilters.labels.filter((m) => m.id !== id);
} else if (meta === TaskMeta.TITLE) {
draftFilters.taskName = null;
} else if (meta === TaskMeta.DUE_DATE) {
@ -576,15 +579,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
{user && (
<ProjectActions>
<ProjectAction
onClick={$labelsRef => {
onClick={($labelsRef) => {
showPopup(
$labelsRef,
<LabelManagerEditor
taskLabels={null}
labelColors={data.labelColors}
labels={labelsRef}
projectID={projectID ?? ''}
/>,
<LabelManagerEditor taskLabels={null} labelColors={data.labelColors} projectID={projectID ?? ''} />,
);
}}
>
@ -604,7 +602,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
</ProjectBar>
<SimpleLists
isPublic={user === null}
onTaskClick={task => {
onTaskClick={(task) => {
history.push(`${match.url}/c/${task.id}`);
}}
onCardLabelClick={onCardLabelClick ?? NOOP}
@ -637,7 +635,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
},
});
}}
onTaskGroupDrop={droppedTaskGroup => {
onTaskGroupDrop={(droppedTaskGroup) => {
updateTaskGroupLocation({
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
optimisticResponse: {
@ -657,7 +655,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onCreateTask={onCreateTask}
onCreateTaskGroup={onCreateList}
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) {
showPopup(
$targetRef,
@ -684,8 +682,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
deleteTaskGroupTasks({ variables: { taskGroupID } });
hidePopup();
}}
onSortTaskGroup={taskSort => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
onSortTaskGroup={(taskSort) => {
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
if (taskGroup) {
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
.sort((a, b) => sortTasks(a, b, taskSort))
@ -697,8 +695,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
hidePopup();
}
}}
onDuplicateTaskGroup={newName => {
const idx = data.findProject.taskGroups.findIndex(t => t.id === taskGroupID);
onDuplicateTaskGroup={(newName) => {
const idx = data.findProject.taskGroups.findIndex((t) => t.id === taskGroupID);
if (idx !== -1) {
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
const prevPos = taskGroups[idx].position;
@ -711,7 +709,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
hidePopup();
}
}}
onArchiveTaskGroup={tgID => {
onArchiveTaskGroup={(tgID) => {
deleteTaskGroup({ variables: { taskGroupID: tgID } });
hidePopup();
}}
@ -745,7 +743,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
);
}}
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) {
showPopup(
$targetRef,
@ -764,12 +762,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
showPopup(
$targetRef,
<LabelManagerEditor
onLabelToggle={labelID => {
onLabelToggle={(labelID) => {
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
}}
taskID={task.id}
labelColors={data.labelColors}
labels={labelsRef}
taskLabels={taskLabelsRef}
projectID={projectID ?? ''}
/>,
@ -778,15 +775,15 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onArchiveCard={(_listId: string, cardId: string) => {
return deleteTask({
variables: { taskID: cardId },
update: client => {
update: (client) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map(taskGroup => ({
(cache) =>
produce(cache, (draftCache) => {
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map((taskGroup) => ({
...taskGroup,
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
tasks: taskGroup.tasks.filter((t) => t.id !== cardId),
}));
}),
{ projectID },
@ -800,7 +797,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
<DueDateManager
task={task}
onRemoveDueDate={t => {
onRemoveDueDate={(t) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
// hidePopup();
}}
@ -813,7 +810,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
</Popup>,
);
}}
onToggleComplete={task => {
onToggleComplete={(task) => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}}
target={quickCardEditor.target}

View File

@ -9,13 +9,13 @@ import {
useCreateProjectLabelMutation,
FindProjectQuery,
useToggleTaskLabelMutation,
useLabelsQuery,
} from 'shared/generated/graphql';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
type LabelManagerEditorProps = {
taskID?: string;
labels: React.RefObject<Array<ProjectLabel>>;
taskLabels: null | React.RefObject<Array<TaskLabel>>;
projectID: string;
labelColors: Array<LabelColor>;
@ -24,7 +24,6 @@ type LabelManagerEditorProps = {
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
taskID,
labels: labelsRef,
projectID,
labelColors,
onLabelToggle,
@ -34,7 +33,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
const { setTab, hidePopup } = usePopup();
const [toggleTaskLabel] = useToggleTaskLabelMutation();
const [createProjectLabel] = useCreateProjectLabelMutation({
onCompleted: data => {
onCompleted: (data) => {
if (taskID) {
toggleTaskLabel({ variables: { taskID, projectLabelID: data.createProjectLabel.id } });
}
@ -43,8 +42,8 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
if (newLabelData.data) {
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
}
@ -61,38 +60,39 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
(cache) =>
produce(cache, (draftCache) => {
draftCache.findProject.labels = cache.findProject.labels.filter(
label => label.id !== newLabelData.data?.deleteProjectLabel.id,
(label) => label.id !== newLabelData.data?.deleteProjectLabel.id,
);
}),
{ 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 [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
return (
<>
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
<LabelManager
labels={labels}
labels={data ? data.findProject.labels : []}
taskLabels={currentTaskLabels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
onLabelEdit={(labelId) => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
onLabelToggle={(labelId) => {
if (onLabelToggle) {
if (currentTaskLabels.find(t => t.projectLabel.id === labelId)) {
setCurrentTaskLabels(currentTaskLabels.filter(t => t.projectLabel.id !== labelId));
} else {
const newProjectLabel = labels.find(l => l.id === labelId);
if (currentTaskLabels.find((t) => t.projectLabel.id === labelId)) {
setCurrentTaskLabels(currentTaskLabels.filter((t) => t.projectLabel.id !== labelId));
} else if (data) {
const newProjectLabel = data.findProject.labels.find((l) => l.id === labelId);
if (newProjectLabel) {
setCurrentTaskLabels([
...currentTaskLabels,
@ -112,14 +112,14 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
<LabelEditor
labelColors={labelColors}
label={labels.find(label => label.id === currentLabel) ?? null}
label={labels.find((label) => label.id === currentLabel) ?? null}
onLabelEdit={(projectLabelID, name, color) => {
if (projectLabelID) {
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
}
setTab(0);
}}
onLabelDelete={labelID => {
onLabelDelete={(labelID) => {
deleteProjectLabel({ variables: { projectLabelID: labelID } });
setTab(0);
}}

View File

@ -31,11 +31,11 @@ import produce from 'immer';
import NOOP from 'shared/utils/noop';
import useStateWithLocalStorage from 'shared/hooks/useStateWithLocalStorage';
import localStorage from 'shared/utils/localStorage';
import polling from 'shared/utils/polling';
import Board, { BoardLoading } from './Board';
import Details from './Details';
import LabelManagerEditor from './LabelManagerEditor';
import UserManagementPopup from './UserManagementPopup';
import polling from 'shared/utils/polling';
type TaskRouteProps = {
taskID: string;
@ -269,7 +269,6 @@ const Project = () => {
}}
taskID={task.id}
labelColors={data.labelColors}
labels={labelsRef}
taskLabels={taskLabelsRef}
projectID={projectID}
/>,

View File

@ -524,7 +524,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
members={data.findTeam.members}
warning={member.role && member.role.code === 'owner' ? warning : null}
// canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} TODO: add permission check
canChangeRole={true}
canChangeRole
onChangeRole={(roleCode) => {
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
}}

View File

@ -10,6 +10,215 @@ import Button from 'shared/components/Button';
import NOOP from 'shared/utils/noop';
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)`
padding-left: 4px;
`;
@ -54,7 +263,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
position: relative;
text-decoration: none;
${props =>
${(props) =>
props.disabled
? css`
user-select: none;
@ -75,7 +284,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span`
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`
@ -86,13 +295,13 @@ export const Separator = styled.div`
export const WarningText = styled.span`
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;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: ${props => props.theme.colors.text.primary};
color: ${(props) => props.theme.colors.text.primary};
`;
export const RemoveMemberButton = styled(Button)`
@ -161,8 +370,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
.filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map((perm) => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
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.
</DeleteDescription>
<UserSelect
onChange={v => setDeleteUser(v)}
onChange={(v) => setDeleteUser(v)}
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.
</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
onClick={() => {
// 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 = {
active: boolean;
name: string;
@ -591,7 +595,7 @@ const Admin: React.FC<AdminProps> = ({
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
{canInviteUser && (
<InviteMemberButton
onClick={$target => {
onClick={($target) => {
onAddUser($target);
}}
>
@ -602,7 +606,7 @@ const Admin: React.FC<AdminProps> = ({
</ListActions>
</MemberListHeader>
<MemberList>
{users.map(member => {
{users.map((member) => {
const projectTotal = member.owned.projects.length + member.member.projects.length;
return (
<MemberListItem>
@ -615,7 +619,7 @@ const Admin: React.FC<AdminProps> = ({
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
<MemberItemOption
variant="outline"
onClick={$target => {
onClick={($target) => {
showPopup(
$target,
<TeamRoleManagerPopup
@ -626,7 +630,7 @@ const Admin: React.FC<AdminProps> = ({
onUpdateUserPassword(user, password);
}}
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
onChangeRole={roleCode => {
onChangeRole={(roleCode) => {
updateUserRole({ variables: { userID: member.id, roleCode } });
}}
onDeleteUser={onDeleteUser}
@ -640,7 +644,7 @@ const Admin: React.FC<AdminProps> = ({
</MemberListItem>
);
})}
{invitedUsers.map(member => {
{invitedUsers.map((member) => {
return (
<MemberListItem>
<MemberProfile
@ -664,7 +668,7 @@ const Admin: React.FC<AdminProps> = ({
<MemberItemOptions>
<MemberItemOption
variant="outline"
onClick={$target => {
onClick={($target) => {
showPopup(
$target,
<TeamRoleManagerPopup

View File

@ -7,6 +7,8 @@ import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns';
import { useForm, Controller } from 'react-hook-form';
import NOOP from 'shared/utils/noop';
import { Clock, Cross } from 'shared/icons';
import Select from 'react-select/src/Select';
import {
Wrapper,
@ -23,8 +25,6 @@ import {
ActionClock,
ActionLabel,
} from './Styles';
import { Clock, Cross } from 'shared/icons';
import Select from 'react-select/src/Select';
type DueDateManagerProps = {
task: Task;
@ -190,21 +190,6 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
};
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 (
<Wrapper>
<DateRangeInputs>

View File

@ -100,7 +100,7 @@ const List = React.forwardRef(
/>
{!isPublic && (
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
<Ellipsis size={16} color="#c2c6dc" />
<Ellipsis vertical={false} size={16} color="#c2c6dc" />
</ListExtraMenuButtonWrapper>
)}
</Header>

View File

@ -98,8 +98,8 @@ const ProjectName = styled.input`
font-weight: 400;
&:focus {
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
}
`;
const ProjectNameLabel = styled.label`
@ -210,8 +210,8 @@ const CreateButton = styled.button`
&:hover {
color: #fff;
background: ${props => props.theme.colors.primary};
border-color: ${props => props.theme.colors.primary};
background: ${(props) => props.theme.colors.primary};
border-color: ${(props) => props.theme.colors.primary};
}
`;
type NewProjectProps = {
@ -224,7 +224,7 @@ type NewProjectProps = {
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
const [projectName, setProjectName] = useState('');
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 (
<Overlay>
<Content>
@ -234,7 +234,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
onClose();
}}
>
<ArrowLeft color="#c2c6dc" />
<ArrowLeft width={16} height={16} color="#c2c6dc" />
</HeaderLeft>
<HeaderRight
onClick={() => {
@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
onChange={(e: any) => {
setTeam(e.value);
}}
value={options.find(d => d.value === team)}
value={options.find((d) => d.value === team)}
styles={colourStyles}
classNamePrefix="teamSelect"
options={options}

View File

@ -218,7 +218,7 @@ export const PopupProvider: React.FC = ({ children }) => {
const setTab = (newTab: number, options?: PopupOptions) => {
setState((prevState: PopupState) =>
produce(prevState, draftState => {
produce(prevState, (draftState) => {
draftState.previousTab = currentState.currentTab;
draftState.currentTab = newTab;
if (options) {
@ -296,7 +296,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
<Wrapper padding borders>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
<AngleLeft size={16} color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
@ -332,7 +332,7 @@ export const Popup: React.FC<PopupProps> = ({ borders = true, padding = true, ti
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
<AngleLeft size={16} color="#c2c6dc" />
</PreviousButton>
)}
{title && (

View File

@ -2,15 +2,15 @@ import React, { useRef } from 'react';
import styled from 'styled-components';
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
width: ${props => props.size}px;
height: ${props => props.size}px;
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
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-size: contain;
`;
@ -22,6 +22,10 @@ type ProfileIconProps = {
};
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
let realSize = size;
if (size === null) {
realSize = 28;
}
const $profileRef = useRef<HTMLDivElement>(null);
return (
<Container
@ -29,7 +33,7 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
onClick={() => {
onProfileClick($profileRef, user);
}}
size={size}
size={realSize}
backgroundURL={user.profileIcon.url ?? 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;

View File

@ -311,7 +311,7 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
};
type UserInfoData = {
full_name: string;
fullName: string;
bio: string;
initials: string;
email: string;
@ -355,12 +355,12 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
})}
>
<UserInfoInput
{...register('full_name', { required: 'Full name is required' })}
{...register('fullName', { required: 'Full name is required' })}
defaultValue={profile.fullName}
width="100%"
label="Name"
/>
{errors.full_name && <FormError>{errors.full_name.message}</FormError>}
{errors.fullName && <FormError>{errors.fullName.message}</FormError>}
<UserInfoInput
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
{...register('initials', {

View File

@ -13,6 +13,7 @@ import {
Smile,
} from 'shared/icons';
import { toArray } from 'react-emoji-render';
import { useCurrentUser } from 'App/context';
import DOMPurify from 'dompurify';
import TaskAssignee from 'shared/components/TaskAssignee';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
@ -80,11 +81,8 @@ import {
ActivityItemHeaderTitleName,
ActivityItemComment,
} from './Styles';
import { useCurrentUser } from 'App/context';
type TaskDetailsProps = {};
const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
const TaskDetailsLoading: React.FC = () => {
const { user } = useCurrentUser();
return (
<Container>

View File

@ -1,4 +1,5 @@
import React, { useState, useRef } from 'react';
import { useCurrentUser } from 'App/context';
import {
Plus,
User,
@ -81,9 +82,8 @@ import {
} from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd';
import { plugin as em } from './remark';
import plugin from './remark';
import ActivityMessage from './ActivityMessage';
import { useCurrentUser } from 'App/context';
const parseEmojis = (value: string) => {
const emojisArray = toArray(value);
@ -136,7 +136,7 @@ const StreamComment: React.FC<StreamCommentProps> = ({
onCreateComment={onUpdateComment}
/>
) : (
<ReactMarkdown skipHtml plugins={[em]}>
<ReactMarkdown skipHtml plugins={[plugin]}>
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
</ReactMarkdown>
)}
@ -514,6 +514,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<Editor
defaultValue={task.description ?? ''}
readOnly={user === null || !editTaskDescription}
theme={dark}
autoFocus
onChange={(value) => {
setSaveTimeout(() => {

View File

@ -1,4 +1,4 @@
import visit from 'unist-util-visit';
import { visit } from 'unist-util-visit';
import emoji from 'node-emoji';
import { emoticon } from 'emoticon';
import { Emoji } from 'emoji-mart';
@ -15,17 +15,17 @@ const DEFAULT_SETTINGS = {
};
function plugin(options) {
const settings = Object.assign({}, DEFAULT_SETTINGS, options);
const settings = { ...DEFAULT_SETTINGS, ...options };
const pad = !!settings.padSpaceAfter;
const emoticonEnable = !!settings.emoticon;
function getEmojiByShortCode(match) {
// 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 iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern
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 trimmedChar = iconPart ? match.slice(-1) : '';
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;
}
@ -33,7 +33,7 @@ function plugin(options) {
console.log(match);
const got = emoji.get(match);
if (pad && got !== match) {
return got + ' ';
return `${got} `;
}
console.log(got);
@ -65,4 +65,4 @@ function plugin(options) {
return transformer;
}
export { plugin };
export default plugin;

View File

@ -1,8 +1,8 @@
import React, { useRef, useState, useEffect } from 'react';
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
import { Link } from 'react-router-dom';
import { RoleCode } from 'shared/generated/graphql';
import * as S from './Styles';
import { Link } from 'react-router-dom';
export type MenuItem = {
name: string;

View File

@ -144,7 +144,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
</ProjectSettingsButton>
{onFavorite && (
<ProjectSettingsButton onClick={() => onFavorite()}>
<Star width={16} height={16} color="#c2c6dc" />
<Star filled width={16} height={16} color="#c2c6dc" />
</ProjectSettingsButton>
)}
</>
@ -228,7 +228,7 @@ const NavBar: React.FC<NavBarProps> = ({
<NavbarWrapper>
<NavbarHeader>
<ProjectActions>
<ProjectSwitch ref={$finder} onClick={e => onOpenProjectFinder($finder)}>
<ProjectSwitch ref={$finder} onClick={(e) => onOpenProjectFinder($finder)}>
<ProjectSwitchInner>
<TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
</ProjectSwitchInner>
@ -304,7 +304,7 @@ const NavBar: React.FC<NavBarProps> = ({
))}
{canInviteUser && (
<InviteButton
onClick={$target => {
onClick={($target) => {
if (onInviteUser) {
onInviteUser($target);
}

File diff suppressed because it is too large Load Diff

View 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
}
}
`;

View File

@ -16,9 +16,4 @@ const AngleLeft = ({ size, color }: Props) => {
);
};
AngleLeft.defaultProps = {
size: 16,
color: '#000',
};
export default AngleLeft;

View File

@ -17,10 +17,4 @@ const ArrowLeft = ({ width, height, color }: Props) => {
);
};
ArrowLeft.defaultProps = {
width: 16,
height: 16,
color: '#000',
};
export default ArrowLeft;

View File

@ -13,9 +13,4 @@ const Bell = ({ size, color }: Props) => {
);
};
Bell.defaultProps = {
size: 16,
color: '#000',
};
export default Bell;

View File

@ -14,9 +14,4 @@ const Bin = ({ size, color }: Props) => {
);
};
Bin.defaultProps = {
size: 16,
color: '#000',
};
export default Bin;

View File

@ -16,9 +16,4 @@ const Cog = ({ size, color }: Props) => {
);
};
Cog.defaultProps = {
size: 16,
color: '#000',
};
export default Cog;

View File

@ -21,10 +21,4 @@ const Ellipsis = ({ size, color, vertical }: Props) => {
);
};
Ellipsis.defaultProps = {
size: 16,
color: '#000',
vertical: false,
};
export default Ellipsis;

View File

@ -13,9 +13,4 @@ const Exit = ({ size, color }: Props) => {
);
};
Exit.defaultProps = {
size: 16,
color: '#000',
};
export default Exit;

View File

@ -13,9 +13,4 @@ const Question = ({ size, color }: Props) => {
);
};
Question.defaultProps = {
size: 16,
color: '#000',
};
export default Question;

View File

@ -13,9 +13,4 @@ const Stack = ({ size, color }: Props) => {
);
};
Stack.defaultProps = {
size: 16,
color: '#000',
};
export default Stack;

View File

@ -25,11 +25,4 @@ const Star = ({ width, height, color, filled }: Props) => {
);
};
Star.defaultProps = {
width: 24,
height: 16,
color: '#000',
filled: false,
};
export default Star;

View File

@ -14,9 +14,4 @@ const Users = ({ size, color }: Props) => {
);
};
Users.defaultProps = {
size: 16,
color: '#000',
};
export default Users;

View File

@ -7,7 +7,7 @@ export function updateApolloCache<T>(
client: DataProxy,
document: DocumentNode,
update: UpdateCacheFn<T>,
variables?: object,
variables?: any,
) {
let queryArgs: DataProxy.Query<any, any>;
if (variables) {

View File

@ -45,6 +45,56 @@ export const base = {
codeInserted: '#202746',
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,
blockToolbarTrigger: colors.white,
blockToolbarTriggerIcon: colors.white,
@ -53,6 +103,10 @@ export const base = {
blockToolbarHoverBackground: colors.primary,
blockToolbarDivider: colors.almostWhite,
blockToolbarIcon: undefined,
blockToolbarIconSelected: colors.black,
blockToolbarTextSelected: colors.black,
noticeInfoBackground: '#F5BE31',
noticeInfoText: colors.almostBlack,
noticeTipBackground: '#9E5CF7',

File diff suppressed because it is too large Load Diff

View File

@ -334,7 +334,7 @@ func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
Active: false,
})
if err != nil {
log.Error("issue registering user account")
log.WithError(err).Error("issue registering user account")
w.WriteHeader(http.StatusInternalServerError)
return
}

View File

@ -3,10 +3,12 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
"time"
@ -19,6 +21,8 @@ const (
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"
func runWith(env map[string]string, cmd string, inArgs ...interface{}) error {
@ -98,8 +102,10 @@ func (Backend) GenFrontend() error {
}
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")
}
tag, err := sh.Output("git", "describe", "--exact-match", "--tags")
if err != nil {
tag = "nightly"
@ -145,6 +151,7 @@ func (Backend) Schema() error {
return sh.Run("gqlgen")
}
// Test run golang unit tests
func (Backend) Test() error {
fmt.Println("running taskcafe backend unit tests")
return sh.RunV("go", "test", "./...")
@ -160,6 +167,58 @@ func 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
type Docker mg.Namespace

View 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

View 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