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/), 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

View File

@ -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

View File

@ -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",

View File

@ -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": {

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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 ?? '' });
} }

View File

@ -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}

View File

@ -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);
}} }}

View File

@ -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}
/>, />,

View File

@ -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 } });
}} }}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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 && (

View File

@ -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;

View File

@ -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', {

View File

@ -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>

View File

@ -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(() => {

View File

@ -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;

View File

@ -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;

View File

@ -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

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; 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; export default ArrowLeft;

View File

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

View File

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

View File

@ -16,9 +16,4 @@ const Cog = ({ size, color }: Props) => {
); );
}; };
Cog.defaultProps = {
size: 16,
color: '#000',
};
export default Cog; 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; export default Ellipsis;

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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',

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, 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
} }

View File

@ -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")
fmt.Println("[ignore] fatal: no tag matches") if err != nil {
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

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