Compare commits

..

No commits in common. "master" and "0.3.6" have entirely different histories.

146 changed files with 6298 additions and 13933 deletions

View File

@ -10,8 +10,6 @@ windows:
- yarn: - yarn:
- cd frontend - cd frontend
- yarn start - yarn start
- worker:
- go run cmd/taskcafe/main.go worker
- web/editor: - web/editor:
root: ./frontend root: ./frontend
panes: panes:

View File

@ -4,14 +4,6 @@ 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).
## UNRELEASED
### Added
- On login page, redirects to `/register` if no users exist (to help streamline initial setup)
### Fixed
- Fixes new user popup form so that it can now be submitted
## [0.3.5] - 2021-09-04 ## [0.3.5] - 2021-09-04
### Added ### Added

View File

@ -19,11 +19,17 @@ services:
ports: ports:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
redis: broker:
image: redis:6.2 image: rabbitmq:3-management
restart: always restart: always
ports: ports:
- 6379:6379 - 8060:15672
- 5672:5672
result_store:
image: memcached:1.6-alpine
restart: always
ports:
- 11211:11211
volumes: volumes:
taskcafe-postgres: taskcafe-postgres:

View File

@ -12,9 +12,6 @@ services:
environment: environment:
TASKCAFE_DATABASE_HOST: postgres TASKCAFE_DATABASE_HOST: postgres
TASKCAFE_MIGRATE: "true" TASKCAFE_MIGRATE: "true"
volumes:
- taskcafe-uploads:/root/uploads
postgres: postgres:
image: postgres:12.3-alpine image: postgres:12.3-alpine
restart: always restart: always
@ -30,8 +27,6 @@ services:
volumes: volumes:
taskcafe-postgres: taskcafe-postgres:
external: false external: false
taskcafe-uploads:
external: false
networks: networks:
taskcafe-test: taskcafe-test:

View File

@ -1,6 +1,6 @@
overwrite: true overwrite: true
schema: schema:
- '../internal/graph/schema/*.gql' - '../internal/graph/schema.graphqls'
documents: documents:
- 'src/shared/graphql/*.graphqls' - 'src/shared/graphql/*.graphqls'
- 'src/shared/graphql/**/*.ts' - 'src/shared/graphql/**/*.ts'

View File

@ -43,8 +43,6 @@
"immer": "^9.0.2", "immer": "^9.0.2",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"loglevel": "^1.7.1",
"loglevel-plugin-remote": "^0.6.8",
"node-emoji": "^1.10.0", "node-emoji": "^1.10.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^7.0.0", "query-string": "^7.0.0",

View File

@ -12,7 +12,7 @@ import {
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { useForm, Controller, UseFormSetError } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer'; import produce from 'immer';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
@ -20,7 +20,6 @@ import { useCurrentUser } from 'App/context';
import { Redirect } from 'react-router'; import { Redirect } from 'react-router';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import ControlledInput from 'shared/components/ControlledInput'; import ControlledInput from 'shared/components/ControlledInput';
import FormInput from 'shared/components/FormInput';
const DeleteUserWrapper = styled.div` const DeleteUserWrapper = styled.div`
display: flex; display: flex;
@ -78,7 +77,7 @@ const CreateUserButton = styled(Button)`
width: 100%; width: 100%;
`; `;
const AddUserInput = styled(FormInput)` const AddUserInput = styled(ControlledInput)`
margin-bottom: 8px; margin-bottom: 8px;
`; `;
@ -88,7 +87,7 @@ const InputError = styled.span`
`; `;
type AddUserPopupProps = { type AddUserPopupProps = {
onAddUser: (user: CreateUserData, setError: UseFormSetError<CreateUserData>) => void; onAddUser: (user: CreateUserData) => void;
}; };
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => { const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
@ -96,16 +95,16 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
setError,
control, control,
} = useForm<CreateUserData>(); } = useForm<CreateUserData>();
const createUser = (data: CreateUserData) => { const createUser = (data: CreateUserData) => {
onAddUser(data, setError); onAddUser(data);
}; };
return ( return (
<CreateUserForm onSubmit={handleSubmit(createUser)}> <CreateUserForm onSubmit={handleSubmit(createUser)}>
<AddUserInput <AddUserInput
floatingLabel
width="100%" width="100%"
label="Full Name" label="Full Name"
variant="alternate" variant="alternate"
@ -119,7 +118,6 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
variant="alternate" variant="alternate"
{...register('email', { required: 'Email is required' })} {...register('email', { required: 'Email is required' })}
/> />
{errors.email && <InputError>{errors.email.message}</InputError>}
<Controller <Controller
control={control} control={control}
name="roleCode" name="roleCode"
@ -243,15 +241,10 @@ TODO: add permision check
$target, $target,
<Popup tab={0} title="Add member" onClose={() => hidePopup()}> <Popup tab={0} title="Add member" onClose={() => hidePopup()}>
<AddUserPopup <AddUserPopup
onAddUser={(u, setError) => { onAddUser={(u) => {
const { roleCode, ...userData } = u; const { roleCode, ...userData } = u;
createUser({ createUser({ variables: { ...userData, roleCode: roleCode.value } });
variables: { ...userData, roleCode: roleCode.value }, hidePopup();
})
.then(() => hidePopup())
.catch((e) => {
setError('email', { type: 'validate', message: e.message });
});
}} }}
/> />
</Popup>, </Popup>,

View File

@ -32,6 +32,7 @@ type ValidateTokenResponse = {
const UserRequiredRoute: React.FC<any> = ({ children }) => { const UserRequiredRoute: React.FC<any> = ({ children }) => {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const location = useLocation(); const location = useLocation();
console.log('user required', user);
if (user) { if (user) {
return children; return children;
} }
@ -61,6 +62,7 @@ const Routes: React.FC = () => {
setLoading(false); setLoading(false);
}); });
}, []); }, []);
console.log('loading', loading);
if (loading) return null; if (loading) return null;
return ( return (
<Switch> <Switch>
@ -69,10 +71,11 @@ const Routes: React.FC = () => {
<Route exact path="/confirm" component={Confirm} /> <Route exact path="/confirm" component={Confirm} />
<Switch> <Switch>
<MainContent> <MainContent>
<Route path="/p/:projectID" component={Project} /> <Route path="/projects/:projectID" component={Project} />
<UserRequiredRoute> <UserRequiredRoute>
<Route exact path="/" component={Projects} /> <Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/teams/:teamID" component={Teams} /> <Route path="/teams/:teamID" component={Teams} />
<Route path="/profile" component={Profile} /> <Route path="/profile" component={Profile} />
<Route path="/admin" component={Admin} /> <Route path="/admin" component={Admin} />

View File

@ -1,16 +1,10 @@
import React, { useState } from 'react'; import React from 'react';
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar'; import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
import LoggedOutNavbar from 'shared/components/TopNavbar/LoggedOut'; import LoggedOutNavbar from 'shared/components/TopNavbar/LoggedOut';
import { ProfileMenu } from 'shared/components/DropdownMenu'; import { ProfileMenu } from 'shared/components/DropdownMenu';
import polling from 'shared/utils/polling';
import { useHistory, useRouteMatch } from 'react-router'; import { useHistory, useRouteMatch } from 'react-router';
import { useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import { import { RoleCode, useTopNavbarQuery } from 'shared/generated/graphql';
RoleCode,
useTopNavbarQuery,
useNotificationAddedSubscription,
useHasUnreadNotificationsQuery,
} from 'shared/generated/graphql';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import MiniProfile from 'shared/components/MiniProfile'; import MiniProfile from 'shared/components/MiniProfile';
import cache from 'App/cache'; import cache from 'App/cache';
@ -55,33 +49,15 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
onRemoveInvitedFromBoard, onRemoveInvitedFromBoard,
onRemoveFromBoard, onRemoveFromBoard,
}) => { }) => {
const [notifications, setNotifications] = useState<Array<{ id: string; notification: { actionType: string } }>>([]); const { data } = useTopNavbarQuery();
const { data } = useTopNavbarQuery({
onCompleted: (d) => {
setNotifications((n) => [...n, ...d.notifications]);
},
});
const { data: nData, loading } = useNotificationAddedSubscription({
onSubscriptionData: (d) => {
setNotifications((n) => {
if (d.subscriptionData.data) {
return [...n, d.subscriptionData.data.notificationAdded];
}
return n;
});
},
});
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();
const { setUser } = useCurrentUser(); const { setUser } = useCurrentUser();
const { data: unreadData, refetch: refetchHasUnread } = useHasUnreadNotificationsQuery({
pollInterval: polling.UNREAD_NOTIFICATIONS,
});
const history = useHistory(); const history = useHistory();
const onLogout = () => { const onLogout = () => {
fetch('/auth/logout', { fetch('/auth/logout', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}).then(async (x) => { }).then(async x => {
const { status } = x; const { status } = x;
if (status === 200) { if (status === 200) {
cache.reset(); cache.reset();
@ -118,20 +94,29 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
} }
}; };
// TODO: rewrite popup to contain subscription and notification fetch
const onNotificationClick = ($target: React.RefObject<HTMLElement>) => { const onNotificationClick = ($target: React.RefObject<HTMLElement>) => {
showPopup($target, <NotificationPopup onToggleRead={() => refetchHasUnread()} />, { if (data) {
width: 605, showPopup(
borders: false, $target,
diamondColor: theme.colors.primary, <NotificationPopup>
}); {data.notifications.map(notification => (
<NotificationItem
title={notification.entity.name}
description={`${notification.actor.name} added you as a meber to the task "${notification.entity.name}"`}
createdAt={notification.createdAt}
/>
))}
</NotificationPopup>,
{ width: 415, borders: false, diamondColor: theme.colors.primary },
);
}
}; };
// TODO: readd permision check // TODO: readd permision check
// const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID); // const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
const userIsTeamOrProjectAdmin = true; const userIsTeamOrProjectAdmin = true;
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => { const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
const member = projectInvitedMembers ? projectInvitedMembers.find((u) => u.email === email) : null; const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
if (member) { if (member) {
showPopup( showPopup(
$targetRef, $targetRef,
@ -159,7 +144,7 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
}; };
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => { const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
const member = projectMembers ? projectMembers.find((u) => u.id === memberID) : null; const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning = const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.'; 'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
if (member) { if (member) {
@ -168,7 +153,7 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
<MiniProfile <MiniProfile
warning={member.role && member.role.code === 'owner' ? warning : null} warning={member.role && member.role.code === 'owner' ? warning : null}
canChangeRole={userIsTeamOrProjectAdmin} canChangeRole={userIsTeamOrProjectAdmin}
onChangeRole={(roleCode) => { onChangeRole={roleCode => {
if (onChangeRole) { if (onChangeRole) {
onChangeRole(member.id, roleCode); onChangeRole(member.id, roleCode);
} }
@ -194,10 +179,9 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
return ( return (
<> <>
<TopNavbar <TopNavbar
hasUnread={unreadData ? unreadData.hasUnreadNotifications.unread : false}
name={name} name={name}
menuType={menuType} menuType={menuType}
onOpenProjectFinder={($target) => { onOpenProjectFinder={$target => {
showPopup( showPopup(
$target, $target,
<Popup tab={0} title={null}> <Popup tab={0} title={null}>

View File

@ -4,20 +4,10 @@ export const Container = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100vw;
height: 100vh; height: 100vh;
@media (max-width: 600px) {
position: relative;
top: 30%;
font-size: 150px;
}
`; `;
export const LoginWrapper = styled.div` export const LoginWrapper = styled.div`
width: 70%; width: 60%;
@media (max-width: 600px) {
width: 90%;
margin-top: 50vh;
}
`; `;

View File

@ -9,6 +9,7 @@ const Auth = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation<{ redirect: string } | undefined>(); const location = useLocation<{ redirect: string } | undefined>();
const { setUser } = useContext(UserContext); const { setUser } = useContext(UserContext);
console.log('auth');
const login = ( const login = (
data: LoginFormData, data: LoginFormData,
setComplete: (val: boolean) => void, setComplete: (val: boolean) => void,

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import Confirm from 'shared/components/Confirm'; import Confirm from 'shared/components/Confirm';
import { useHistory, useLocation } from 'react-router'; import { useHistory, useLocation } from 'react-router';
import * as QueryString from 'query-string'; import * as QueryString from 'query-string';
@ -9,16 +9,20 @@ const UsersConfirm = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const params = QueryString.parse(location.search); const params = QueryString.parse(location.search);
const [hasFailed, setFailed] = useState(false);
const { setUser } = useCurrentUser(); const { setUser } = useCurrentUser();
useEffect(() => { return (
<Container>
<LoginWrapper>
<Confirm
hasConfirmToken={params.confirmToken !== undefined}
onConfirmUser={setFailed => {
fetch('/auth/confirm', { fetch('/auth/confirm', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
confirmToken: params.confirmToken, confirmToken: params.confirmToken,
}), }),
}) })
.then(async (x) => { .then(async x => {
const { status } = x; const { status } = x;
if (status === 200) { if (status === 200) {
const response = await x.json(); const response = await x.json();
@ -26,17 +30,14 @@ const UsersConfirm = () => {
setUser(userID); setUser(userID);
history.push('/'); history.push('/');
} else { } else {
setFailed(true); setFailed();
} }
}) })
.catch(() => { .catch(() => {
setFailed(false); setFailed();
}); });
}, []); }}
return ( />
<Container>
<LoginWrapper>
<Confirm hasConfirmToken={params.confirmToken !== undefined} hasFailed={hasFailed} />
</LoginWrapper> </LoginWrapper>
</Container> </Container>
); );

View File

@ -562,36 +562,13 @@ const Projects = () => {
onCancel={() => null} onCancel={() => null}
onDueDateChange={(task, dueDate, hasTime) => { onDueDateChange={(task, dueDate, hasTime) => {
if (dateEditor.task) { if (dateEditor.task) {
hidePopup(); updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } });
updateTaskDueDate({ setDateEditor((prev) => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } }));
variables: {
taskID: dateEditor.task.id,
dueDate,
hasTime,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
setDateEditor((prev) => ({
...prev,
task: { ...task, dueDate: { at: dueDate.toISOString(), notifications: [] }, hasTime },
}));
} }
}} }}
onRemoveDueDate={(task) => { onRemoveDueDate={(task) => {
if (dateEditor.task) { if (dateEditor.task) {
hidePopup(); updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } });
updateTaskDueDate({
variables: {
taskID: dateEditor.task.id,
dueDate: null,
hasTime: false,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } })); setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } }));
} }
}} }}
@ -678,8 +655,8 @@ const Projects = () => {
if (a.dueDate === null && b.dueDate === null) return 0; if (a.dueDate === null && b.dueDate === null) return 0;
if (a.dueDate === null && b.dueDate !== null) return 1; if (a.dueDate === null && b.dueDate !== null) return 1;
if (a.dueDate !== null && b.dueDate === null) return -1; if (a.dueDate !== null && b.dueDate === null) return -1;
const first = dayjs(a.dueDate.at); const first = dayjs(a.dueDate);
const second = dayjs(b.dueDate.at); const second = dayjs(b.dueDate);
if (first.isSame(second, 'minute')) return 0; if (first.isSame(second, 'minute')) return 0;
if (first.isAfter(second)) return -1; if (first.isAfter(second)) return -1;
return 1; return 1;
@ -815,19 +792,10 @@ const Projects = () => {
history.push(`${match.url}/c/${task.id}`); history.push(`${match.url}/c/${task.id}`);
}} }}
onRemoveDueDate={() => { onRemoveDueDate={() => {
updateTaskDueDate({ updateTaskDueDate({ variables: { taskID: task.id, dueDate: null, hasTime: false } });
variables: {
taskID: task.id,
dueDate: null,
hasTime: false,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
}} }}
project={projectName ?? 'none'} project={projectName ?? 'none'}
dueDate={task.dueDate.at} dueDate={task.dueDate}
hasTime={task.hasTime ?? false} hasTime={task.hasTime ?? false}
name={task.name} name={task.name}
onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })} onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })}
@ -853,9 +821,7 @@ const Projects = () => {
<EditorCell width={120}> <EditorCell width={120}>
<DueDateEditorLabel> <DueDateEditorLabel>
{dateEditor.task.dueDate {dateEditor.task.dueDate
? dayjs(dateEditor.task.dueDate.at).format( ? dayjs(dateEditor.task.dueDate).format(dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D')
dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D',
)
: ''} : ''}
</DueDateEditorLabel> </DueDateEditorLabel>
</EditorCell> </EditorCell>

View File

@ -108,7 +108,7 @@ const ActionItemLine = styled.div`
margin: 0.25rem !important; margin: 0.25rem !important;
`; `;
type ControlFilterProps = { type FilterMetaProps = {
filters: TaskMetaFilters; filters: TaskMetaFilters;
userID: string; userID: string;
projectID: string; projectID: string;
@ -116,13 +116,7 @@ type ControlFilterProps = {
onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void; onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void;
}; };
const ControlFilter: React.FC<ControlFilterProps> = ({ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter, userID, projectID, members }) => {
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('');
@ -329,4 +323,4 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
); );
}; };
export default ControlFilter; export default FilterMeta;

View File

@ -30,7 +30,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};
} }
&:hover ${ActionExtraMenuContainer} { &:hover ${ActionExtraMenuContainer} {
visibility: visible; visibility: visible;
@ -69,11 +69,11 @@ export const ActionExtraMenuItem = 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};
} }
`; `;
const ActionExtraMenuSeparator = styled.li` const ActionExtraMenuSeparator = styled.li`
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;
@ -85,12 +85,12 @@ const ActiveIcon = styled(Checkmark)`
position: absolute; position: absolute;
`; `;
type ControlStatusProps = { type FilterStatusProps = {
filter: TaskStatusFilter; filter: TaskStatusFilter;
onChangeTaskStatusFilter: (filter: TaskStatusFilter) => void; onChangeTaskStatusFilter: (filter: TaskStatusFilter) => void;
}; };
const ControlStatus: React.FC<ControlStatusProps> = ({ filter, onChangeTaskStatusFilter }) => { const FilterStatus: React.FC<FilterStatusProps> = ({ filter, onChangeTaskStatusFilter }) => {
const [currentFilter, setFilter] = useState(filter); const [currentFilter, setFilter] = useState(filter);
const handleFilterChange = (f: TaskStatusFilter) => { const handleFilterChange = (f: TaskStatusFilter) => {
setFilter(f); setFilter(f);
@ -146,4 +146,4 @@ const ControlStatus: React.FC<ControlStatusProps> = ({ filter, onChangeTaskStatu
); );
}; };
export default ControlStatus; export default FilterStatus;

View File

@ -1,11 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting'; import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
import { Checkmark } from 'shared/icons'; import { mixin } from 'shared/utils/styles';
const ActiveIcon = styled(Checkmark)`
position: absolute;
`;
export const ActionsList = styled.ul` export const ActionsList = styled.ul`
margin: 0; margin: 0;
@ -25,7 +21,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};
} }
`; `;
@ -33,12 +29,21 @@ export const ActionTitle = styled.span`
margin-left: 20px; margin-left: 20px;
`; `;
type ControlSortProps = { const ActionItemSeparator = styled.li`
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
font-size: 12px;
padding-left: 4px;
padding-right: 4px;
padding-top: 0.75rem;
padding-bottom: 0.25rem;
`;
type SortPopupProps = {
sorting: TaskSorting; sorting: TaskSorting;
onChangeTaskSorting: (taskSorting: TaskSorting) => void; onChangeTaskSorting: (taskSorting: TaskSorting) => void;
}; };
const ControlSort: React.FC<ControlSortProps> = ({ sorting, onChangeTaskSorting }) => { const SortPopup: React.FC<SortPopupProps> = ({ sorting, onChangeTaskSorting }) => {
const [currentSorting, setSorting] = useState(sorting); const [currentSorting, setSorting] = useState(sorting);
const handleSetSorting = (s: TaskSorting) => { const handleSetSorting = (s: TaskSorting) => {
setSorting(s); setSorting(s);
@ -47,41 +52,35 @@ const ControlSort: React.FC<ControlSortProps> = ({ sorting, onChangeTaskSorting
return ( return (
<ActionsList> <ActionsList>
<ActionItem onClick={() => handleSetSorting({ type: TaskSortingType.NONE, direction: TaskSortingDirection.ASC })}> <ActionItem onClick={() => handleSetSorting({ type: TaskSortingType.NONE, direction: TaskSortingDirection.ASC })}>
{currentSorting.type === TaskSortingType.NONE && <ActiveIcon width={12} height={12} />}
<ActionTitle>None</ActionTitle> <ActionTitle>None</ActionTitle>
</ActionItem> </ActionItem>
<ActionItem <ActionItem
onClick={() => handleSetSorting({ type: TaskSortingType.DUE_DATE, direction: TaskSortingDirection.ASC })} onClick={() => handleSetSorting({ type: TaskSortingType.DUE_DATE, direction: TaskSortingDirection.ASC })}
> >
{currentSorting.type === TaskSortingType.DUE_DATE && <ActiveIcon width={12} height={12} />}
<ActionTitle>Due date</ActionTitle> <ActionTitle>Due date</ActionTitle>
</ActionItem> </ActionItem>
<ActionItem <ActionItem
onClick={() => handleSetSorting({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })} onClick={() => handleSetSorting({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })}
> >
{currentSorting.type === TaskSortingType.MEMBERS && <ActiveIcon width={12} height={12} />}
<ActionTitle>Members</ActionTitle> <ActionTitle>Members</ActionTitle>
</ActionItem> </ActionItem>
<ActionItem <ActionItem
onClick={() => handleSetSorting({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })} onClick={() => handleSetSorting({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })}
> >
{currentSorting.type === TaskSortingType.LABELS && <ActiveIcon width={12} height={12} />}
<ActionTitle>Labels</ActionTitle> <ActionTitle>Labels</ActionTitle>
</ActionItem> </ActionItem>
<ActionItem <ActionItem
onClick={() => handleSetSorting({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })} onClick={() => handleSetSorting({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
> >
{currentSorting.type === TaskSortingType.TASK_TITLE && <ActiveIcon width={12} height={12} />}
<ActionTitle>Task title</ActionTitle> <ActionTitle>Task title</ActionTitle>
</ActionItem> </ActionItem>
<ActionItem <ActionItem
onClick={() => handleSetSorting({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })} onClick={() => handleSetSorting({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })}
> >
{currentSorting.type === TaskSortingType.COMPLETE && <ActiveIcon width={12} height={12} />}
<ActionTitle>Complete</ActionTitle> <ActionTitle>Complete</ActionTitle>
</ActionItem> </ActionItem>
</ActionsList> </ActionsList>
); );
}; };
export default ControlSort; export default SortPopup;

View File

@ -49,9 +49,9 @@ import LabelManagerEditor from 'Projects/Project/LabelManagerEditor';
import Chip from 'shared/components/Chip'; import Chip from 'shared/components/Chip';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import ControlStatus from './ControlStatus'; import FilterStatus from './FilterStatus';
import ControlFilter from './ControlFilter'; import FilterMeta from './FilterMeta';
import ControlSort from './ControlSort'; import SortPopup from './SortPopup';
const FilterChip = styled(Chip)` const FilterChip = styled(Chip)`
margin-right: 4px; margin-right: 4px;
@ -60,20 +60,19 @@ const FilterChip = styled(Chip)`
type MetaFilterCloseFn = (meta: TaskMeta, key: string) => void; type MetaFilterCloseFn = (meta: TaskMeta, key: string) => void;
const renderTaskSortingLabel = (sorting: TaskSorting) => { const renderTaskSortingLabel = (sorting: TaskSorting) => {
switch (sorting.type) { if (sorting.type === TaskSortingType.TASK_TITLE) {
case TaskSortingType.TASK_TITLE: return 'Sort: Card title';
return 'Sort: Task Title';
case TaskSortingType.MEMBERS:
return 'Sort: Members';
case TaskSortingType.DUE_DATE:
return 'Sort: Due Date';
case TaskSortingType.LABELS:
return 'Sort: Labels';
case TaskSortingType.COMPLETE:
return 'Sort: Complete';
default:
return 'Sort';
} }
if (sorting.type === TaskSortingType.MEMBERS) {
return 'Sort: Members';
}
if (sorting.type === TaskSortingType.DUE_DATE) {
return 'Sort: Due Date';
}
if (sorting.type === TaskSortingType.LABELS) {
return 'Sort: Labels';
}
return 'Sort';
}; };
const renderMetaFilters = (filters: TaskMetaFilters, onClose: MetaFilterCloseFn) => { const renderMetaFilters = (filters: TaskMetaFilters, onClose: MetaFilterCloseFn) => {
@ -429,9 +428,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
createTask: { createTask: {
__typename: 'Task', __typename: 'Task',
id: `${Math.round(Math.random() * -1000000)}`, id: `${Math.round(Math.random() * -1000000)}`,
shortId: '',
name, name,
watched: false,
complete: false, complete: false,
completedAt: null, completedAt: null,
hasTime: false, hasTime: false,
@ -446,7 +443,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
checklist: null, checklist: null,
}, },
position, position,
dueDate: { at: null }, dueDate: null,
description: null, description: null,
labels: [], labels: [],
assigned: [], assigned: [],
@ -509,7 +506,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
showPopup( showPopup(
target, target,
<Popup tab={0} title={null}> <Popup tab={0} title={null}>
<ControlStatus <FilterStatus
filter={taskStatusFilter} filter={taskStatusFilter}
onChangeTaskStatusFilter={(filter) => { onChangeTaskStatusFilter={(filter) => {
setTaskStatusFilter(filter); setTaskStatusFilter(filter);
@ -529,7 +526,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
showPopup( showPopup(
target, target,
<Popup tab={0} title={null}> <Popup tab={0} title={null}>
<ControlSort <SortPopup
sorting={taskSorting} sorting={taskSorting}
onChangeTaskSorting={(sorting) => { onChangeTaskSorting={(sorting) => {
setTaskSorting(sorting); setTaskSorting(sorting);
@ -547,7 +544,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
onClick={(target) => { onClick={(target) => {
showPopup( showPopup(
target, target,
<ControlFilter <FilterMeta
filters={taskMetaFilters} filters={taskMetaFilters}
onChangeTaskMetaFilter={(filter) => { onChangeTaskMetaFilter={(filter) => {
setTaskMetaFilters(filter); setTaskMetaFilters(filter);
@ -606,7 +603,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<SimpleLists <SimpleLists
isPublic={user === null} isPublic={user === null}
onTaskClick={(task) => { onTaskClick={(task) => {
history.push(`${match.url}/c/${task.shortId}`); history.push(`${match.url}/c/${task.id}`);
}} }}
onCardLabelClick={onCardLabelClick ?? NOOP} onCardLabelClick={onCardLabelClick ?? NOOP}
cardLabelVariant={cardLabelVariant ?? 'large'} cardLabelVariant={cardLabelVariant ?? 'large'}
@ -801,30 +798,12 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={(t) => { onRemoveDueDate={(t) => {
hidePopup(); updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
updateTaskDueDate({ // hidePopup();
variables: {
taskID: t.id,
dueDate: null,
hasTime: false,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
}} }}
onDueDateChange={(t, newDueDate, hasTime) => { onDueDateChange={(t, newDueDate, hasTime) => {
hidePopup(); updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
updateTaskDueDate({ // hidePopup();
variables: {
taskID: t.id,
dueDate: newDueDate,
hasTime,
deleteNotifications: [],
updateNotifications: [],
createNotifications: [],
},
});
}} }}
onCancel={NOOP} onCancel={NOOP}
/> />

View File

@ -7,12 +7,10 @@ import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory, useParams } from 'react-router'; import { useRouteMatch, useHistory, useParams } from 'react-router';
import { import {
useDeleteTaskChecklistMutation, useDeleteTaskChecklistMutation,
useToggleTaskWatchMutation,
useUpdateTaskChecklistNameMutation, useUpdateTaskChecklistNameMutation,
useUpdateTaskChecklistItemLocationMutation, useUpdateTaskChecklistItemLocationMutation,
useCreateTaskChecklistMutation, useCreateTaskChecklistMutation,
useFindTaskQuery, useFindTaskQuery,
DueDateNotificationDuration,
useUpdateTaskDueDateMutation, useUpdateTaskDueDateMutation,
useSetTaskCompleteMutation, useSetTaskCompleteMutation,
useAssignTaskMutation, useAssignTaskMutation,
@ -218,7 +216,6 @@ const Details: React.FC<DetailsProps> = ({
); );
}, },
}); });
const [toggleTaskWatch] = useToggleTaskWatchMutation();
const [createTaskComment] = useCreateTaskCommentMutation({ const [createTaskComment] = useCreateTaskCommentMutation({
update: (client, response) => { update: (client, response) => {
updateApolloCache<FindTaskQuery>( updateApolloCache<FindTaskQuery>(
@ -443,19 +440,6 @@ const Details: React.FC<DetailsProps> = ({
); );
}} }}
task={data.findTask} task={data.findTask}
onToggleTaskWatch={(task, watched) => {
toggleTaskWatch({
variables: { taskID: task.id },
optimisticResponse: {
__typename: 'Mutation',
toggleTaskWatch: {
id: task.id,
__typename: 'Task',
watched,
},
},
});
}}
onCreateComment={(task, message) => { onCreateComment={(task, message) => {
createTaskComment({ variables: { taskID: task.id, message } }); createTaskComment({ variables: { taskID: task.id, message } });
}} }}
@ -556,8 +540,7 @@ const Details: React.FC<DetailsProps> = ({
bio="None" bio="None"
onRemoveFromTask={() => { onRemoveFromTask={() => {
if (user) { if (user) {
unassignTask({ variables: { taskID: data.findTask.id, userID: member.id ?? '' } }); unassignTask({ variables: { taskID: data.findTask.id, userID: user ?? '' } });
hidePopup();
} }
}} }}
/> />
@ -648,79 +631,12 @@ const Details: React.FC<DetailsProps> = ({
<DueDateManager <DueDateManager
task={task} task={task}
onRemoveDueDate={(t) => { onRemoveDueDate={(t) => {
updateTaskDueDate({ updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
variables: { // hidePopup();
taskID: t.id,
dueDate: null,
hasTime: false,
deleteNotifications: t.dueDate.notifications
? t.dueDate.notifications.map((n) => ({ id: n.id }))
: [],
updateNotifications: [],
createNotifications: [],
},
});
hidePopup();
}} }}
onDueDateChange={(t, newDueDate, hasTime, notifications) => { onDueDateChange={(t, newDueDate, hasTime) => {
const updatedNotifications = notifications.current updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
.filter((c) => c.externalId !== null) // hidePopup();
.map((c) => {
let duration = DueDateNotificationDuration.Minute;
switch (c.duration.value) {
case 'hour':
duration = DueDateNotificationDuration.Hour;
break;
case 'day':
duration = DueDateNotificationDuration.Day;
break;
case 'week':
duration = DueDateNotificationDuration.Week;
break;
default:
break;
}
return {
id: c.externalId ?? '',
period: c.period,
duration,
};
});
const newNotifications = notifications.current
.filter((c) => c.externalId === null)
.map((c) => {
let duration = DueDateNotificationDuration.Minute;
switch (c.duration.value) {
case 'hour':
duration = DueDateNotificationDuration.Hour;
break;
case 'day':
duration = DueDateNotificationDuration.Day;
break;
case 'week':
duration = DueDateNotificationDuration.Week;
break;
default:
break;
}
return {
taskID: task.id,
period: c.period,
duration,
};
});
// const updatedNotifications = notifications.filter(c => c.externalId === null);
updateTaskDueDate({
variables: {
taskID: t.id,
dueDate: newDueDate,
hasTime,
createNotifications: newNotifications,
updateNotifications: updatedNotifications,
deleteNotifications: notifications.removed.map((n) => ({ id: n })),
},
});
hidePopup();
}} }}
onCancel={NOOP} onCancel={NOOP}
/> />

View File

@ -87,7 +87,7 @@ const Project = () => {
} }
} }
}), }),
{ projectID: data ? data.findProject.id : '' }, { projectID },
), ),
}); });
@ -100,7 +100,7 @@ const Project = () => {
produce(cache, (draftCache) => { produce(cache, (draftCache) => {
draftCache.findProject.name = newName.data?.updateProjectName.name ?? ''; draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
}), }),
{ projectID: data ? data.findProject.id : '' }, { projectID },
); );
}, },
}); });
@ -123,7 +123,7 @@ const Project = () => {
]; ];
} }
}), }),
{ projectID: data ? data.findProject.id : '' }, { projectID },
); );
}, },
}); });
@ -138,7 +138,7 @@ const Project = () => {
(m) => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '', (m) => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
); );
}), }),
{ projectID: data ? data.findProject.id : '' }, { projectID },
); );
}, },
}); });
@ -153,7 +153,7 @@ const Project = () => {
(m) => m.id !== response.data?.deleteProjectMember.member.id, (m) => m.id !== response.data?.deleteProjectMember.member.id,
); );
}), }),
{ projectID: data ? data.findProject.id : '' }, { projectID },
); );
}, },
}); });
@ -171,29 +171,29 @@ const Project = () => {
<> <>
<GlobalTopNavbar <GlobalTopNavbar
onChangeRole={(userID, roleCode) => { onChangeRole={(userID, roleCode) => {
updateProjectMemberRole({ variables: { userID, roleCode, projectID: data ? data.findProject.id : '' } }); updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
}} }}
onChangeProjectOwner={() => { onChangeProjectOwner={() => {
hidePopup(); hidePopup();
}} }}
onRemoveFromBoard={(userID) => { onRemoveFromBoard={(userID) => {
deleteProjectMember({ variables: { userID, projectID: data ? data.findProject.id : '' } }); deleteProjectMember({ variables: { userID, projectID } });
hidePopup(); hidePopup();
}} }}
onRemoveInvitedFromBoard={(email) => { onRemoveInvitedFromBoard={(email) => {
deleteInvitedProjectMember({ variables: { projectID: data ? data.findProject.id : '', email } }); deleteInvitedProjectMember({ variables: { projectID, email } });
hidePopup(); hidePopup();
}} }}
onSaveProjectName={(projectName) => { onSaveProjectName={(projectName) => {
updateProjectName({ variables: { projectID: data ? data.findProject.id : '', name: projectName } }); updateProjectName({ variables: { projectID, name: projectName } });
}} }}
onInviteUser={($target) => { onInviteUser={($target) => {
showPopup( showPopup(
$target, $target,
<UserManagementPopup <UserManagementPopup
projectID={data ? data.findProject.id : ''} projectID={projectID}
onInviteProjectMembers={(members) => { onInviteProjectMembers={(members) => {
inviteProjectMembers({ variables: { projectID: data ? data.findProject.id : '', members } }); inviteProjectMembers({ variables: { projectID, members } });
hidePopup(); hidePopup();
}} }}
users={data.users} users={data.users}

View File

@ -9,7 +9,6 @@ import {
GetProjectsDocument, GetProjectsDocument,
GetProjectsQuery, GetProjectsQuery,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import FormInput from 'shared/components/FormInput';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NewProject from 'shared/components/NewProject'; import NewProject from 'shared/components/NewProject';
@ -53,7 +52,7 @@ const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
return ( return (
<CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}> <CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}>
{errors.name && <ErrorText>{errors.name.message}</ErrorText>} {errors.name && <ErrorText>{errors.name.message}</ErrorText>}
<FormInput width="100%" label="Team name" variant="alternate" {...register('name')} /> <ControlledInput width="100%" label="Team name" variant="alternate" {...register('name')} />
<CreateTeamButton type="submit">Create</CreateTeamButton> <CreateTeamButton type="submit">Create</CreateTeamButton>
</CreateTeamFormContainer> </CreateTeamFormContainer>
); );
@ -307,7 +306,7 @@ const Projects = () => {
<ProjectList> <ProjectList>
{personalProjects.map((project, idx) => ( {personalProjects.map((project, idx) => (
<ProjectListItem key={project.id}> <ProjectListItem key={project.id}>
<ProjectTile color={colors[idx % 5]} to={`/p/${project.shortId}`}> <ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
<ProjectTileFade /> <ProjectTileFade />
<ProjectTileDetails> <ProjectTileDetails>
<ProjectTileName>{project.name}</ProjectTileName> <ProjectTileName>{project.name}</ProjectTileName>
@ -351,7 +350,7 @@ const Projects = () => {
<ProjectList> <ProjectList>
{team.projects.map((project, idx) => ( {team.projects.map((project, idx) => (
<ProjectListItem key={project.id}> <ProjectListItem key={project.id}>
<ProjectTile color={colors[idx % 5]} to={`/p/${project.shortId}`}> <ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
<ProjectTileFade /> <ProjectTileFade />
<ProjectTileDetails> <ProjectTileDetails>
<ProjectTileName>{project.name}</ProjectTileName> <ProjectTileName>{project.name}</ProjectTileName>

View File

@ -17,7 +17,6 @@ const UsersRegister = () => {
<Register <Register
registered={registered} registered={registered}
onSubmit={(data, setComplete, setError) => { onSubmit={(data, setComplete, setError) => {
let isRedirected = false;
if (data.password !== data.password_confirm) { if (data.password !== data.password_confirm) {
setError('password', { type: 'error', message: 'Passwords must match' }); setError('password', { type: 'error', message: 'Passwords must match' });
setError('password_confirm', { type: 'error', message: 'Passwords must match' }); setError('password_confirm', { type: 'error', message: 'Passwords must match' });
@ -39,23 +38,20 @@ const UsersRegister = () => {
.then(async (x) => { .then(async (x) => {
const response = await x.json(); const response = await x.json();
const { setup } = response; const { setup } = response;
console.log(response);
if (setup) { if (setup) {
history.replace(`/confirm?confirmToken=xxxx`); history.replace(`/confirm?confirmToken=xxxx`);
isRedirected = true;
} else if (params.confirmToken) { } else if (params.confirmToken) {
history.replace(`/confirm?confirmToken=${params.confirmToken}`); history.replace(`/confirm?confirmToken=${params.confirmToken}`);
isRedirected = true;
} else { } else {
setRegistered(true); setRegistered(true);
} }
}) })
.catch((e) => { .catch(() => {
toast('There was an issue trying to register'); toast('There was an issue trying to register');
}); });
} }
if (!isRedirected) {
setComplete(true); setComplete(true);
}
}} }}
/> />
</LoginWrapper> </LoginWrapper>

View File

@ -10,26 +10,10 @@ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween'; import isBetween from 'dayjs/plugin/isBetween';
import weekday from 'dayjs/plugin/weekday'; import weekday from 'dayjs/plugin/weekday';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import log from 'loglevel';
import remote from 'loglevel-plugin-remote';
import cache from './App/cache'; import cache from './App/cache';
import App from './App'; import App from './App';
if (process.env.REACT_APP_NODE_ENV === 'production') { // https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
remote.apply(log, { format: remote.json });
switch (process.env.REACT_APP_LOG_LEVEL) {
case 'info':
log.setLevel(log.levels.INFO);
break;
case 'debug':
log.setLevel(log.levels.DEBUG);
break;
default:
log.setLevel(log.levels.ERROR);
}
}
enableMapSet(); enableMapSet();
@ -38,8 +22,6 @@ dayjs.extend(weekday);
dayjs.extend(isBetween); dayjs.extend(isBetween);
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
dayjs.extend(updateLocale); dayjs.extend(updateLocale);
dayjs.extend(duration);
dayjs.extend(relativeTime);
dayjs.updateLocale('en', { dayjs.updateLocale('en', {
week: { week: {
dow: 1, // First day of week is Monday dow: 1, // First day of week is Monday
@ -48,6 +30,7 @@ dayjs.updateLocale('en', {
}); });
const client = new ApolloClient({ uri: '/graphql', cache }); const client = new ApolloClient({ uri: '/graphql', cache });
console.log('cloient', client);
ReactDOM.render( ReactDOM.render(
<ApolloProvider client={client}> <ApolloProvider client={client}>

View File

@ -6,11 +6,11 @@ const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: ${(props) => props.justifyTextContent}; justify-content: ${props => props.justifyTextContent};
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: ${(props) => props.fontSize}; font-size: ${props => props.fontSize};
color: ${(props) => props.theme.colors.text.secondary}; color: ${props => props.theme.colors.text.secondary};
${(props) => ${props =>
props.hasIcon && props.hasIcon &&
css` css`
padding-left: 4px; padding-left: 4px;
@ -23,11 +23,11 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 0.75rem 2rem; padding: 0.75rem 2rem;
border-radius: ${(props) => props.theme.borderRadius.alternate}; border-radius: ${props => props.theme.borderRadius.alternate};
display: flex; display: flex;
align-items: center; align-items: center;
${(props) => ${props =>
props.disabled && props.disabled &&
css` css`
opacity: 0.5; opacity: 0.5;
@ -37,8 +37,8 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
`; `;
const Filled = styled(Base)<{ hoverVariant: HoverVariant }>` const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
background: ${(props) => props.theme.colors[props.color]}; background: ${props => props.theme.colors[props.color]};
${(props) => ${props =>
props.hoverVariant === 'boxShadow' && props.hoverVariant === 'boxShadow' &&
css` css`
&:hover { &:hover {
@ -48,9 +48,9 @@ const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
`; `;
const Outline = styled(Base)<{ invert: boolean }>` const Outline = styled(Base)<{ invert: boolean }>`
border: 1px solid ${(props) => props.theme.colors[props.color]}; border: 1px solid ${props => props.theme.colors[props.color]};
background: transparent; background: transparent;
${(props) => ${props =>
props.invert props.invert
? css` ? css`
background: ${props.theme.colors[props.color]}); background: ${props.theme.colors[props.color]});
@ -74,7 +74,7 @@ const Outline = styled(Base)<{ invert: boolean }>`
const Flat = styled(Base)` const Flat = styled(Base)`
background: transparent; background: transparent;
&:hover { &:hover {
background: ${(props) => mixin.rgba(props.theme.colors[props.color], 0.2)}; background: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
} }
`; `;
@ -87,7 +87,7 @@ const LineX = styled.span<{ color: string }>`
bottom: -2px; bottom: -2px;
left: 50%; left: 50%;
transform: translate(-50%); transform: translate(-50%);
background: ${(props) => mixin.rgba(props.theme.colors[props.color], 1)}; background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
`; `;
const LineDown = styled(Base)` const LineDown = styled(Base)`
@ -96,7 +96,7 @@ const LineDown = styled(Base)`
border-width: 0; border-width: 0;
border-style: solid; border-style: solid;
border-bottom-width: 2px; border-bottom-width: 2px;
border-color: ${(props) => mixin.rgba(props.theme.colors[props.color], 0.2)}; border-color: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
&:hover ${LineX} { &:hover ${LineX} {
width: 100%; width: 100%;
@ -109,14 +109,17 @@ const LineDown = styled(Base)`
const Gradient = styled(Base)` const Gradient = styled(Base)`
background: linear-gradient( background: linear-gradient(
30deg, 30deg,
${(props) => mixin.rgba(props.theme.colors[props.color], 1)}, ${props => mixin.rgba(props.theme.colors[props.color], 1)},
${(props) => mixin.rgba(props.theme.colors[props.color], 0.5)} ${props => mixin.rgba(props.theme.colors[props.color], 0.5)}
); );
text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
&:hover {
transform: translateY(-2px);
}
`; `;
const Relief = styled(Base)` const Relief = styled(Base)`
background: ${(props) => mixin.rgba(props.theme.colors[props.color], 1)}; background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
-webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset; -webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);

View File

@ -1,27 +1,18 @@
import styled, { css, keyframes } from 'styled-components'; import styled, { css, keyframes } from 'styled-components';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle, CheckSquareOutline, Clock, Bubble } from 'shared/icons'; import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>` export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.secondary}, box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.secondary},
inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.secondary, 0.07)}; inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.secondary, 0.07)};
z-index: ${(props) => props.zIndex}; z-index: ${props => props.zIndex};
position: relative; position: relative;
`; `;
export const CommentsIcon = styled(Bubble)<{ color: 'success' | 'normal' }>`
${(props) =>
props.color === 'success' &&
css`
fill: ${props.theme.colors.success};
stroke: ${props.theme.colors.success};
`}
`;
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>` export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
${(props) => ${props =>
props.color === 'success' && props.color === 'success' &&
css` css`
fill: ${props.theme.colors.success}; fill: ${props.theme.colors.success};
@ -30,7 +21,7 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
`; `;
export const ClockIcon = styled(Clock)<{ color: string }>` export const ClockIcon = styled(Clock)<{ color: string }>`
fill: ${(props) => props.color}; fill: ${props => props.color};
`; `;
export const EditorTextarea = styled(TextareaAutosize)` export const EditorTextarea = styled(TextareaAutosize)`
@ -49,7 +40,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
padding: 0; padding: 0;
font-size: 14px; font-size: 14px;
line-height: 18px; line-height: 18px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
&:focus { &:focus {
border: none; border: none;
outline: none; outline: none;
@ -63,22 +54,6 @@ export const ListCardBadges = styled.div`
margin-left: -2px; margin-left: -2px;
`; `;
export const CommentsBadge = styled.div`
color: #5e6c84;
display: flex;
align-items: center;
margin: 0 6px 4px 0;
font-size: 12px;
max-width: 100%;
min-height: 20px;
overflow: hidden;
position: relative;
padding: 2px;
text-decoration: none;
text-overflow: ellipsis;
vertical-align: top;
`;
export const ListCardBadge = styled.div` export const ListCardBadge = styled.div`
color: #5e6c84; color: #5e6c84;
display: flex; display: flex;
@ -101,7 +76,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>` export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
font-size: 12px; font-size: 12px;
${(props) => ${props =>
props.isPastDue && props.isPastDue &&
css` css`
padding-left: 4px; padding-left: 4px;
@ -116,7 +91,7 @@ export const ListCardBadgeText = styled.span<{ color?: 'success' | 'normal' }>`
padding: 0 4px 0 6px; padding: 0 4px 0 6px;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
${(props) => props.color === 'success' && `color: ${props.theme.colors.success};`} ${props => props.color === 'success' && `color: ${props.theme.colors.success};`}
`; `;
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>` export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
@ -127,7 +102,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
cursor: pointer !important; cursor: pointer !important;
position: relative; position: relative;
background-color: ${(props) => background-color: ${props =>
props.isActive && !props.editable props.isActive && !props.editable
? mixin.darken(props.theme.colors.bg.secondary, 0.1) ? mixin.darken(props.theme.colors.bg.secondary, 0.1)
: `${props.theme.colors.bg.secondary}`}; : `${props.theme.colors.bg.secondary}`};
@ -144,7 +119,7 @@ export const ListCardDetails = styled.div<{ complete: boolean }>`
position: relative; position: relative;
z-index: 10; z-index: 10;
${(props) => props.complete && 'opacity: 0.6;'} ${props => props.complete && 'opacity: 0.6;'}
`; `;
const labelVariantExpandAnimation = keyframes` const labelVariantExpandAnimation = keyframes`
@ -182,7 +157,7 @@ export const ListCardLabelsWrapper = styled.div`
`; `;
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>` export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
${(props) => ${props =>
props.variant === 'small' props.variant === 'small'
? css` ? css`
height: 8px; height: 8px;
@ -208,14 +183,14 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
color: #fff; color: #fff;
display: flex; display: flex;
position: relative; position: relative;
background-color: ${(props) => props.color}; background-color: ${props => props.color};
`; `;
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>` export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
${(props) => ${props =>
props.toggleLabels && props.toggleLabels &&
props.toggleDirection === 'expand' && props.toggleDirection === 'expand' &&
css` css`
@ -226,7 +201,7 @@ export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirectio
animation: ${labelTextVariantExpandAnimation} 0.45s ease-out; animation: ${labelTextVariantExpandAnimation} 0.45s ease-out;
} }
`} `}
${(props) => ${props =>
props.toggleLabels && props.toggleLabels &&
props.toggleDirection === 'shrink' && props.toggleDirection === 'shrink' &&
css` css`
@ -250,7 +225,7 @@ export const ListCardOperation = styled.span`
top: 2px; top: 2px;
z-index: 100; z-index: 100;
&:hover { &:hover {
background-color: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.25)}; background-color: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.25)};
} }
`; `;
@ -259,7 +234,7 @@ export const CardTitle = styled.div`
margin: 0 0 4px; margin: 0 0 4px;
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
display: block; display: block;
align-items: center; align-items: center;
`; `;
@ -276,7 +251,7 @@ export const CardMembers = styled.div`
`; `;
export const CompleteIcon = styled(CheckCircle)` export const CompleteIcon = styled(CheckCircle)`
fill: ${(props) => props.theme.colors.success}; fill: ${props => props.theme.colors.success};
margin-right: 4px; margin-right: 4px;
flex-shrink: 0; flex-shrink: 0;
margin-bottom: -2px; margin-bottom: -2px;

View File

@ -23,8 +23,6 @@ import {
CardTitle, CardTitle,
CardMembers, CardMembers,
CardTitleText, CardTitleText,
CommentsIcon,
CommentsBadge,
} from './Styles'; } from './Styles';
type DueDate = { type DueDate = {
@ -49,7 +47,6 @@ type Props = {
dueDate?: DueDate; dueDate?: DueDate;
checklists?: Checklist | null; checklists?: Checklist | null;
labels?: Array<ProjectLabel>; labels?: Array<ProjectLabel>;
comments?: { unread: boolean; total: number } | null;
watched?: boolean; watched?: boolean;
wrapperProps?: any; wrapperProps?: any;
members?: Array<TaskUser> | null; members?: Array<TaskUser> | null;
@ -75,7 +72,6 @@ const Card = React.forwardRef(
taskGroupID, taskGroupID,
complete, complete,
toggleLabels = false, toggleLabels = false,
comments,
toggleDirection = 'shrink', toggleDirection = 'shrink',
setToggleLabels, setToggleLabels,
onClick, onClick,
@ -142,7 +138,7 @@ const Card = React.forwardRef(
onMouseEnter={() => setActive(true)} onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)} onMouseLeave={() => setActive(false)}
ref={$cardRef} ref={$cardRef}
onClick={(e) => { onClick={e => {
if (onClick) { if (onClick) {
onClick(e); onClick(e);
} }
@ -155,7 +151,7 @@ const Card = React.forwardRef(
<ListCardInnerContainer ref={$innerCardRef}> <ListCardInnerContainer ref={$innerCardRef}>
{!isPublic && isActive && !editable && ( {!isPublic && isActive && !editable && (
<ListCardOperation <ListCardOperation
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
if (onContextMenu) { if (onContextMenu) {
onContextMenu($innerCardRef, taskID, taskGroupID); onContextMenu($innerCardRef, taskID, taskGroupID);
@ -171,7 +167,7 @@ const Card = React.forwardRef(
<ListCardLabels <ListCardLabels
toggleLabels={toggleLabels} toggleLabels={toggleLabels}
toggleDirection={toggleDirection} toggleDirection={toggleDirection}
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
if (onCardLabelClick) { if (onCardLabelClick) {
onCardLabelClick(); onCardLabelClick();
@ -181,7 +177,7 @@ const Card = React.forwardRef(
{labels {labels
.slice() .slice()
.sort((a, b) => a.labelColor.position - b.labelColor.position) .sort((a, b) => a.labelColor.position - b.labelColor.position)
.map((label) => ( .map(label => (
<ListCardLabel <ListCardLabel
onAnimationEnd={() => { onAnimationEnd={() => {
if (setToggleLabels) { if (setToggleLabels) {
@ -202,13 +198,13 @@ const Card = React.forwardRef(
<EditorContent> <EditorContent>
{complete && <CompleteIcon width={16} height={16} />} {complete && <CompleteIcon width={16} height={16} />}
<EditorTextarea <EditorTextarea
onChange={(e) => { onChange={e => {
setCardTitle(e.currentTarget.value); setCardTitle(e.currentTarget.value);
if (onCardTitleChange) { if (onCardTitleChange) {
onCardTitleChange(e.currentTarget.value); onCardTitleChange(e.currentTarget.value);
} }
}} }}
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
}} }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -225,7 +221,7 @@ const Card = React.forwardRef(
<ListCardBadges> <ListCardBadges>
{watched && ( {watched && (
<ListCardBadge> <ListCardBadge>
<Eye width={12} height={12} /> <Eye width={8} height={8} />
</ListCardBadge> </ListCardBadge>
)} )}
{dueDate && ( {dueDate && (
@ -239,12 +235,6 @@ const Card = React.forwardRef(
<List width={8} height={8} /> <List width={8} height={8} />
</DescriptionBadge> </DescriptionBadge>
)} )}
{comments && (
<CommentsBadge>
<CommentsIcon color={comments.unread ? 'success' : 'normal'} width={8} height={8} />
<ListCardBadgeText color={comments.unread ? 'success' : 'normal'}>{comments.total}</ListCardBadgeText>
</CommentsBadge>
)}
{checklists && ( {checklists && (
<ListCardBadge> <ListCardBadge>
<ChecklistIcon <ChecklistIcon
@ -266,7 +256,7 @@ const Card = React.forwardRef(
size={28} size={28}
zIndex={members.length - idx} zIndex={members.length - idx}
member={member} member={member}
onMemberProfile={($target) => { onMemberProfile={$target => {
if (onCardMemberClick) { if (onCardMemberClick) {
onCardMemberClick($target, taskID, member.id); onCardMemberClick($target, taskID, member.id);
} }

View File

@ -21,7 +21,14 @@ import {
SubTitle, SubTitle,
} from './Styles'; } from './Styles';
const Confirm = ({ hasFailed, hasConfirmToken }: ConfirmProps) => { const Confirm = ({ onConfirmUser, hasConfirmToken }: ConfirmProps) => {
const [hasFailed, setFailed] = useState(false);
const setHasFailed = () => {
setFailed(true);
};
useEffect(() => {
onConfirmUser(setHasFailed);
});
return ( return (
<Wrapper> <Wrapper>
<Column> <Column>

View File

@ -1,8 +1,9 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Input from 'shared/components/Input';
import ControlledInput from 'shared/components/ControlledInput'; import ControlledInput from 'shared/components/ControlledInput';
import { Bell, Clock } from 'shared/icons'; import { Clock } from 'shared/icons';
export const Wrapper = styled.div` export const Wrapper = styled.div`
display: flex display: flex
@ -21,27 +22,27 @@ display: flex
& .react-datepicker__close-icon::after { & .react-datepicker__close-icon::after {
background: none; background: none;
font-size: 16px; font-size: 16px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
} }
& .react-datepicker-time__header { & .react-datepicker-time__header {
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
} }
& .react-datepicker__time-list-item { & .react-datepicker__time-list-item {
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
} }
& .react-datepicker__time-container .react-datepicker__time & .react-datepicker__time-container .react-datepicker__time
.react-datepicker__time-box ul.react-datepicker__time-list .react-datepicker__time-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover { li.react-datepicker__time-list-item:hover {
color: ${(props) => props.theme.colors.text.secondary}; color: ${props => props.theme.colors.text.secondary};
background: ${(props) => props.theme.colors.bg.secondary}; background: ${props => props.theme.colors.bg.secondary};
} }
& .react-datepicker__time-container .react-datepicker__time { & .react-datepicker__time-container .react-datepicker__time {
background: ${(props) => props.theme.colors.bg.primary}; background: ${props => props.theme.colors.bg.primary};
} }
& .react-datepicker--time-only { & .react-datepicker--time-only {
background: ${(props) => props.theme.colors.bg.primary}; background: ${props => props.theme.colors.bg.primary};
border: 1px solid ${(props) => props.theme.colors.border}; border: 1px solid ${props => props.theme.colors.border};
} }
& .react-datepicker * { & .react-datepicker * {
@ -81,12 +82,12 @@ display: flex
} }
& .react-datepicker__day--selected { & .react-datepicker__day--selected {
border-radius: 50%; border-radius: 50%;
background: ${(props) => props.theme.colors.primary}; background: ${props => props.theme.colors.primary};
color: #fff; color: #fff;
} }
& .react-datepicker__day--selected:hover { & .react-datepicker__day--selected:hover {
border-radius: 50%; border-radius: 50%;
background: ${(props) => props.theme.colors.primary}; background: ${props => props.theme.colors.primary};
color: #fff; color: #fff;
} }
& .react-datepicker__header { & .react-datepicker__header {
@ -94,12 +95,12 @@ display: flex
border: none; border: none;
} }
& .react-datepicker__header--time { & .react-datepicker__header--time {
border-bottom: 1px solid ${(props) => props.theme.colors.border}; border-bottom: 1px solid ${props => props.theme.colors.border};
} }
& .react-datepicker__input-container input { & .react-datepicker__input-container input {
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-color: ${(props) => props.theme.colors.alternate}; border-color: ${props => props.theme.colors.alternate};
background: #262c49; background: #262c49;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15); box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
padding: 0.7rem; padding: 0.7rem;
@ -113,7 +114,7 @@ padding: 0.7rem;
&:focus { &:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240); border: 1px solid rgba(115, 103, 240);
background: ${(props) => props.theme.colors.bg.primary}; background: ${props => props.theme.colors.bg.primary};
} }
`; `;
@ -141,9 +142,9 @@ export const AddDateRange = styled.div`
width: 100%; width: 100%;
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
color: ${(props) => mixin.rgba(props.theme.colors.primary, 0.8)}; color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
&:hover { &:hover {
color: ${(props) => mixin.rgba(props.theme.colors.primary, 1)}; color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
text-decoration: underline; text-decoration: underline;
} }
`; `;
@ -200,62 +201,18 @@ export const ActionsWrapper = styled.div`
align-items: center; align-items: center;
& .react-datepicker-wrapper { & .react-datepicker-wrapper {
margin-left: auto; margin-left: auto;
width: 86px; width: 82px;
} }
& .react-datepicker__input-container input { & .react-datepicker__input-container input {
padding-bottom: 4px; padding-bottom: 4px;
padding-top: 4px; padding-top: 4px;
width: 100%; width: 100%;
} }
& .react-period-select__indicators {
display: none;
}
& .react-period {
width: 100%;
max-width: 86px;
}
& .react-period-select__single-value {
color: #c2c6dc;
margin-left: 0;
margin-right: 0;
}
& .react-period-select__value-container {
padding-left: 0;
padding-right: 0;
}
& .react-period-select__control {
border: 1px solid rgba(0, 0, 0, 0.2);
min-height: 30px;
border-color: rgb(65, 69, 97);
background: #262c49;
box-shadow: 0 0 0 0 rgb(0 0 0 / 15%);
color: #c2c6dc;
padding-right: 12px;
padding-left: 12px;
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
font-size: 13px;
line-height: 20px;
padding: 0 12px;
}
`; `;
export const ActionClock = styled(Clock)` export const ActionClock = styled(Clock)`
align-self: center; align-self: center;
fill: ${(props) => props.theme.colors.primary}; fill: ${props => props.theme.colors.primary};
margin: 0 8px;
flex: 0 0 auto;
`;
export const ActionBell = styled(Bell)`
align-self: center;
fill: ${(props) => props.theme.colors.primary};
margin: 0 8px; margin: 0 8px;
flex: 0 0 auto; flex: 0 0 auto;
`; `;
@ -265,7 +222,7 @@ export const ActionLabel = styled.div`
line-height: 14px; line-height: 14px;
`; `;
export const ActionIcon = styled.div<{ disabled?: boolean }>` export const ActionIcon = styled.div`
height: 36px; height: 36px;
min-height: 36px; min-height: 36px;
min-width: 36px; min-width: 36px;
@ -275,25 +232,17 @@ export const ActionIcon = styled.div<{ disabled?: boolean }>`
cursor: pointer; cursor: pointer;
margin-right: 8px; margin-right: 8px;
svg { svg {
fill: ${(props) => props.theme.colors.text.primary}; fill: ${props => props.theme.colors.text.primary};
transition-duration: 0.2s; transition-duration: 0.2s;
transition-property: background, border, box-shadow, fill; transition-property: background, border, box-shadow, fill;
} }
&:hover svg { &:hover svg {
fill: ${(props) => props.theme.colors.text.secondary}; fill: ${props => props.theme.colors.text.secondary};
} }
${(props) =>
props.disabled &&
css`
opacity: 0.8;
cursor: not-allowed;
`}
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
position: relative;
`; `;
export const ClearButton = styled.div` export const ClearButton = styled.div`
@ -311,38 +260,8 @@ export const ClearButton = styled.div`
justify-content: center; justify-content: center;
transition-duration: 0.2s; transition-duration: 0.2s;
transition-property: background, border, box-shadow, color, fill; transition-property: background, border, box-shadow, color, fill;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
&:hover { &:hover {
color: ${(props) => props.theme.colors.text.secondary}; color: ${props => props.theme.colors.text.secondary};
} }
`; `;
export const ControlWrapper = styled.div`
display: flex;
align-items: center;
margin-top: 8px;
`;
export const RightWrapper = styled.div`
flex: 1 1 50%;
display: flex;
align-items: center;
flex-direction: row-reverse;
`;
export const LeftWrapper = styled.div`
flex: 1 1 50%;
display: flex;
align-items: center;
`;
export const SaveButton = styled(Button)`
padding: 6px 12px;
justify-content: center;
margin-right: 4px;
`;
export const RemoveButton = styled.div`
width: 100%;
justify-content: center;
`;

View File

@ -3,21 +3,16 @@ import dayjs from 'dayjs';
import styled from 'styled-components'; import styled from 'styled-components';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import _ from 'lodash'; import _ from 'lodash';
import { colourStyles } from 'shared/components/Select';
import produce from 'immer';
import Select from 'react-select';
import 'react-datepicker/dist/react-datepicker.css'; 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 { Bell, Clock, Cross, Plus, Trash } from 'shared/icons'; import { Clock, Cross } from 'shared/icons';
import Select from 'react-select/src/Select';
import { import {
Wrapper, Wrapper,
RemoveDueDate, RemoveDueDate,
SaveButton,
RightWrapper,
LeftWrapper,
DueDateInput, DueDateInput,
DueDatePickerWrapper, DueDatePickerWrapper,
ConfirmAddDueDate, ConfirmAddDueDate,
@ -29,19 +24,11 @@ import {
ActionsSeparator, ActionsSeparator,
ActionClock, ActionClock,
ActionLabel, ActionLabel,
ControlWrapper,
RemoveButton,
ActionBell,
} from './Styles'; } from './Styles';
type DueDateManagerProps = { type DueDateManagerProps = {
task: Task; task: Task;
onDueDateChange: ( onDueDateChange: (task: Task, newDueDate: Date, hasTime: boolean) => void;
task: Task,
newDueDate: Date,
hasTime: boolean,
notifications: { current: Array<NotificationInternal>; removed: Array<string> },
) => void;
onRemoveDueDate: (task: Task) => void; onRemoveDueDate: (task: Task) => void;
onCancel: () => void; onCancel: () => void;
}; };
@ -54,39 +41,6 @@ const FormField = styled.div`
width: 50%; width: 50%;
display: inline-block; display: inline-block;
`; `;
const NotificationCount = styled.input``;
const ActionPlus = styled(Plus)`
position: absolute;
fill: ${(props) => props.theme.colors.bg.primary} !important;
stroke: ${(props) => props.theme.colors.bg.primary};
`;
const ActionInput = styled.input`
border: 1px solid rgba(0, 0, 0, 0.2);
margin-left: auto;
margin-right: 4px;
border-color: rgb(65, 69, 97);
background: #262c49;
box-shadow: 0 0 0 0 rgb(0 0 0 / 15%);
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
font-size: 13px;
line-height: 20px;
padding: 0 12px;
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
max-width: 48px;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
`;
const HeaderSelectLabel = styled.div` const HeaderSelectLabel = styled.div`
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -177,69 +131,8 @@ const HeaderActions = styled.div`
} }
`; `;
const notificationPeriodOptions = [
{ value: 'minute', label: 'Minutes' },
{ value: 'hour', label: 'Hours' },
{ value: 'day', label: 'Days' },
{ value: 'week', label: 'Weeks' },
];
type NotificationInternal = {
internalId: string;
externalId: string | null;
period: number;
duration: { value: string; label: string };
};
type NotificationEntryProps = {
notification: NotificationInternal;
onChange: (period: number, duration: { value: string; label: string }) => void;
onRemove: () => void;
};
const NotificationEntry: React.FC<NotificationEntryProps> = ({ notification, onChange, onRemove }) => {
return (
<>
<ActionBell width={16} height={16} />
<ActionLabel>Notification</ActionLabel>
<ActionInput
value={notification.period}
onChange={(e) => {
onChange(parseInt(e.currentTarget.value, 10), notification.duration);
}}
onKeyPress={(e) => {
const isNumber = /^[0-9]$/i.test(e.key);
if (!isNumber && e.key !== 'Backspace') {
e.preventDefault();
}
}}
dir="ltr"
autoComplete="off"
min="0"
type="number"
/>
<Select
menuPlacement="top"
className="react-period"
classNamePrefix="react-period-select"
styles={colourStyles}
isSearchable={false}
defaultValue={notification.duration}
options={notificationPeriodOptions}
onChange={(e) => {
if (e !== null) {
onChange(notification.period, e);
}
}}
/>
<ActionIcon onClick={() => onRemove()}>
<Cross width={16} height={16} />
</ActionIcon>
</>
);
};
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => { const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const currentDueDate = task.dueDate.at ? dayjs(task.dueDate.at).toDate() : null; const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null;
const { const {
register, register,
handleSubmit, handleSubmit,
@ -252,7 +145,28 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
const [startDate, setStartDate] = useState<Date | null>(currentDueDate); const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
const [endDate, setEndDate] = useState<Date | null>(currentDueDate); const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
const [hasTime, enableTime] = useState(task.hasTime ?? false); const [hasTime, enableTime] = useState(task.hasTime ?? false);
const firstRun = useRef<boolean>(true);
const debouncedFunctionRef = useRef((newDate: Date | null, nowHasTime: boolean) => {
if (!firstRun.current) {
if (newDate) {
onDueDateChange(task, newDate, nowHasTime);
} else {
onRemoveDueDate(task);
enableTime(false);
}
} else {
firstRun.current = false;
}
});
const debouncedChange = useCallback(
_.debounce((newDate, nowHasTime) => debouncedFunctionRef.current(newDate, nowHasTime), 500),
[],
);
useEffect(() => {
debouncedChange(startDate, hasTime);
}, [startDate, hasTime]);
const years = _.range(2010, getYear(new Date()) + 10, 1); const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [ const months = [
'January', 'January',
@ -269,41 +183,33 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'December', 'December',
]; ];
const [isRange, setIsRange] = useState(false); const onChange = (dates: any) => {
const [notDuration, setNotDuration] = useState(10); const [start, end] = dates;
const [removedNotifications, setRemovedNotifications] = useState<Array<string>>([]); setStartDate(start);
const [notifications, setNotifications] = useState<Array<NotificationInternal>>( setEndDate(end);
task.dueDate.notifications
? task.dueDate.notifications.map((c, idx) => {
const duration =
notificationPeriodOptions.find((o) => o.value === c.duration.toLowerCase()) ?? notificationPeriodOptions[0];
return {
internalId: `n${idx}`,
externalId: c.id,
period: c.period,
duration,
}; };
}) const [isRange, setIsRange] = useState(false);
: [],
);
return ( return (
<Wrapper> <Wrapper>
<DateRangeInputs> <DateRangeInputs>
<DatePicker <DatePicker
selected={startDate} selected={startDate}
onChange={(date) => { onChange={(date) => {
if (!Array.isArray(date) && date !== null) { if (!Array.isArray(date)) {
setStartDate(date); setStartDate(date);
} }
}} }}
popperClassName="picker-hidden" popperClassName="picker-hidden"
dateFormat="yyyy-MM-dd" dateFormat="yyyy-MM-dd"
disabledKeyboardNavigation disabledKeyboardNavigation
isClearable
placeholderText="Select due date" placeholderText="Select due date"
/> />
{isRange ? ( {isRange ? (
<DatePicker <DatePicker
selected={startDate} selected={startDate}
isClearable
onChange={(date) => { onChange={(date) => {
if (!Array.isArray(date)) { if (!Array.isArray(date)) {
setStartDate(date); setStartDate(date);
@ -393,75 +299,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
</ActionIcon> </ActionIcon>
</ActionsWrapper> </ActionsWrapper>
)} )}
{notifications.map((n, idx) => ( <ActionsWrapper>
<ActionsWrapper key={n.internalId}>
<NotificationEntry
notification={n}
onChange={(period, duration) => {
setNotifications((prev) =>
produce(prev, (draft) => {
draft[idx].duration = duration;
draft[idx].period = period;
}),
);
}}
onRemove={() => {
setNotifications((prev) =>
produce(prev, (draft) => {
draft.splice(idx, 1);
if (n.externalId !== null) {
setRemovedNotifications((prev) => {
if (n.externalId !== null) {
return [...prev, n.externalId];
}
return prev;
});
}
}),
);
}}
/>
</ActionsWrapper>
))}
<ControlWrapper>
<LeftWrapper>
<SaveButton
onClick={() => {
if (startDate && notifications.findIndex((n) => Number.isNaN(n.period)) === -1) {
onDueDateChange(task, startDate, hasTime, { current: notifications, removed: removedNotifications });
}
}}
>
Save
</SaveButton>
{currentDueDate !== null && (
<ActionIcon
onClick={() => {
onRemoveDueDate(task);
}}
>
<Trash width={16} height={16} />
</ActionIcon>
)}
</LeftWrapper>
<RightWrapper>
<ActionIcon
disabled={notifications.length === 3}
onClick={() => {
setNotifications((prev) => [
...prev,
{
externalId: null,
internalId: `n${prev.length + 1}`,
duration: notificationPeriodOptions[0],
period: 10,
},
]);
}}
>
<Bell width={16} height={16} />
<ActionPlus width={8} height={8} />
</ActionIcon>
{!hasTime && ( {!hasTime && (
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
@ -476,8 +314,8 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
<Clock width={16} height={16} /> <Clock width={16} height={16} />
</ActionIcon> </ActionIcon>
)} )}
</RightWrapper> <ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton>
</ControlWrapper> </ActionsWrapper>
</Wrapper> </Wrapper>
); );
}; };

View File

@ -1,202 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import styled, { css } from 'styled-components/macro';
import theme from '../../../App/ThemeStyles';
const InputWrapper = styled.div<{ width: string }>`
position: relative;
width: ${(props) => props.width};
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
justify-content: center;
margin-bottom: 2.2rem;
margin-top: 24px;
`;
const InputLabel = styled.span<{ width: string }>`
width: ${(props) => props.width};
padding: 0.7rem !important;
color: #c2c6dc;
left: 0;
top: 0;
transition: all 0.2s ease;
position: absolute;
border-radius: 5px;
overflow: hidden;
font-size: 0.85rem;
cursor: text;
font-size: 12px;
user-select: none;
pointer-events: none;
}
`;
const InputInput = styled.input<{
hasValue: boolean;
hasIcon: boolean;
width: string;
focusBg: string;
borderColor: string;
}>`
width: ${(props) => props.width};
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-color: ${(props) => props.borderColor};
background: #262c49;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
${(props) => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
&:-webkit-autofill, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #262c49 inset !important;
}
&:-webkit-autofill {
-webkit-text-fill-color: #c2c6dc !important;
}
line-height: 16px;
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
&:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid ${(props) => props.theme.colors.primary};
background: ${(props) => props.focusBg};
}
&:focus ~ ${InputLabel} {
color: ${(props) => props.theme.colors.primary};
transform: translate(-3px, -90%);
}
${(props) =>
props.hasValue &&
css`
& ~ ${InputLabel} {
color: ${props.theme.colors.primary};
transform: translate(-3px, -90%);
}
`}
`;
const Icon = styled.div`
display: flex;
left: 16px;
position: absolute;
`;
type FormInputProps = {
variant?: 'normal' | 'alternate';
disabled?: boolean;
label?: string;
width?: string;
floatingLabel?: boolean;
placeholder?: string;
icon?: JSX.Element;
type?: string;
autocomplete?: boolean;
autoFocus?: boolean;
autoSelect?: boolean;
id?: string;
name?: string;
onChange: any;
onBlur: any;
className?: string;
defaultValue?: string;
value?: string;
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
};
function useCombinedRefs(...refs: any) {
const targetRef = React.useRef();
React.useEffect(() => {
refs.forEach((ref: any) => {
if (!ref) return;
if (typeof ref === 'function') {
ref(targetRef.current);
} else {
ref.current = targetRef.current;
}
});
}, [refs]);
return targetRef;
}
const FormInput = React.forwardRef(
(
{
disabled = false,
width = 'auto',
variant = 'normal',
type = 'text',
autoFocus = false,
autoSelect = false,
autocomplete,
label,
placeholder,
onBlur,
onChange,
icon,
name,
className,
onClick,
floatingLabel,
defaultValue,
value,
id,
}: FormInputProps,
$ref: any,
) => {
const [hasValue, setHasValue] = useState(defaultValue !== '');
const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
// Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
// The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
// TODO(jordanknott): This is super ugly, find a better approach?
const $innerRef = React.useRef<HTMLInputElement>(null);
const combinedRef: any = useCombinedRefs($ref, $innerRef);
useEffect(() => {
if (combinedRef && combinedRef.current) {
if (autoFocus) {
combinedRef.current.focus();
}
if (autoSelect) {
combinedRef.current.select();
}
}
}, []);
return (
<InputWrapper className={className} width={width}>
<InputInput
onChange={(e) => {
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
onChange(e);
}}
disabled={disabled}
hasValue={hasValue}
ref={combinedRef}
id={id}
type={type}
name={name}
onClick={onClick}
autoComplete={autocomplete ? 'on' : 'off'}
defaultValue={defaultValue}
onBlur={onBlur}
value={value}
hasIcon={typeof icon !== 'undefined'}
width={width}
placeholder={placeholder}
focusBg={focusBg}
borderColor={borderColor}
/>
{label && <InputLabel width={width}>{label}</InputLabel>}
<Icon>{icon && icon}</Icon>
</InputWrapper>
);
},
);
export default FormInput;

View File

@ -4,7 +4,6 @@ import List, { ListCards } from 'shared/components/List';
import Card from 'shared/components/Card'; import Card from 'shared/components/Card';
import CardComposer from 'shared/components/CardComposer'; import CardComposer from 'shared/components/CardComposer';
import AddList from 'shared/components/AddList'; import AddList from 'shared/components/AddList';
import log from 'loglevel';
import { import {
isPositionChanged, isPositionChanged,
getSortedDraggables, getSortedDraggables,
@ -112,16 +111,24 @@ function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
const TODAY = REFERENCE.clone().startOf('day'); const TODAY = REFERENCE.clone().startOf('day');
return completedAt.isSame(TODAY, 'd'); return completedAt.isSame(TODAY, 'd');
case TaskSince.YESTERDAY: case TaskSince.YESTERDAY:
const YESTERDAY = REFERENCE.clone().subtract(1, 'day').startOf('day'); const YESTERDAY = REFERENCE.clone()
.subtract(1, 'day')
.startOf('day');
return completedAt.isSameOrAfter(YESTERDAY, 'd'); return completedAt.isSameOrAfter(YESTERDAY, 'd');
case TaskSince.ONE_WEEK: case TaskSince.ONE_WEEK:
const ONE_WEEK = REFERENCE.clone().subtract(7, 'day').startOf('day'); const ONE_WEEK = REFERENCE.clone()
.subtract(7, 'day')
.startOf('day');
return completedAt.isSameOrAfter(ONE_WEEK, 'd'); return completedAt.isSameOrAfter(ONE_WEEK, 'd');
case TaskSince.TWO_WEEKS: case TaskSince.TWO_WEEKS:
const TWO_WEEKS = REFERENCE.clone().subtract(14, 'day').startOf('day'); const TWO_WEEKS = REFERENCE.clone()
.subtract(14, 'day')
.startOf('day');
return completedAt.isSameOrAfter(TWO_WEEKS, 'd'); return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
case TaskSince.THREE_WEEKS: case TaskSince.THREE_WEEKS:
const THREE_WEEKS = REFERENCE.clone().subtract(21, 'day').startOf('day'); const THREE_WEEKS = REFERENCE.clone()
.subtract(21, 'day')
.startOf('day');
return completedAt.isSameOrAfter(THREE_WEEKS, 'd'); return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
default: default:
return true; return true;
@ -196,14 +203,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
let beforeDropDraggables: Array<DraggableElement> | null = null; let beforeDropDraggables: Array<DraggableElement> | null = null;
if (isList) { if (isList) {
const droppedGroup = taskGroups.find((taskGroup) => taskGroup.id === draggableId); const droppedGroup = taskGroups.find(taskGroup => taskGroup.id === draggableId);
if (droppedGroup) { if (droppedGroup) {
droppedDraggable = { droppedDraggable = {
id: draggableId, id: draggableId,
position: droppedGroup.position, position: droppedGroup.position,
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
taskGroups.map((taskGroup) => { taskGroups.map(taskGroup => {
return { id: taskGroup.id, position: taskGroup.position }; return { id: taskGroup.id, position: taskGroup.position };
}), }),
); );
@ -227,13 +234,13 @@ const SimpleLists: React.FC<SimpleProps> = ({
} }
} else { } else {
const curTaskGroup = taskGroups.findIndex( const curTaskGroup = taskGroups.findIndex(
(taskGroup) => taskGroup.tasks.findIndex((task) => task.id === draggableId) !== -1, taskGroup => taskGroup.tasks.findIndex(task => task.id === draggableId) !== -1,
); );
let targetTaskGroup = curTaskGroup; let targetTaskGroup = curTaskGroup;
if (!isSameList) { if (!isSameList) {
targetTaskGroup = taskGroups.findIndex((taskGroup) => taskGroup.id === destination.droppableId); targetTaskGroup = taskGroups.findIndex(taskGroup => taskGroup.id === destination.droppableId);
} }
const droppedTask = taskGroups[curTaskGroup].tasks.find((task) => task.id === draggableId); const droppedTask = taskGroups[curTaskGroup].tasks.find(task => task.id === draggableId);
if (droppedTask) { if (droppedTask) {
droppedDraggable = { droppedDraggable = {
@ -241,7 +248,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
position: droppedTask.position, position: droppedTask.position,
}; };
beforeDropDraggables = getSortedDraggables( beforeDropDraggables = getSortedDraggables(
taskGroups[targetTaskGroup].tasks.map((task) => { taskGroups[targetTaskGroup].tasks.map(task => {
return { id: task.id, position: task.position }; return { id: task.id, position: task.position };
}), }),
); );
@ -263,9 +270,6 @@ const SimpleLists: React.FC<SimpleProps> = ({
id: destination.droppableId, id: destination.droppableId,
}, },
}; };
log.debug(
`action=move taskId=${droppedTask.id} source=${source.droppableId} dest=${destination.droppableId} oldPos=${droppedTask.position} newPos=${newPosition}`,
);
onTaskDrop(newTask, droppedTask.taskGroup.id); onTaskDrop(newTask, droppedTask.taskGroup.id);
} }
} }
@ -282,7 +286,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
<BoardWrapper> <BoardWrapper>
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="horizontal" type="column" droppableId="root"> <Droppable direction="horizontal" type="column" droppableId="root">
{(provided) => ( {provided => (
<Container {...provided.droppableProps} ref={provided.innerRef}> <Container {...provided.droppableProps} ref={provided.innerRef}>
{taskGroups {taskGroups
.slice() .slice()
@ -290,14 +294,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
.map((taskGroup: TaskGroup, index: number) => { .map((taskGroup: TaskGroup, index: number) => {
return ( return (
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}> <Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
{(columnDragProvided) => ( {columnDragProvided => (
<Droppable type="tasks" droppableId={taskGroup.id}> <Droppable type="tasks" droppableId={taskGroup.id}>
{(columnDropProvided, snapshot) => ( {(columnDropProvided, snapshot) => (
<List <List
name={taskGroup.name} name={taskGroup.name}
onOpenComposer={(id) => setCurrentComposer(id)} onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id} isComposerOpen={currentComposer === taskGroup.id}
onSaveName={(name) => onChangeTaskGroupName(taskGroup.id, name)} onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
isPublic={isPublic} isPublic={isPublic}
ref={columnDragProvided.innerRef} ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps} wrapperProps={columnDragProvided.draggableProps}
@ -310,8 +314,8 @@ const SimpleLists: React.FC<SimpleProps> = ({
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}> <ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{taskGroup.tasks {taskGroup.tasks
.slice() .slice()
.filter((t) => shouldStatusFilter(t, taskStatusFilter)) .filter(t => shouldStatusFilter(t, taskStatusFilter))
.filter((t) => shouldMetaFilter(t, taskMetaFilters)) .filter(t => shouldMetaFilter(t, taskMetaFilters))
.sort((a: any, b: any) => a.position - b.position) .sort((a: any, b: any) => a.position - b.position)
.sort((a: any, b: any) => sortTasks(a, b, taskSorting)) .sort((a: any, b: any) => sortTasks(a, b, taskSorting))
.map((task: Task, taskIndex: any) => { .map((task: Task, taskIndex: any) => {
@ -322,14 +326,13 @@ const SimpleLists: React.FC<SimpleProps> = ({
index={taskIndex} index={taskIndex}
isDragDisabled={taskSorting.type !== TaskSortingType.NONE} isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
> >
{(taskProvided) => { {taskProvided => {
return ( return (
<Card <Card
toggleDirection={toggleDirection} toggleDirection={toggleDirection}
toggleLabels={toggleLabels} toggleLabels={toggleLabels}
isPublic={isPublic} isPublic={isPublic}
labelVariant={cardLabelVariant} labelVariant={cardLabelVariant}
watched={task.watched}
wrapperProps={{ wrapperProps={{
...taskProvided.draggableProps, ...taskProvided.draggableProps,
...taskProvided.dragHandleProps, ...taskProvided.dragHandleProps,
@ -349,12 +352,12 @@ const SimpleLists: React.FC<SimpleProps> = ({
complete={task.complete ?? false} complete={task.complete ?? false}
taskGroupID={taskGroup.id} taskGroupID={taskGroup.id}
description="" description=""
labels={task.labels.map((label) => label.projectLabel)} labels={task.labels.map(label => label.projectLabel)}
dueDate={ dueDate={
task.dueDate.at task.dueDate
? { ? {
isPastDue: false, isPastDue: false,
formattedDate: dayjs(task.dueDate.at).format('MMM D, YYYY'), formattedDate: dayjs(task.dueDate).format('MMM D, YYYY'),
} }
: undefined : undefined
} }
@ -364,7 +367,6 @@ const SimpleLists: React.FC<SimpleProps> = ({
onTaskClick(task); onTaskClick(task);
}} }}
checklists={task.badges && task.badges.checklist} checklists={task.badges && task.badges.checklist}
comments={task.badges && task.badges.comments}
onCardMemberClick={onCardMemberClick} onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen} onContextMenu={onQuickEditorOpen}
/> />
@ -379,7 +381,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
onClose={() => { onClose={() => {
setCurrentComposer(''); setCurrentComposer('');
}} }}
onCreateCard={(name) => { onCreateCard={name => {
onCreateTask(taskGroup.id, name); onCreateTask(taskGroup.id, name);
}} }}
isOpen isOpen
@ -400,7 +402,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
</DragDropContext> </DragDropContext>
{!isPublic && ( {!isPublic && (
<AddList <AddList
onSave={(listName) => { onSave={listName => {
onCreateTaskGroup(listName); onCreateTaskGroup(listName);
}} }}
/> />

View File

@ -24,7 +24,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null)); isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
} }
if (task.dueDate) { if (task.dueDate) {
const taskDueDate = dayjs(task.dueDate.at); const taskDueDate = dayjs(task.dueDate);
const today = dayjs(); const today = dayjs();
let start; let start;
let end; let end;
@ -36,31 +36,61 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
isFiltered = shouldFilter(taskDueDate.isSame(today, 'day')); isFiltered = shouldFilter(taskDueDate.isSame(today, 'day'));
break; break;
case DueDateFilterType.TOMORROW: case DueDateFilterType.TOMORROW:
isFiltered = shouldFilter(taskDueDate.isBefore(today.clone().add(1, 'day').endOf('day'))); isFiltered = shouldFilter(
taskDueDate.isBefore(
today
.clone()
.add(1, 'day')
.endOf('day'),
),
);
break; break;
case DueDateFilterType.THIS_WEEK: case DueDateFilterType.THIS_WEEK:
start = today.clone().weekday(0).startOf('day'); start = today
end = today.clone().weekday(6).endOf('day'); .clone()
.weekday(0)
.startOf('day');
end = today
.clone()
.weekday(6)
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.NEXT_WEEK: case DueDateFilterType.NEXT_WEEK:
start = today.clone().weekday(0).add(7, 'day').startOf('day'); start = today
end = today.clone().weekday(6).add(7, 'day').endOf('day'); .clone()
.weekday(0)
.add(7, 'day')
.startOf('day');
end = today
.clone()
.weekday(6)
.add(7, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.ONE_WEEK: case DueDateFilterType.ONE_WEEK:
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today.clone().add(7, 'day').endOf('day'); end = today
.clone()
.add(7, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.TWO_WEEKS: case DueDateFilterType.TWO_WEEKS:
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today.clone().add(14, 'day').endOf('day'); end = today
.clone()
.add(14, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
case DueDateFilterType.THREE_WEEKS: case DueDateFilterType.THREE_WEEKS:
start = today.clone().startOf('day'); start = today.clone().startOf('day');
end = today.clone().add(21, 'day').endOf('day'); end = today
.clone()
.add(21, 'day')
.endOf('day');
isFiltered = shouldFilter(taskDueDate.isBetween(start, end)); isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
break; break;
default: default:
@ -74,7 +104,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
} }
for (const member of filters.members) { for (const member of filters.members) {
if (task.assigned) { if (task.assigned) {
if (task.assigned.findIndex((m) => m.id === member.id) !== -1) { if (task.assigned.findIndex(m => m.id === member.id) !== -1) {
isFiltered = ShouldFilter.VALID; isFiltered = ShouldFilter.VALID;
} }
} }
@ -86,7 +116,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
} }
for (const label of filters.labels) { for (const label of filters.labels) {
if (task.labels) { if (task.labels) {
if (task.labels.findIndex((m) => m.projectLabel.id === label.id) !== -1) { if (task.labels.findIndex(m => m.projectLabel.id === label.id) !== -1) {
isFiltered = ShouldFilter.VALID; isFiltered = ShouldFilter.VALID;
} }
} }

View File

@ -1,7 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import AccessAccount from 'shared/undraw/AccessAccount';
export const Wrapper = styled.div` export const Wrapper = styled.div`
background: #eff2f7; background: #eff2f7;
@ -16,12 +15,6 @@ export const Column = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@media (max-width: 600px) {
svg {
display: none;
}
position: absolute;
}
`; `;
export const LoginFormWrapper = styled.div` export const LoginFormWrapper = styled.div`
@ -32,47 +25,18 @@ export const LoginFormWrapper = styled.div`
export const LoginFormContainer = styled.div` export const LoginFormContainer = styled.div`
min-height: 505px; min-height: 505px;
padding: 2rem; padding: 2rem;
@media (max-width: 600px) {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
border: solid black 1px;
width: 600px;
height: 1100px;
box-shadow: 20px 20px 50px black;
}
@media (min-height: 641px) and (max-height: 653px) {
margin-top: 25%;
}
@media (min-height: 654px) and (max-height: 823px) and (max-width: 500px) {
margin-top: -20%;
}
@media (min-height: 480px) and (max-height: 639px) {
margin-top: 20%;
}
`; `;
export const Title = styled.h1` export const Title = styled.h1`
color: #ebeefd; color: #ebeefd;
font-size: 18px; font-size: 18px;
margin-bottom: 14px; margin-bottom: 14px;
@media (max-width: 600px) {
font-size: 38px;
margin-top: 50px;
text-align: center;
}
`; `;
export const SubTitle = styled.h2` export const SubTitle = styled.h2`
color: #c2c6dc; color: #c2c6dc;
font-size: 14px; font-size: 14px;
margin-bottom: 14px; margin-bottom: 14px;
@media (max-width: 600px) {
margin-top: 30px;
font-size: 24.5px;
margin-bottom: 90px;
text-align: center;
}
`; `;
export const Form = styled.form` export const Form = styled.form`
display: flex; display: flex;
@ -84,10 +48,6 @@ export const FormLabel = styled.label`
font-size: 12px; font-size: 12px;
position: relative; position: relative;
margin-top: 14px; margin-top: 14px;
@media (max-width: 600px) {
font-size: 35px;
font-weight: bold;
}
`; `;
export const FormTextInput = styled.input` export const FormTextInput = styled.input`
@ -99,92 +59,28 @@ export const FormTextInput = styled.input`
font-size: 1rem; font-size: 1rem;
color: #c2c6dc; color: #c2c6dc;
border-radius: 5px; border-radius: 5px;
@media (max-width: 600px) {
border: 5px solid rgba(0, 0, 0, 0.2);
border-radius: 5%;
font-size: 30px;
background-color: #353D64;
color: black;
padding: 0.7rem 1rem 1rem 3rem;
text-align: center;
&::placeholder {
visibility: hidden;
}
&:not(:placeholder-shown) {
background-color: white;
}
}
`; `;
export const FormIcon = styled.div` export const FormIcon = styled.div`
top: 30px; top: 30px;
left: 16px; left: 16px;
position: absolute; position: absolute;
@media (max-width: 600px) {
svg {
width: 40px;
height: 40px;
display: inline;
position: absolute;
top: 30px;
left: -5px;
}
}
`; `;
export const FormError = styled.span` export const FormError = styled.span`
font-size: 0.875rem; font-size: 0.875rem;
color: ${(props) => props.theme.colors.danger}; color: ${props => props.theme.colors.danger};
@media (max-width: 600px) {
font-size: 1.8rem;
}
`; `;
export const LoginButton = styled(Button)` export const LoginButton = styled(Button)``;
@media (max-width: 600px) {
span {
font-size: 40px;
text-align: center;
width: 100%;
}
align-self: center;
position: absolute;
right: 0px;
margin-top: 40%;
width: 100%;
&:hover {
box-shadow: 5px 5px 20px white;
}
}
`;
export const ActionButtons = styled.div` export const ActionButtons = styled.div`
margin-top: 17.5px; margin-top: 17.5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@media (max-width: 600px) {
width: 150px;
align-content: center;
font-size: 50px;
}
`; `;
export const RegisterButton = styled(Button)` export const RegisterButton = styled(Button)``;
@media (max-width: 600px) {
span {
font-size: 40px;
text-align: center;
width: 100%;
}
width: 100%;
position: absolute;
left: 0px;
margin-top: 29%;
}
`;
export const LogoTitle = styled.div` export const LogoTitle = styled.div`
font-size: 24px; font-size: 24px;
@ -192,9 +88,6 @@ export const LogoTitle = styled.div`
margin-left: 12px; margin-left: 12px;
transition: visibility, opacity, transform 0.25s ease; transition: visibility, opacity, transform 0.25s ease;
color: #7367f0; color: #7367f0;
@media (max-width: 600px) {
font-size: 60px;
}
`; `;
export const LogoWrapper = styled.div` export const LogoWrapper = styled.div`
@ -207,16 +100,5 @@ export const LogoWrapper = styled.div`
padding-bottom: 16px; padding-bottom: 16px;
margin-bottom: 24px; margin-bottom: 24px;
color: rgb(222, 235, 255); color: rgb(222, 235, 255);
border-bottom: 1px solid ${(props) => mixin.rgba(props.theme.colors.alternate, 0.65)}; border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
@media (max-width: 600px) {
svg {
display: inline;
width: 80px;
height: 80px;
}
}
`; `;

View File

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import AccessAccount from 'shared/undraw/AccessAccount'; import AccessAccount from 'shared/undraw/AccessAccount';
import { User, Lock, Taskcafe } from 'shared/icons'; import { User, Lock, Taskcafe } from 'shared/icons';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router';
import LoadingSpinner from 'shared/components/LoadingSpinner'; import LoadingSpinner from 'shared/components/LoadingSpinner';
import { import {
Form, Form,
@ -25,7 +25,6 @@ import {
const Login = ({ onSubmit }: LoginProps) => { const Login = ({ onSubmit }: LoginProps) => {
const [isComplete, setComplete] = useState(true); const [isComplete, setComplete] = useState(true);
const [showRegistration, setShowRegistration] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit,
@ -36,17 +35,6 @@ const Login = ({ onSubmit }: LoginProps) => {
setComplete(false); setComplete(false);
onSubmit(data, setComplete, setError); onSubmit(data, setComplete, setError);
}; };
const history = useHistory();
useEffect(() => {
fetch('/settings').then(async (x) => {
const { isConfigured, allowPublicRegistration } = await x.json();
if (!isConfigured) {
history.push('/register');
} else if (allowPublicRegistration) {
setShowRegistration(true);
}
});
}, []);
return ( return (
<Wrapper> <Wrapper>
<Column> <Column>
@ -64,11 +52,7 @@ const Login = ({ onSubmit }: LoginProps) => {
<Form onSubmit={handleSubmit(loginSubmit)}> <Form onSubmit={handleSubmit(loginSubmit)}>
<FormLabel htmlFor="username"> <FormLabel htmlFor="username">
Username Username
<FormTextInput <FormTextInput type="text" {...register('username', { required: 'Username is required' })} />
placeholder="Username"
type="text"
{...register('username', { required: 'Username is required' })}
/>
<FormIcon> <FormIcon>
<User width={20} height={20} /> <User width={20} height={20} />
</FormIcon> </FormIcon>
@ -76,11 +60,7 @@ const Login = ({ onSubmit }: LoginProps) => {
{errors.username && <FormError>{errors.username.message}</FormError>} {errors.username && <FormError>{errors.username.message}</FormError>}
<FormLabel htmlFor="password"> <FormLabel htmlFor="password">
Password Password
<FormTextInput <FormTextInput type="password" {...register('password', { required: 'Password is required' })} />
placeholder="Password"
type="password"
{...register('password', { required: 'Password is required' })}
/>
<FormIcon> <FormIcon>
<Lock width={20} height={20} /> <Lock width={20} height={20} />
</FormIcon> </FormIcon>
@ -88,7 +68,7 @@ const Login = ({ onSubmit }: LoginProps) => {
{errors.password && <FormError>{errors.password.message}</FormError>} {errors.password && <FormError>{errors.password.message}</FormError>}
<ActionButtons> <ActionButtons>
{showRegistration ? <RegisterButton variant="outline">Register</RegisterButton> : <div />} <RegisterButton variant="outline">Register</RegisterButton>
{!isComplete && <LoadingSpinner size="32px" thickness="2px" borderSize="48px" />} {!isComplete && <LoadingSpinner size="32px" thickness="2px" borderSize="48px" />}
<LoginButton type="submit" disabled={!isComplete}> <LoginButton type="submit" disabled={!isComplete}>
Login Login

View File

@ -1,37 +1,8 @@
import React, { useRef, useState } from 'react'; import React from 'react';
import styled, { css } from 'styled-components'; import styled from 'styled-components';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import { Link } from 'react-router-dom';
import { mixin } from 'shared/utils/styles';
import {
useNotificationMarkAllReadMutation,
useNotificationsQuery,
NotificationFilter,
ActionType,
useNotificationAddedSubscription,
useNotificationToggleReadMutation,
} from 'shared/generated/graphql';
import dayjs from 'dayjs';
import { Popup, usePopup } from 'shared/components/PopupMenu'; import { Popup } from 'shared/components/PopupMenu';
import { Bell, CheckCircleOutline, Circle, Ellipsis, UserCircle } from 'shared/icons';
import produce from 'immer';
import { useLocalStorage } from 'shared/hooks/useStateWithLocalStorage';
import localStorage from 'shared/utils/localStorage';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
function getFilterMessage(filter: NotificationFilter) {
switch (filter) {
case NotificationFilter.Unread:
return 'no unread';
case NotificationFilter.Assigned:
return 'no assigned';
case NotificationFilter.Mentioned:
return 'no mentioned';
default:
return 'no';
}
}
const ItemWrapper = styled.div` const ItemWrapper = styled.div`
cursor: pointer; cursor: pointer;
@ -66,7 +37,7 @@ const ItemTextContainer = styled.div`
const ItemTextTitle = styled.span` const ItemTextTitle = styled.span`
font-weight: 500; font-weight: 500;
display: block; display: block;
color: ${(props) => props.theme.colors.primary}; color: ${props => props.theme.colors.primary};
font-size: 14px; font-size: 14px;
`; `;
const ItemTextDesc = styled.span` const ItemTextDesc = styled.span`
@ -101,578 +72,38 @@ export const NotificationItem: React.FC<NotificationItemProps> = ({ title, descr
}; };
const NotificationHeader = styled.div` const NotificationHeader = styled.div`
padding: 20px 28px; padding: 0.75rem;
text-align: center; text-align: center;
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-top-right-radius: 6px; border-top-right-radius: 6px;
background: ${(props) => props.theme.colors.primary}; background: ${props => props.theme.colors.primary};
`; `;
const NotificationHeaderTitle = styled.span` const NotificationHeaderTitle = styled.span`
font-size: 14px; font-size: 14px;
color: ${(props) => props.theme.colors.text.secondary}; color: ${props => props.theme.colors.text.secondary};
`; `;
const EmptyMessage = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
height: 448px;
`;
const EmptyMessageLabel = styled.span`
margin-bottom: 80px;
`;
const Notifications = styled.div`
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
height: 448px;
overflow-y: scroll;
user-select: none;
`;
const NotificationFooter = styled.div` const NotificationFooter = styled.div`
cursor: pointer; cursor: pointer;
padding: 0.5rem; padding: 0.5rem;
text-align: center; text-align: center;
color: ${(props) => props.theme.colors.primary}; color: ${props => props.theme.colors.primary};
&:hover { &:hover {
background: ${(props) => props.theme.colors.bg.primary}; background: ${props => props.theme.colors.bg.primary};
} }
border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`; `;
const NotificationTabs = styled.div` const NotificationPopup: React.FC = ({ children }) => {
align-items: flex-end;
align-self: stretch;
display: flex;
flex: 1 0 auto;
justify-content: flex-start;
max-width: 100%;
padding-top: 4px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
const NotificationTab = styled.div<{ active: boolean }>`
font-size: 80%;
color: ${(props) => props.theme.colors.text.primary};
font-size: 15px;
cursor: pointer;
display: flex;
user-select: none;
justify-content: center;
line-height: normal;
min-width: 1px;
transition-duration: 0.2s;
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding: 12px 16px;
&:first-child {
margin-left: 12px;
}
&:hover {
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
color: ${(props) => props.theme.colors.text.secondary};
}
&:not(:last-child) {
margin-right: 12px;
}
${(props) =>
props.active &&
css`
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
color: ${props.theme.colors.secondary};
&:hover {
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
color: ${props.theme.colors.secondary};
}
`}
`;
const NotificationLink = styled(Link)`
display: flex;
text-decoration: none;
padding: 16px 8px;
width: 100%;
`;
const NotificationControls = styled.div`
width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
visibility: hidden;
padding: 4px;
`;
const NotificationButtons = styled.div`
display: flex;
align-self: flex-end;
align-items: center;
margin-top: auto;
margin-bottom: 6px;
`;
const NotificationButton = styled.div`
padding: 4px 15px;
cursor: pointer;
&:hover svg {
fill: rgb(216, 93, 216);
stroke: rgb(216, 93, 216);
}
`;
const NotificationWrapper = styled.li<{ read: boolean }>`
min-height: 80px;
display: flex;
font-size: 14px;
transition: background-color 0.1s ease-in-out;
margin: 2px 8px;
border-radius: 8px;
justify-content: space-between;
position: relative;
&:hover {
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)};
}
&:hover ${NotificationLink} {
color: #fff;
}
&:hover ${NotificationControls} {
visibility: visible;
}
${(props) =>
!props.read &&
css`
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)};
&:hover {
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.6)};
}
`}
`;
const NotificationContentFooter = styled.div`
margin-top: 10px;
display: flex;
align-items: center;
color: ${(props) => props.theme.colors.text.primary};
`;
const NotificationCausedBy = styled.div`
height: 48px;
width: 48px;
min-height: 48px;
min-width: 48px;
`;
const NotificationCausedByInitials = styled.div`
position: relative;
display: flex;
align-items: center;
text: #fff;
font-size: 18px;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
height: 100%;
width: 100%;
border: none;
background: #7367f0;
`;
const NotificationCausedByImage = styled.img`
position: relative;
display: flex;
border-radius: 50%;
flex-shrink: 0;
height: 100%;
width: 100%;
border: none;
background: #7367f0;
`;
const NotificationContent = styled.div`
display: flex;
overflow: hidden;
flex-direction: column;
margin-left: 16px;
`;
const NotificationContentHeader = styled.div`
font-weight: bold;
font-size: 14px;
color: #fff;
svg {
margin-left: 8px;
fill: rgb(216, 93, 216);
stroke: rgb(216, 93, 216);
}
`;
const NotificationBody = styled.div`
display: flex;
align-items: center;
color: #fff;
svg {
fill: rgb(216, 93, 216);
stroke: rgb(216, 93, 216);
}
`;
const NotificationPrefix = styled.span`
color: rgb(216, 93, 216);
margin: 0 4px;
`;
const NotificationSeparator = styled.span`
margin: 0 6px;
`;
type NotificationProps = {
causedBy?: { fullname: string; username: string; id: string } | null;
createdAt: string;
read: boolean;
data: Array<{ key: string; value: string }>;
actionType: ActionType;
onToggleRead: () => void;
};
const Notification: React.FC<NotificationProps> = ({ causedBy, createdAt, data, actionType, read, onToggleRead }) => {
const prefix: any = [];
const { hidePopup } = usePopup();
const dataMap = new Map<string, string>();
data.forEach((d) => dataMap.set(d.key, d.value));
let link = '#';
switch (actionType) {
case ActionType.TaskAssigned:
prefix.push(<UserCircle key="profile" width={14} height={16} />);
prefix.push(
<NotificationPrefix key="prefix">
<span style={{ fontWeight: 'bold' }}>{causedBy ? causedBy.fullname : 'Removed user'}</span>
</NotificationPrefix>,
);
prefix.push(<span key="content">assigned you to the task &quote;{dataMap.get('TaskName')}&quote;</span>);
link = `/p/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`;
break;
case ActionType.DueDateReminder:
prefix.push(<Bell key="profile" width={14} height={16} />);
prefix.push(<NotificationPrefix key="prefix">{dataMap.get('TaskName')}</NotificationPrefix>);
const now = dayjs();
if (dayjs(dataMap.get('DueDate')).isBefore(dayjs())) {
prefix.push(
<span key="content">is due {dayjs.duration(now.diff(dayjs(dataMap.get('DueAt')))).humanize(true)}</span>,
);
} else {
prefix.push(
<span key="content">
has passed the due date {dayjs.duration(dayjs(dataMap.get('DueAt')).diff(now)).humanize(true)}
</span>,
);
}
link = `/p/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`;
break;
default:
throw new Error('unknown action type');
}
return (
<NotificationWrapper read={read}>
<NotificationLink to={link} onClick={hidePopup}>
<NotificationCausedBy>
<NotificationCausedByInitials>
{causedBy
? causedBy.fullname
.split(' ')
.map((n) => n[0])
.join('.')
: 'RU'}
</NotificationCausedByInitials>
</NotificationCausedBy>
<NotificationContent>
<NotificationBody>{prefix}</NotificationBody>
<NotificationContentFooter>
<span>{dayjs.duration(dayjs(createdAt).diff(dayjs())).humanize(true)}</span>
<NotificationSeparator></NotificationSeparator>
<span>{dataMap.get('ProjectName')}</span>
</NotificationContentFooter>
</NotificationContent>
</NotificationLink>
<NotificationControls>
<NotificationButtons>
<NotificationButton onClick={() => onToggleRead()}>
{read ? <Circle width={18} height={18} /> : <CheckCircleOutline width={18} height={18} />}
</NotificationButton>
</NotificationButtons>
</NotificationControls>
</NotificationWrapper>
);
};
const PopupContent = styled.div`
display: flex;
flex-direction: column;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 10px;
border-color: #414561;
`;
const tabs = [
{ label: 'All', key: NotificationFilter.All },
{ label: 'Unread', key: NotificationFilter.Unread },
{ label: 'I was mentioned', key: NotificationFilter.Mentioned },
{ label: 'Assigned to me', key: NotificationFilter.Assigned },
];
type NotificationEntry = {
id: string;
read: boolean;
readAt?: string | undefined | null;
notification: {
id: string;
data: Array<{ key: string; value: string }>;
actionType: ActionType;
causedBy?: { id: string; username: string; fullname: string } | undefined | null;
createdAt: string;
};
};
type NotificationPopupProps = {
onToggleRead: () => void;
};
const NotificationHeaderMenu = styled.div`
position: absolute;
right: 16px;
top: 16px;
`;
const NotificationHeaderMenuIcon = styled.div`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
svg {
fill: #fff;
stroke: #fff;
}
`;
const NotificationHeaderMenuContent = styled.div<{ show: boolean }>`
min-width: 130px;
position: absolute;
top: 16px;
background: #fff;
border-radius: 6px;
height: 50px;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
background: #262c49;
padding: 6px;
display: flex;
flex-direction: column;
`;
const NotificationHeaderMenuButton = styled.div`
position: relative;
padding-left: 4px;
padding-right: 4px;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
&:hover {
background: ${(props) => props.theme.colors.primary};
}
`;
const NotificationPopup: React.FC<NotificationPopupProps> = ({ onToggleRead }) => {
const [filter, setFilter] = useLocalStorage<NotificationFilter>(
localStorage.NOTIFICATIONS_FILTER,
NotificationFilter.Unread,
);
const [data, setData] = useState<{ nodes: Array<NotificationEntry>; hasNextPage: boolean; cursor: string }>({
nodes: [],
hasNextPage: false,
cursor: '',
});
const [toggleRead] = useNotificationToggleReadMutation({
onCompleted: (data) => {
setData((prev) => {
return produce(prev, (draft) => {
const idx = draft.nodes.findIndex((n) => n.id === data.notificationToggleRead.id);
if (idx !== -1) {
draft.nodes[idx].read = data.notificationToggleRead.read;
draft.nodes[idx].readAt = data.notificationToggleRead.readAt;
}
});
});
onToggleRead();
},
});
const { fetchMore } = useNotificationsQuery({
variables: { limit: 8, filter },
fetchPolicy: 'network-only',
onCompleted: (d) => {
setData((prev) => ({
hasNextPage: d.notified.pageInfo.hasNextPage,
cursor: d.notified.pageInfo.endCursor ?? '',
nodes: [...prev.nodes, ...d.notified.notified],
}));
},
});
useNotificationAddedSubscription({
onSubscriptionData: (d) => {
setData((n) => {
if (d.subscriptionData.data) {
return {
...n,
nodes: [d.subscriptionData.data.notificationAdded, ...n.nodes],
};
}
return n;
});
},
});
const [toggleAllRead] = useNotificationMarkAllReadMutation();
const [showHeaderMenu, setShowHeaderMenu] = useState(false);
const $menuContent = useRef<HTMLDivElement>(null);
useOnOutsideClick($menuContent, true, () => setShowHeaderMenu(false), null);
return ( return (
<Popup title={null} tab={0} borders={false} padding={false}> <Popup title={null} tab={0} borders={false} padding={false}>
<PopupContent>
<NotificationHeader> <NotificationHeader>
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle> <NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
<NotificationHeaderMenu>
<NotificationHeaderMenuIcon onClick={() => setShowHeaderMenu(true)}>
<Ellipsis size={18} color="#fff" vertical={false} />
<NotificationHeaderMenuContent ref={$menuContent} show={showHeaderMenu}>
<NotificationHeaderMenuButton
onClick={(e) => {
e.stopPropagation();
setShowHeaderMenu(() => false);
toggleAllRead().then(() => {
setData((prev) =>
produce(prev, (draftData) => {
draftData.nodes = draftData.nodes.map((node) => ({ ...node, read: true }));
}),
);
onToggleRead();
});
}}
>
Mark all as read
</NotificationHeaderMenuButton>
</NotificationHeaderMenuContent>
</NotificationHeaderMenuIcon>
</NotificationHeaderMenu>
</NotificationHeader> </NotificationHeader>
<NotificationTabs> <ul>{children}</ul>
{tabs.map((tab) => ( <NotificationFooter>View All</NotificationFooter>
<NotificationTab
key={tab.key}
onClick={() => {
if (filter !== tab.key) {
setData({ cursor: '', hasNextPage: false, nodes: [] });
setFilter(tab.key);
}
}}
active={tab.key === filter}
>
{tab.label}
</NotificationTab>
))}
</NotificationTabs>
{data.nodes.length !== 0 ? (
<Notifications
onScroll={({ currentTarget }) => {
if (Math.ceil(currentTarget.scrollTop + currentTarget.clientHeight) >= currentTarget.scrollHeight) {
if (data.hasNextPage) {
console.log(`fetching more = ${data.cursor} - ${data.hasNextPage}`);
fetchMore({
variables: {
limit: 8,
filter,
cursor: data.cursor,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
setData((d) => ({
cursor: fetchMoreResult.notified.pageInfo.endCursor ?? '',
hasNextPage: fetchMoreResult.notified.pageInfo.hasNextPage,
nodes: [...d.nodes, ...fetchMoreResult.notified.notified],
}));
return {
...prev,
notified: {
...prev.notified,
pageInfo: {
...fetchMoreResult.notified.pageInfo,
},
notified: [...prev.notified.notified, ...fetchMoreResult.notified.notified],
},
};
},
});
}
}
}}
>
{data.nodes.map((n) => (
<Notification
key={n.id}
read={n.read}
actionType={n.notification.actionType}
data={n.notification.data}
createdAt={n.notification.createdAt}
causedBy={n.notification.causedBy}
onToggleRead={() =>
toggleRead({
variables: { notifiedID: n.id },
optimisticResponse: {
__typename: 'Mutation',
notificationToggleRead: {
__typename: 'Notified',
id: n.id,
read: !n.read,
readAt: new Date().toUTCString(),
},
},
}).then(() => {
onToggleRead();
})
}
/>
))}
</Notifications>
) : (
<EmptyMessage>
<EmptyMessageLabel>You have {getFilterMessage(filter)} notifications</EmptyMessageLabel>
</EmptyMessage>
)}
</PopupContent>
</Popup> </Popup>
); );
}; };

View File

@ -9,22 +9,14 @@ type ActivityMessageProps = {
}; };
function getVariable(data: Array<TaskActivityData>, name: string) { function getVariable(data: Array<TaskActivityData>, name: string) {
const target = data.find((d) => d.name === name); const target = data.find(d => d.name === name);
return target ? target.value : null; return target ? target.value : null;
} }
function getVariableBool(data: Array<TaskActivityData>, name: string, defaultValue = false) { function renderDate(timestamp: string | null) {
const target = data.find((d) => d.name === name);
return target ? target.value === 'true' : defaultValue;
}
function renderDate(timestamp: string | null, hasTime: boolean) {
if (timestamp) { if (timestamp) {
if (hasTime) {
return dayjs(timestamp).format('MMM D [at] h:mm A'); return dayjs(timestamp).format('MMM D [at] h:mm A');
} }
return dayjs(timestamp).format('MMM D');
}
return null; return null;
} }
@ -38,19 +30,13 @@ const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`; message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`;
break; break;
case ActivityType.TaskDueDateAdded: case ActivityType.TaskDueDateAdded:
message = `set this task to be due ${renderDate( message = `set this task to be due ${renderDate(getVariable(data, 'DueDate'))}`;
getVariable(data, 'DueDate'),
getVariableBool(data, 'HasTime', true),
)}`;
break; break;
case ActivityType.TaskDueDateRemoved: case ActivityType.TaskDueDateRemoved:
message = `removed the due date from this task`; message = `removed the due date from this task`;
break; break;
case ActivityType.TaskDueDateChanged: case ActivityType.TaskDueDateChanged:
message = `changed the due date of this task to ${renderDate( message = `changed the due date of this task to ${renderDate(getVariable(data, 'CurDueDate'))}`;
getVariable(data, 'CurDueDate'),
getVariableBool(data, 'HasTime', true),
)}`;
break; break;
case ActivityType.TaskMarkedComplete: case ActivityType.TaskMarkedComplete:
message = `marked this task complete`; message = `marked this task complete`;

View File

@ -4,7 +4,6 @@ import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
import theme from 'App/ThemeStyles'; import theme from 'App/ThemeStyles';
import { Checkmark } from 'shared/icons';
export const Container = styled.div` export const Container = styled.div`
display: flex; display: flex;
@ -23,14 +22,14 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
position: relative; position: relative;
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: ${(props) => props.theme.borderRadius.alternate}; border-radius: ${props => props.theme.borderRadius.alternate};
display: flex; display: flex;
align-items: center; align-items: center;
background: transparent; background: transparent;
& span { & span {
margin-left: 4px; margin-left: 4px;
} }
${(props) => ${props =>
props.invert props.invert
? css` ? css`
background: ${props.theme.colors.success}; background: ${props.theme.colors.success};
@ -64,7 +63,7 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
color: ${props.theme.colors.success}; color: ${props.theme.colors.success};
} }
`} `}
${(props) => ${props =>
props.invert && props.invert &&
css` css`
opacity: 0.6; opacity: 0.6;
@ -90,7 +89,7 @@ export const SidebarTitle = styled.div`
font-size: 12px; font-size: 12px;
min-height: 24px; min-height: 24px;
margin-left: 8px; margin-left: 8px;
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)}; color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
padding-top: 4px; padding-top: 4px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
@ -111,12 +110,12 @@ export const skeletonKeyframes = keyframes`
export const SidebarButton = styled.div<{ $loading?: boolean }>` export const SidebarButton = styled.div<{ $loading?: boolean }>`
font-size: 14px; font-size: 14px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
min-height: 32px; min-height: 32px;
width: 100%; width: 100%;
border-radius: 6px; border-radius: 6px;
${(props) => ${props =>
props.$loading props.$loading
? css` ? css`
background: ${props.theme.colors.bg.primary}; background: ${props.theme.colors.bg.primary};
@ -184,7 +183,7 @@ export const TaskDetailsTitleWrapper = styled.div<{ $loading?: boolean }>`
margin: 8px 0 4px 0; margin: 8px 0 4px 0;
display: flex; display: flex;
border-radius: 6px; border-radius: 6px;
${(props) => props.$loading && `background: ${props.theme.colors.bg.primary};`} ${props => props.$loading && `background: ${props.theme.colors.bg.primary};`}
`; `;
export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>` export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>`
@ -202,7 +201,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>
&:disabled { &:disabled {
opacity: 1; opacity: 1;
} }
${(props) => ${props =>
props.$loading props.$loading
? css` ? css`
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor}); background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
@ -227,7 +226,7 @@ export const DueDateTitle = styled.div`
font-size: 12px; font-size: 12px;
min-height: 24px; min-height: 24px;
margin-left: 8px; margin-left: 8px;
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)}; color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
padding-top: 8px; padding-top: 8px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
@ -238,7 +237,7 @@ export const AssignedUsersSection = styled.div`
padding-right: 32px; padding-right: 32px;
padding-top: 24px; padding-top: 24px;
padding-bottom: 24px; padding-bottom: 24px;
border-bottom: 1px solid ${(props) => props.theme.colors.alternate}; border-bottom: 1px solid ${props => props.theme.colors.alternate};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`; `;
@ -256,10 +255,10 @@ export const AssignUserIcon = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&:hover { &:hover {
border: 1px solid ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.75)}; border: 1px solid ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
} }
&:hover svg { &:hover svg {
fill: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.75)}; fill: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
} }
`; `;
@ -274,17 +273,17 @@ export const AssignUsersButton = styled.div`
align-items: center; align-items: center;
border: 1px solid transparent; border: 1px solid transparent;
&:hover { &:hover {
border: 1px solid ${(props) => mixin.darken(props.theme.colors.alternate, 0.15)}; border: 1px solid ${props => mixin.darken(props.theme.colors.alternate, 0.15)};
} }
&:hover ${AssignUserIcon} { &:hover ${AssignUserIcon} {
border: 1px solid ${(props) => props.theme.colors.alternate}; border: 1px solid ${props => props.theme.colors.alternate};
} }
`; `;
export const AssignUserLabel = styled.span` export const AssignUserLabel = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
line-height: 15px; line-height: 15px;
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)}; color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
`; `;
export const ExtraActionsSection = styled.div` export const ExtraActionsSection = styled.div`
@ -296,7 +295,7 @@ export const ExtraActionsSection = styled.div`
`; `;
export const ActionButtonsTitle = styled.h3` export const ActionButtonsTitle = styled.h3`
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@ -306,17 +305,16 @@ export const ActionButton = styled(Button)`
margin-top: 8px; margin-top: 8px;
margin-left: -10px; margin-left: -10px;
padding: 8px 16px; padding: 8px 16px;
background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.5)}; background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.5)};
text-align: left; text-align: left;
transition: transform 0.2s ease; transition: transform 0.2s ease;
& span { & span {
position: unset;
justify-content: flex-start; justify-content: flex-start;
} }
&:hover { &:hover {
box-shadow: none; box-shadow: none;
transform: translateX(4px); transform: translateX(4px);
background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.75)}; background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.75)};
} }
`; `;
@ -335,10 +333,10 @@ export const HeaderActionIcon = styled.div`
cursor: pointer; cursor: pointer;
svg { svg {
fill: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)}; fill: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
} }
&:hover svg { &:hover svg {
fill: ${(props) => mixin.rgba(props.theme.colors.primary, 0.75)}); fill: ${props => mixin.rgba(props.theme.colors.primary, 0.75)});
} }
`; `;
@ -395,7 +393,7 @@ export const MetaDetail = styled.div`
`; `;
export const MetaDetailTitle = styled.h3` export const MetaDetailTitle = styled.h3`
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@ -414,7 +412,7 @@ export const MetaDetailContent = styled.div`
`; `;
export const TaskDetailsAddLabel = styled.div` export const TaskDetailsAddLabel = styled.div`
border-radius: 3px; border-radius: 3px;
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)}; background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
@ -429,7 +427,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 3px; border-radius: 3px;
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)}; background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
@ -445,7 +443,7 @@ export const TaskDetailLabel = styled.div<{ color: string }>`
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
background-color: ${(props) => props.color}; background-color: ${props => props.color};
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -498,22 +496,17 @@ export const TabBarSection = styled.div`
margin-top: 2px; margin-top: 2px;
padding-left: 23px; padding-left: 23px;
display: flex; display: flex;
justify-content: space-between;
text-transform: uppercase; text-transform: uppercase;
min-height: 35px; min-height: 35px;
border-bottom: 1px solid #414561; border-bottom: 1px solid #414561;
`; `;
export const TabBarItem = styled.div` export const TabBarItem = styled.div`
box-shadow: inset 0 -2px ${(props) => props.theme.colors.primary}; box-shadow: inset 0 -2px ${props => props.theme.colors.primary};
padding: 12px 7px 14px 7px; padding: 12px 7px 14px 7px;
margin-bottom: -1px; margin-bottom: -1px;
margin-right: 36px; margin-right: 36px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
`;
export const TabBarButton = styled(Button)`
padding: 6px 12px;
`; `;
export const CommentContainer = styled.div` export const CommentContainer = styled.div`
@ -549,13 +542,13 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
line-height: 28px; line-height: 28px;
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
background: #1f243e; background: #1f243e;
border: none; border: none;
transition: max-height 200ms, height 200ms, min-height 200ms; transition: max-height 200ms, height 200ms, min-height 200ms;
min-height: 36px; min-height: 36px;
max-height: 36px; max-height: 36px;
${(props) => ${props =>
props.$showCommentActions props.$showCommentActions
? css` ? css`
min-height: 80px; min-height: 80px;
@ -568,7 +561,7 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
`; `;
export const CommentEditorActions = styled.div<{ visible: boolean }>` export const CommentEditorActions = styled.div<{ visible: boolean }>`
display: ${(props) => (props.visible ? 'flex' : 'none')}; display: ${props => (props.visible ? 'flex' : 'none')};
align-items: center; align-items: center;
padding: 5px 5px 5px 9px; padding: 5px 5px 5px 9px;
border-top: 1px solid #414561; border-top: 1px solid #414561;
@ -601,7 +594,7 @@ export const ActivityItemCommentAction = styled.div`
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
svg { svg {
fill: ${(props) => props.theme.colors.text.primary} !important; fill: ${props => props.theme.colors.text.primary} !important;
} }
`; `;
@ -621,7 +614,7 @@ export const ActivityItemHeader = styled.div<{ editable?: boolean }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 8px; padding-left: 8px;
${(props) => props.editable && 'width: 100%;'} ${props => props.editable && 'width: 100%;'}
`; `;
export const ActivityItemHeaderUser = styled(TaskAssignee)` export const ActivityItemHeaderUser = styled(TaskAssignee)`
align-items: start; align-items: start;
@ -630,7 +623,7 @@ export const ActivityItemHeaderUser = styled(TaskAssignee)`
export const ActivityItemHeaderTitle = styled.div` export const ActivityItemHeaderTitle = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
padding-bottom: 2px; padding-bottom: 2px;
`; `;
@ -641,8 +634,8 @@ export const ActivityItemHeaderTitleName = styled.span`
export const ActivityItemTimestamp = styled.span<{ margin: number }>` export const ActivityItemTimestamp = styled.span<{ margin: number }>`
font-size: 12px; font-size: 12px;
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.65)}; color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.65)};
margin-left: ${(props) => props.margin}px; margin-left: ${props => props.margin}px;
`; `;
export const ActivityItemDetails = styled.div` export const ActivityItemDetails = styled.div`
@ -656,11 +649,11 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
border-radius: 3px; border-radius: 3px;
${mixin.boxShadowCard} ${mixin.boxShadowCard}
position: relative; position: relative;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
padding: 8px 12px; padding: 8px 12px;
margin: 4px 0; margin: 4px 0;
background-color: ${(props) => mixin.darken(props.theme.colors.alternate, 0.1)}; background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
${(props) => props.editable && 'width: 100%;'} ${props => props.editable && 'width: 100%;'}
& span { & span {
display: inline-flex; display: inline-flex;
@ -690,7 +683,7 @@ export const ActivityItemCommentActions = styled.div`
export const ActivityItemLog = styled.span` export const ActivityItemLog = styled.span`
margin-left: 2px; margin-left: 2px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
`; `;
export const ViewRawButton = styled.button` export const ViewRawButton = styled.button`
@ -701,9 +694,9 @@ export const ViewRawButton = styled.button`
right: 4px; right: 4px;
bottom: -24px; bottom: -24px;
cursor: pointer; cursor: pointer;
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.25)}; color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.25)};
&:hover { &:hover {
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
} }
`; `;
@ -719,8 +712,3 @@ export const TaskDetailsEditor = styled(TextareaAutosize)`
outline: none; outline: none;
border: none; border: none;
`; `;
export const WatchedCheckmark = styled(Checkmark)`
position: absolute;
right: 16px;
`;

View File

@ -12,7 +12,6 @@ import {
CheckSquareOutline, CheckSquareOutline,
At, At,
Smile, Smile,
Eye,
} from 'shared/icons'; } from 'shared/icons';
import { toArray } from 'react-emoji-render'; import { toArray } from 'react-emoji-render';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
@ -80,8 +79,6 @@ import {
ActivityItemHeaderTitle, ActivityItemHeaderTitle,
ActivityItemHeaderTitleName, ActivityItemHeaderTitleName,
ActivityItemComment, ActivityItemComment,
TabBarButton,
WatchedCheckmark,
} from './Styles'; } from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd'; import onDragEnd from './onDragEnd';
@ -239,7 +236,6 @@ type TaskDetailsProps = {
onToggleChecklistItem: (itemID: string, complete: boolean) => void; onToggleChecklistItem: (itemID: string, complete: boolean) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onToggleTaskWatch: (task: Task, watched: boolean) => void;
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void; onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onCreateComment: (task: Task, message: string) => void; onCreateComment: (task: Task, message: string) => void;
@ -261,7 +257,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
task, task,
editableComment = null, editableComment = null,
onDeleteChecklist, onDeleteChecklist,
onToggleTaskWatch,
onTaskNameChange, onTaskNameChange,
onCommentShowActions, onCommentShowActions,
onOpenAddChecklistPopup, onOpenAddChecklistPopup,
@ -350,9 +345,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
} }
}} }}
> >
{task.dueDate.at ? ( {task.dueDate ? (
<SidebarButtonText> <SidebarButtonText>
{dayjs(task.dueDate.at).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')} {dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')}
</SidebarButtonText> </SidebarButtonText>
) : ( ) : (
<SidebarButtonText>No due date</SidebarButtonText> <SidebarButtonText>No due date</SidebarButtonText>
@ -422,14 +417,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
Checklist Checklist
</ActionButton> </ActionButton>
<ActionButton>Cover</ActionButton> <ActionButton>Cover</ActionButton>
<ActionButton
onClick={() => {
onToggleTaskWatch(task, !task.watched);
}}
icon={<Eye width={12} height={12} />}
>
Watch {task.watched && <WatchedCheckmark width={18} height={18} />}
</ActionButton>
</ExtraActionsSection> </ExtraActionsSection>
)} )}
</LeftSidebarContent> </LeftSidebarContent>
@ -631,7 +618,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
{activityStream.map((stream) => {activityStream.map((stream) =>
stream.data.type === 'comment' ? ( stream.data.type === 'comment' ? (
<StreamComment <StreamComment
key={stream.id}
onExtraActions={onCommentShowActions} onExtraActions={onCommentShowActions}
onCancelCommentEdit={onCancelCommentEdit} onCancelCommentEdit={onCancelCommentEdit}
onUpdateComment={(message) => onUpdateComment(stream.id, message)} onUpdateComment={(message) => onUpdateComment(stream.id, message)}
@ -640,7 +626,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
/> />
) : ( ) : (
<StreamActivity <StreamActivity
key={stream.id}
activity={task.activity && task.activity.find((activity) => activity.id === stream.id)} activity={task.activity && task.activity.find((activity) => activity.id === stream.id)}
/> />
), ),

View File

@ -30,16 +30,19 @@ function plugin(options) {
} }
function getEmoji(match) { function getEmoji(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);
return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />); return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
} }
function transformer(tree) { function transformer(tree) {
visit(tree, 'paragraph', function (node) { visit(tree, 'paragraph', function (node) {
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';
// jnode.tagName = 'div'; // jnode.tagName = 'div';
@ -55,6 +58,7 @@ function plugin(options) {
if (emoticonEnable) { if (emoticonEnable) {
// node.value = node.value.replace(RE_SHORT, getEmojiByShortCode); // node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
} }
console.log(node);
}); });
} }

View File

@ -6,11 +6,11 @@ import { NavLink, Link } from 'react-router-dom';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>` export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
z-index: ${(props) => props.zIndex}; z-index: ${props => props.zIndex};
position: relative; position: relative;
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.primary}, box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.primary},
inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.07)}; inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
`; `;
export const NavbarWrapper = styled.div` export const NavbarWrapper = styled.div`
@ -27,9 +27,9 @@ export const NavbarHeader = styled.header`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: ${(props) => props.theme.colors.bg.primary}; background: ${props => props.theme.colors.bg.primary};
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
border-bottom: 1px solid ${(props) => mixin.rgba(props.theme.colors.alternate, 0.65)}; border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
`; `;
export const Breadcrumbs = styled.div` export const Breadcrumbs = styled.div`
color: rgb(94, 108, 132); color: rgb(94, 108, 132);
@ -59,7 +59,7 @@ export const ProjectSwitchInner = styled.div`
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
background-color: ${(props) => props.theme.colors.primary}; background-color: ${props => props.theme.colors.primary};
`; `;
export const ProjectSwitch = styled.div` export const ProjectSwitch = styled.div`
@ -109,27 +109,10 @@ export const NavbarLink = styled(Link)`
cursor: pointer; cursor: pointer;
`; `;
export const NotificationCount = styled.div`
position: absolute;
top: -6px;
right: -6px;
background: #7367f0;
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid rgb(16, 22, 58);
color: #fff;
font-size: 14px;
`;
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>` export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
margin-right: 20px; margin-right: 20px;
position: relative;
cursor: pointer; cursor: pointer;
${(props) => ${props =>
props.disabled && props.disabled &&
css` css`
opacity: 0.5; opacity: 0.5;
@ -159,14 +142,14 @@ export const ProfileIcon = styled.div<{
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;
`; `;
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>` export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
display: flex; display: flex;
${(props) => !props.nameOnly && 'padding-top: 9px;'} ${props => !props.nameOnly && 'padding-top: 9px;'}
margin-left: -6px; margin-left: -6px;
align-items: center; align-items: center;
max-width: 100%; max-width: 100%;
@ -184,7 +167,7 @@ export const ProjectTabs = styled.div`
export const ProjectTab = styled(NavLink)` export const ProjectTab = styled(NavLink)`
font-size: 80%; font-size: 80%;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
font-size: 15px; font-size: 15px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -201,22 +184,22 @@ export const ProjectTab = styled(NavLink)`
} }
&:hover { &:hover {
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary}; box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
color: ${(props) => props.theme.colors.text.secondary}; color: ${props => props.theme.colors.text.secondary};
} }
&.active { &.active {
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary}; box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: ${(props) => props.theme.colors.secondary}; color: ${props => props.theme.colors.secondary};
} }
&.active:hover { &.active:hover {
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary}; box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
color: ${(props) => props.theme.colors.secondary}; color: ${props => props.theme.colors.secondary};
} }
`; `;
export const ProjectName = styled.h1` export const ProjectName = styled.h1`
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
font-weight: 600; font-weight: 600;
font-size: 20px; font-size: 20px;
padding: 3px 10px 3px 8px; padding: 3px 10px 3px 8px;
@ -258,7 +241,7 @@ export const ProjectNameTextarea = styled.input`
font-size: 20px; font-size: 20px;
padding: 3px 10px 3px 8px; padding: 3px 10px 3px 8px;
&:focus { &:focus {
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px; box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
} }
`; `;
@ -276,7 +259,7 @@ export const ProjectSwitcher = styled.button`
color: #c2c6dc; color: #c2c6dc;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: ${(props) => props.theme.colors.primary}; background: ${props => props.theme.colors.primary};
} }
`; `;
@ -300,7 +283,7 @@ export const ProjectSettingsButton = styled.button`
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: ${(props) => props.theme.colors.primary}; background: ${props => props.theme.colors.primary};
} }
`; `;
@ -326,7 +309,7 @@ export const SignIn = styled(Button)`
export const NavSeparator = styled.div` export const NavSeparator = styled.div`
width: 1px; width: 1px;
background: ${(props) => props.theme.colors.border}; background: ${props => props.theme.colors.border};
height: 34px; height: 34px;
margin: 0 20px; margin: 0 20px;
`; `;
@ -343,11 +326,11 @@ export const LogoContainer = styled(Link)`
export const TaskcafeTitle = styled.h2` export const TaskcafeTitle = styled.h2`
margin-left: 5px; margin-left: 5px;
color: ${(props) => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
font-size: 20px; font-size: 20px;
`; `;
export const TaskcafeLogo = styled(Taskcafe)` export const TaskcafeLogo = styled(Taskcafe)`
fill: ${(props) => props.theme.colors.text.primary}; fill: ${props => props.theme.colors.text.primary};
stroke: ${(props) => props.theme.colors.text.primary}; stroke: ${props => props.theme.colors.text.primary};
`; `;

View File

@ -36,7 +36,6 @@ import {
ProjectMember, ProjectMember,
ProjectMembers, ProjectMembers,
ProjectSwitchInner, ProjectSwitchInner,
NotificationCount,
} from './Styles'; } from './Styles';
type IconContainerProps = { type IconContainerProps = {
@ -186,7 +185,6 @@ type NavBarProps = {
projectMembers?: Array<TaskUser> | null; projectMembers?: Array<TaskUser> | null;
projectInvitedMembers?: Array<InvitedUser> | null; projectInvitedMembers?: Array<InvitedUser> | null;
hasUnread: boolean;
onRemoveFromBoard?: (userID: string) => void; onRemoveFromBoard?: (userID: string) => void;
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void; onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void; onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
@ -205,7 +203,6 @@ const NavBar: React.FC<NavBarProps> = ({
onOpenProjectFinder, onOpenProjectFinder,
onFavorite, onFavorite,
onSetTab, onSetTab,
hasUnread,
projectInvitedMembers, projectInvitedMembers,
onChangeRole, onChangeRole,
name, name,
@ -333,9 +330,8 @@ const NavBar: React.FC<NavBarProps> = ({
<IconContainer disabled onClick={NOOP}> <IconContainer disabled onClick={NOOP}>
<ListUnordered width={20} height={20} /> <ListUnordered width={20} height={20} />
</IconContainer> </IconContainer>
<IconContainer onClick={onNotificationClick}> <IconContainer disabled onClick={onNotificationClick}>
<Bell width={20} height={20} /> <Bell color="#c2c6dc" size={20} />
{hasUnread && <NotificationCount />}
</IconContainer> </IconContainer>
<IconContainer disabled onClick={NOOP}> <IconContainer disabled onClick={NOOP}>
<BarChart width={20} height={20} /> <BarChart width={20} height={20} />

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
mutation createProject($teamID: UUID, $name: String!) { mutation createProject($teamID: UUID, $name: String!) {
createProject(input: {teamID: $teamID, name: $name}) { createProject(input: {teamID: $teamID, name: $name}) {
id id
shortId
name name
team { team {
id id

View File

@ -2,9 +2,8 @@ import gql from 'graphql-tag';
import TASK_FRAGMENT from './fragments/task'; import TASK_FRAGMENT from './fragments/task';
const FIND_PROJECT_QUERY = gql` const FIND_PROJECT_QUERY = gql`
query findProject($projectID: String!) { query findProject($projectID: UUID!) {
findProject(input: { projectShortID: $projectID }) { findProject(input: { projectID: $projectID }) {
id
name name
publicOn publicOn
team { team {

View File

@ -1,18 +1,9 @@
query findTask($taskID: String!) { query findTask($taskID: UUID!) {
findTask(input: {taskShortID: $taskID}) { findTask(input: {taskID: $taskID}) {
id id
shortId
name name
watched
description description
dueDate { dueDate
at
notifications {
id
period
duration
}
}
position position
complete complete
hasTime hasTime

View File

@ -3,15 +3,11 @@ import gql from 'graphql-tag';
const TASK_FRAGMENT = gql` const TASK_FRAGMENT = gql`
fragment TaskFields on Task { fragment TaskFields on Task {
id id
shortId
name name
description description
dueDate { dueDate
at
}
hasTime hasTime
complete complete
watched
completedAt completedAt
position position
badges { badges {
@ -19,10 +15,6 @@ const TASK_FRAGMENT = gql`
complete complete
total total
} }
comments {
unread
total
}
} }
taskGroup { taskGroup {
id id

View File

@ -10,7 +10,6 @@ query getProjects {
} }
projects { projects {
id id
shortId
name name
team { team {
id id

View File

@ -6,15 +6,12 @@ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
myTasks(input: { status: $status, sort: $sort }) { myTasks(input: { status: $status, sort: $sort }) {
tasks { tasks {
id id
shortId
taskGroup { taskGroup {
id id
name name
} }
name name
dueDate { dueDate
at
}
hasTime hasTime
complete complete
completedAt completedAt

View File

@ -1,13 +0,0 @@
import gql from 'graphql-tag';
const CREATE_TASK_MUTATION = gql`
mutation notificationToggleRead($notifiedID: UUID!) {
notificationToggleRead(input: { notifiedID: $notifiedID }) {
id
read
readAt
}
}
`;
export default CREATE_TASK_MUTATION;

View File

@ -1,34 +0,0 @@
import gql from 'graphql-tag';
export const TOP_NAVBAR_QUERY = gql`
query notifications($limit: Int!, $cursor: String, $filter: NotificationFilter!) {
notified(input: { limit: $limit, cursor: $cursor, filter: $filter }) {
totalCount
pageInfo {
endCursor
hasNextPage
}
notified {
id
read
readAt
notification {
id
actionType
data {
key
value
}
causedBy {
username
fullname
id
}
createdAt
}
}
}
}
`;
export default TOP_NAVBAR_QUERY;

View File

@ -1,11 +0,0 @@
import gql from 'graphql-tag';
const CREATE_TASK_MUTATION = gql`
mutation notificationMarkAllRead {
notificationMarkAllRead {
success
}
}
`;
export default CREATE_TASK_MUTATION;

View File

@ -1,26 +0,0 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from './fragments/task';
const FIND_PROJECT_QUERY = gql`
subscription notificationAdded {
notificationAdded {
id
read
readAt
notification {
id
actionType
data {
key
value
}
causedBy {
username
fullname
id
}
createdAt
}
}
}
`;

View File

@ -1,12 +0,0 @@
import gql from 'graphql-tag';
const CREATE_TASK_MUTATION = gql`
mutation toggleTaskWatch($taskID: UUID!) {
toggleTaskWatch(input: { taskID: $taskID }) {
id
watched
}
}
`;
export default CREATE_TASK_MUTATION;

View File

@ -3,19 +3,20 @@ import gql from 'graphql-tag';
export const TOP_NAVBAR_QUERY = gql` export const TOP_NAVBAR_QUERY = gql`
query topNavbar { query topNavbar {
notifications { notifications {
id
read
readAt
notification {
id
actionType
causedBy {
username
fullname
id
}
createdAt createdAt
read
id
entity {
id
type
name
} }
actor {
id
type
name
}
actionType
} }
me { me {
user { user {

View File

@ -1,11 +0,0 @@
import gql from 'graphql-tag';
export const TOP_NAVBAR_QUERY = gql`
query hasUnreadNotifications {
hasUnreadNotifications {
unread
}
}
`;
export default TOP_NAVBAR_QUERY;

View File

@ -1,8 +1,4 @@
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!, mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) {
$createNotifications: [CreateTaskDueDateNotification!]!,
$updateNotifications: [UpdateTaskDueDateNotification!]!
$deleteNotifications: [DeleteTaskDueDateNotification!]!
) {
updateTaskDueDate ( updateTaskDueDate (
input: { input: {
taskID: $taskID taskID: $taskID
@ -11,26 +7,7 @@ $deleteNotifications: [DeleteTaskDueDateNotification!]!
} }
) { ) {
id id
dueDate { dueDate
at
}
hasTime hasTime
} }
createTaskDueDateNotifications(input: $createNotifications) {
notifications {
id
period
duration
}
}
updateTaskDueDateNotifications(input: $updateNotifications) {
notifications {
id
period
duration
}
}
deleteTaskDueDateNotifications(input: $deleteNotifications) {
notifications
}
} }

View File

@ -1,13 +1,4 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import React from 'react';
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '');
} catch (error) {
return undefined;
}
}
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => { const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || ''); const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
@ -20,78 +11,3 @@ const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispa
}; };
export default useStateWithLocalStorage; export default useStateWithLocalStorage;
type SetValue<T> = Dispatch<SetStateAction<T>>;
function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
// Get from local storage then
// parse stored json or return initialValue
const readValue = (): T => {
// Prevent build error "window is undefined" but keep keep working
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? (parseJSON(item) as T) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
return initialValue;
}
};
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(readValue);
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: SetValue<T> = (value) => {
// Prevent build error "window is undefined" but keeps working
if (typeof window === 'undefined') {
console.warn(`Tried setting localStorage key “${key}” even though environment is not a client`);
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(storedValue) : value;
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue));
// Save state
setStoredValue(newValue);
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'));
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error);
}
};
useEffect(() => {
setStoredValue(readValue());
}, []);
useEffect(() => {
const handleStorageChange = () => {
setStoredValue(readValue());
};
// this only works for other documents, not the current one
window.addEventListener('storage', handleStorageChange);
// this is a custom event, triggered in writeValueToLocalStorage
window.addEventListener('local-storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('local-storage', handleStorageChange);
};
}, []);
return [storedValue, setValue];
}
export { useLocalStorage };

View File

@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import Icon, { IconProps } from './Icon';
const Bell: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => { type Props = {
size: number | string;
color: string;
};
const Bell = ({ size, color }: Props) => {
return ( return (
<Icon width={width} height={height} className={className} viewBox="0 0 448 512"> <svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 448 512">
<path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" /> <path d="M439.39 362.29c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71zM67.53 368c21.22-27.97 44.42-74.33 44.53-159.42 0-.2-.06-.38-.06-.58 0-61.86 50.14-112 112-112s112 50.14 112 112c0 .2-.06.38-.06.58.11 85.1 23.31 131.46 44.53 159.42H67.53zM224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64z" />
</Icon> </svg>
); );
}; };

View File

@ -1,12 +0,0 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Bubble: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 576 512">
<path d="M416 192c0-88.4-93.1-160-208-160S0 103.6 0 192c0 34.3 14.1 65.9 38 92-13.4 30.2-35.5 54.2-35.8 54.5-2.2 2.3-2.8 5.7-1.5 8.7S4.8 352 8 352c36.6 0 66.9-12.3 88.7-25 32.2 15.7 70.3 25 111.3 25 114.9 0 208-71.6 208-160zm122 220c23.9-26 38-57.7 38-92 0-66.9-53.5-124.2-129.3-148.1.9 6.6 1.3 13.3 1.3 20.1 0 105.9-107.7 192-240 192-10.8 0-21.3-.8-31.7-1.9C207.8 439.6 281.8 480 368 480c41 0 79.1-9.2 111.3-25 21.8 12.7 52.1 25 88.7 25 3.2 0 6.1-1.9 7.3-4.8 1.3-2.9.7-6.3-1.5-8.7-.3-.3-22.4-24.2-35.8-54.5z" />
</Icon>
);
};
export default Bubble;

View File

@ -1,12 +0,0 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Circle: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z" />
</Icon>
);
};
export default Circle;

View File

@ -1,12 +0,0 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const CircleSolid: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" />
</Icon>
);
};
export default CircleSolid;

View File

@ -1,12 +0,0 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const UserCircle: React.FC<IconProps> = ({ width = '16px', height = '16px', className, onClick }) => {
return (
<Icon onClick={onClick} width={width} height={height} className={className} viewBox="0 0 496 512">
<path d="M248 104c-53 0-96 43-96 96s43 96 96 96 96-43 96-96-43-96-96-96zm0 144c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48zm0-240C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-49.7 0-95.1-18.3-130.1-48.4 14.9-23 40.4-38.6 69.6-39.5 20.8 6.4 40.6 9.6 60.5 9.6s39.7-3.1 60.5-9.6c29.2 1 54.7 16.5 69.6 39.5-35 30.1-80.4 48.4-130.1 48.4zm162.7-84.1c-24.4-31.4-62.1-51.9-105.1-51.9-10.2 0-26 9.6-57.6 9.6-31.5 0-47.4-9.6-57.6-9.6-42.9 0-80.6 20.5-105.1 51.9C61.9 339.2 48 299.2 48 256c0-110.3 89.7-200 200-200s200 89.7 200 200c0 43.2-13.9 83.2-37.3 115.9z" />
</Icon>
);
};
export default UserCircle;

View File

@ -1,10 +1,6 @@
import Cross from './Cross'; import Cross from './Cross';
import Cog from './Cog'; import Cog from './Cog';
import Cogs from './Cogs'; import Cogs from './Cogs';
import Circle from './Circle';
import CircleSolid from './CircleSolid';
import UserCircle from './UserCircle';
import Bubble from './Bubble';
import ArrowDown from './ArrowDown'; import ArrowDown from './ArrowDown';
import CheckCircleOutline from './CheckCircleOutline'; import CheckCircleOutline from './CheckCircleOutline';
import Briefcase from './Briefcase'; import Briefcase from './Briefcase';
@ -114,9 +110,5 @@ export {
Briefcase, Briefcase,
DotCircle, DotCircle,
ChevronRight, ChevronRight,
Circle,
CircleSolid,
Bubble,
UserCircle,
Cogs, Cogs,
}; };

View File

@ -52,10 +52,10 @@ export const base = {
blockToolbarText: colors.white, blockToolbarText: colors.white,
blockToolbarHoverBackground: colors.primary, blockToolbarHoverBackground: colors.primary,
blockToolbarDivider: colors.almostWhite, blockToolbarDivider: colors.almostWhite,
blockToolbarIcon: undefined, blockToolbarIcon: undefined,
blockToolbarIconSelected: colors.white, blockToolbarIconSelected: colors.white,
blockToolbarTextSelected: colors.white, blockToolbarTextSelected: colors.white,
blockToolbarSelectedBackground: colors.greyMid,
noticeInfoBackground: '#F5BE31', noticeInfoBackground: '#F5BE31',
noticeInfoText: colors.almostBlack, noticeInfoText: colors.almostBlack,

View File

@ -1,5 +1,4 @@
const localStorage = { const localStorage = {
NOTIFICATIONS_FILTER: 'notifications_filter',
CARD_LABEL_VARIANT_STORAGE_KEY: 'card_label_variant', CARD_LABEL_VARIANT_STORAGE_KEY: 'card_label_variant',
}; };

View File

@ -8,7 +8,6 @@ const polling = {
MEMBERS: resolve(3000), MEMBERS: resolve(3000),
TEAM_PROJECTS: resolve(3000), TEAM_PROJECTS: resolve(3000),
TASK_DETAILS: resolve(3000), TASK_DETAILS: resolve(3000),
UNREAD_NOTIFICATIONS: resolve(30000),
}; };
export default polling; export default polling;

View File

@ -46,7 +46,7 @@ export function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
if (b.dueDate && !a.dueDate) { if (b.dueDate && !a.dueDate) {
return 1; return 1;
} }
return dayjs(a.dueDate.at).diff(dayjs(b.dueDate.at)); return dayjs(a.dueDate).diff(dayjs(b.dueDate));
} }
if (taskSorting.type === TaskSortingType.COMPLETE) { if (taskSorting.type === TaskSortingType.COMPLETE) {
if (a.complete && !b.complete) { if (a.complete && !b.complete) {

View File

@ -1,5 +1,3 @@
declare module 'loglevel-plugin-remote';
interface JWTToken { interface JWTToken {
userId: string; userId: string;
orgRole: string; orgRole: string;
@ -91,7 +89,7 @@ type ErrorOption =
type SetFailedFn = () => void; type SetFailedFn = () => void;
type ConfirmProps = { type ConfirmProps = {
hasConfirmToken: boolean; hasConfirmToken: boolean;
hasFailed: boolean; onConfirmUser: (setFailed: SetFailedFn) => void;
}; };
type RegisterProps = { type RegisterProps = {

View File

@ -59,13 +59,8 @@ type ChecklistBadge = {
total: number; total: number;
}; };
type CommentsBadge = {
total: number;
unread: boolean;
};
type TaskBadges = { type TaskBadges = {
checklist?: ChecklistBadge | null; checklist?: ChecklistBadge | null;
comments?: CommentsBadge | null;
}; };
type TaskActivityData = { type TaskActivityData = {
@ -103,14 +98,12 @@ type TaskComment = {
type Task = { type Task = {
id: string; id: string;
shortId: string;
taskGroup: InnerTaskGroup; taskGroup: InnerTaskGroup;
name: string; name: string;
watched?: boolean;
badges?: TaskBadges; badges?: TaskBadges;
position: number; position: number;
hasTime?: boolean; hasTime?: boolean;
dueDate: { at?: string; notifications?: Array<{ id: string; period: number; duration: string }> }; dueDate?: string;
complete?: boolean; complete?: boolean;
completedAt?: string | null; completedAt?: string | null;
labels: TaskLabel[]; labels: TaskLabel[];

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@ -8,8 +8,6 @@ require (
github.com/brianvoe/gofakeit/v5 v5.11.2 github.com/brianvoe/gofakeit/v5 v5.11.2
github.com/go-chi/chi v3.3.2+incompatible github.com/go-chi/chi v3.3.2+incompatible
github.com/go-chi/cors v1.2.0 github.com/go-chi/cors v1.2.0
github.com/go-redis/redis v6.15.8+incompatible
github.com/go-redis/redis/v8 v8.0.0-beta.6
github.com/golang-migrate/migrate/v4 v4.11.0 github.com/golang-migrate/migrate/v4 v4.11.0
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/jinzhu/now v1.1.1 github.com/jinzhu/now v1.1.1

View File

@ -1,6 +1,6 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls # Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema: schema:
- internal/graph/schema/*.gql - internal/graph/*.graphqls
# Where should the generated server code go? # Where should the generated server code go?
exec: exec:
@ -22,8 +22,6 @@ resolver:
layout: follow-schema layout: follow-schema
dir: internal/graph dir: internal/graph
package: graph package: graph
filename_template: "{name}.resolvers.go"
# Optional: turn on to use []Thing instead of []*Thing # Optional: turn on to use []Thing instead of []*Thing
omit_slice_element_pointers: true omit_slice_element_pointers: true

View File

@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/utils" "github.com/jordanknott/taskcafe/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -53,7 +52,6 @@ func initConfig() {
viper.SetEnvPrefix("TASKCAFE") viper.SetEnvPrefix("TASKCAFE")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() viper.AutomaticEnv()
config.InitDefaults()
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err == nil { if err == nil {
@ -63,11 +61,31 @@ func initConfig() {
panic(err) panic(err)
} }
viper.SetDefault("server.hostname", "0.0.0.0:3333")
viper.SetDefault("database.host", "127.0.0.1")
viper.SetDefault("database.name", "taskcafe")
viper.SetDefault("database.user", "taskcafe")
viper.SetDefault("database.password", "taskcafe_test")
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
viper.SetDefault("queue.store", "memcache://localhost:11211")
} }
// Execute the root cobra command // Execute the root cobra command
func Execute() { func Execute() {
viper.SetDefault("server.hostname", "0.0.0.0:3333")
viper.SetDefault("database.host", "127.0.0.1")
viper.SetDefault("database.name", "taskcafe")
viper.SetDefault("database.user", "taskcafe")
viper.SetDefault("database.password", "taskcafe_test")
viper.SetDefault("database.port", "5432")
viper.SetDefault("security.token_expiration", "15m")
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
viper.SetDefault("queue.store", "memcache://localhost:11211")
rootCmd.SetVersionTemplate(VersionTemplate()) rootCmd.SetVersionTemplate(VersionTemplate())
rootCmd.AddCommand(newJobCmd(), newTokenCmd(), newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd()) rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
rootCmd.Execute() rootCmd.Execute()
} }

View File

@ -1,60 +0,0 @@
package commands
import (
"time"
"github.com/spf13/cobra"
"github.com/RichardKnop/machinery/v1"
mTasks "github.com/RichardKnop/machinery/v1/tasks"
queueLog "github.com/RichardKnop/machinery/v1/log"
"github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/jobs"
log "github.com/sirupsen/logrus"
)
func newJobCmd() *cobra.Command {
cc := &cobra.Command{
Use: "job",
Short: "Run a task manually",
Long: "Run a task manually",
RunE: func(cmd *cobra.Command, args []string) error {
Formatter := new(log.TextFormatter)
Formatter.TimestampFormat = "02-01-2006 15:04:05"
Formatter.FullTimestamp = true
log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel)
appConfig, err := config.GetAppConfig()
if err != nil {
log.Panic(err)
}
db, err := sqlx.Connect("postgres", config.GetDatabaseConfig().GetDatabaseConnectionUri())
if err != nil {
log.Panic(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
defer db.Close()
log.Info("starting task queue server instance")
jobConfig := appConfig.Job.GetJobConfig()
server, err := machinery.NewServer(&jobConfig)
if err != nil {
// do something with the error
}
queueLog.Set(&jobs.MachineryLogger{})
signature := &mTasks.Signature{
Name: "scheduleDueDateNotifications",
}
server.SendTask(signature)
return nil
},
}
return cc
}

View File

@ -15,7 +15,7 @@ import (
func newResetPasswordCmd() *cobra.Command { func newResetPasswordCmd() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "reset-password <username> <password>", Use: "reset-password",
Short: "Resets password of the specified user", Short: "Resets password of the specified user",
Long: "If the user forgets its password you can reset it with this command.", Long: "If the user forgets its password you can reset it with this command.",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),

View File

@ -1,93 +0,0 @@
package commands
import (
"context"
"fmt"
"time"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus"
)
func newTokenCmd() *cobra.Command {
cc := &cobra.Command{
Use: "token [username]",
Short: "Creates an access token for a user",
Long: "Creates an access token for a user",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Formatter := new(log.TextFormatter)
Formatter.TimestampFormat = "02-01-2006 15:04:05"
Formatter.FullTimestamp = true
log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel)
appConfig, err := config.GetAppConfig()
if err != nil {
return err
}
var dbConnection *sqlx.DB
var retryDuration time.Duration
maxRetryNumber := 4
for i := 0; i < maxRetryNumber; i++ {
dbConnection, err = sqlx.Connect("postgres", appConfig.Database.GetDatabaseConnectionUri())
if err == nil {
break
}
retryDuration = time.Duration(i*2) * time.Second
log.WithFields(log.Fields{"retryNumber": i, "retryDuration": retryDuration}).WithError(err).Error("issue connecting to database, retrying")
if i != maxRetryNumber-1 {
time.Sleep(retryDuration)
}
}
if err != nil {
return err
}
dbConnection.SetMaxOpenConns(25)
dbConnection.SetMaxIdleConns(25)
dbConnection.SetConnMaxLifetime(5 * time.Minute)
defer dbConnection.Close()
if viper.GetBool("migrate") {
log.Info("running auto schema migrations")
if err = runMigration(dbConnection); err != nil {
return err
}
}
ctx := context.Background()
repository := db.NewRepository(dbConnection)
user, err := repository.GetUserAccountByUsername(ctx, args[0])
if err != nil {
return err
}
token, err := repository.CreateAuthToken(ctx, db.CreateAuthTokenParams{
UserID: user.UserID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Hour * 24 * 7),
})
if err != nil {
return err
}
fmt.Printf("Created token: %s\n", token.TokenID.String())
return nil
},
}
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
cc.Flags().IntVar(&teams, "teams", 5, "number of teams to generate")
cc.Flags().IntVar(&projects, "projects", 10, "number of projects to create per team (personal projects are included)")
cc.Flags().IntVar(&taskGroups, "task_groups", 5, "number of task groups to generate per project")
cc.Flags().IntVar(&tasks, "tasks", 25, "number of tasks to generate per task group")
viper.SetDefault("migrate", false)
return cc
}

View File

@ -1,20 +1,21 @@
package commands package commands
import ( import (
"fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/RichardKnop/machinery/v1"
mTasks "github.com/RichardKnop/machinery/v1/tasks"
"github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/httpfs" "github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/google/uuid"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/route" "github.com/jordanknott/taskcafe/internal/route"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -32,19 +33,15 @@ func newWebCmd() *cobra.Command {
log.SetFormatter(Formatter) log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
appConfig, err := config.GetAppConfig() connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=disable",
if err != nil { viper.GetString("database.user"),
return err viper.GetString("database.password"),
} viper.GetString("database.host"),
viper.GetString("database.name"),
redisClient, err := appConfig.MessageQueue.GetMessageQueueClient() viper.GetString("database.port"),
if err != nil { )
return err
}
defer redisClient.Close()
connection := appConfig.Database.GetDatabaseConnectionUri()
var db *sqlx.DB var db *sqlx.DB
var err error
var retryDuration time.Duration var retryDuration time.Duration
maxRetryNumber := 4 maxRetryNumber := 4
for i := 0; i < maxRetryNumber; i++ { for i := 0; i < maxRetryNumber; i++ {
@ -73,26 +70,36 @@ func newWebCmd() *cobra.Command {
} }
} }
var server *machinery.Server secret := viper.GetString("server.secret")
jobConfig := appConfig.Job.GetJobConfig() if strings.TrimSpace(secret) == "" {
server, err = machinery.NewServer(&jobConfig) log.Warn("server.secret is not set, generating a random secret")
if err != nil { secret = uuid.New().String()
return err
} }
signature := &mTasks.Signature{ security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret))
Name: "scheduleDueDateNotifications", r, _ := route.NewRouter(db, utils.EmailConfig{
} From: viper.GetString("smtp.from"),
server.SendTask(signature) Host: viper.GetString("smtp.host"),
Port: viper.GetInt("smtp.port"),
r, _ := route.NewRouter(db, redisClient, server, appConfig) Username: viper.GetString("smtp.username"),
Password: viper.GetString("smtp.password"),
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
}, security)
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server") log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
return http.ListenAndServe(viper.GetString("server.hostname"), r) return http.ListenAndServe(viper.GetString("server.hostname"), r)
}, },
} }
viper.SetDefault("smtp.from", "no-reply@example.com")
viper.SetDefault("smtp.host", "localhost")
viper.SetDefault("smtp.port", 587)
viper.SetDefault("smtp.username", "")
viper.SetDefault("smtp.password", "")
viper.SetDefault("smtp.skip_verify", false)
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server") cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate")) viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
viper.SetDefault("migrate", false) viper.SetDefault("migrate", false)
return cc return cc
} }

View File

@ -1,16 +1,18 @@
package commands package commands
import ( import (
"fmt"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/RichardKnop/machinery/v1" "github.com/RichardKnop/machinery/v1"
"github.com/RichardKnop/machinery/v1/config"
queueLog "github.com/RichardKnop/machinery/v1/log" queueLog "github.com/RichardKnop/machinery/v1/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
repo "github.com/jordanknott/taskcafe/internal/db" repo "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/jobs" "github.com/jordanknott/taskcafe/internal/notification"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -26,11 +28,13 @@ func newWorkerCmd() *cobra.Command {
log.SetFormatter(Formatter) log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
appConfig, err := config.GetAppConfig() connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
if err != nil { viper.GetString("database.user"),
log.Panic(err) viper.GetString("database.password"),
} viper.GetString("database.host"),
db, err := sqlx.Connect("postgres", config.GetDatabaseConfig().GetDatabaseConnectionUri()) viper.GetString("database.name"),
)
db, err := sqlx.Connect("postgres", connection)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
@ -39,19 +43,25 @@ func newWorkerCmd() *cobra.Command {
db.SetConnMaxLifetime(5 * time.Minute) db.SetConnMaxLifetime(5 * time.Minute)
defer db.Close() defer db.Close()
var cnf = &config.Config{
Broker: viper.GetString("queue.broker"),
DefaultQueue: "machinery_tasks",
ResultBackend: viper.GetString("queue.store"),
AMQP: &config.AMQPConfig{
Exchange: "machinery_exchange",
ExchangeType: "direct",
BindingKey: "machinery_task",
},
}
log.Info("starting task queue server instance") log.Info("starting task queue server instance")
jobConfig := appConfig.Job.GetJobConfig() server, err := machinery.NewServer(cnf)
server, err := machinery.NewServer(&jobConfig)
if err != nil { if err != nil {
// do something with the error // do something with the error
} }
queueLog.Set(&jobs.MachineryLogger{}) queueLog.Set(&notification.MachineryLogger{})
repo := *repo.NewRepository(db) repo := *repo.NewRepository(db)
redisClient, err := appConfig.MessageQueue.GetMessageQueueClient() notification.RegisterTasks(server, repo)
if err != nil {
return err
}
jobs.RegisterTasks(server, repo, appConfig, redisClient)
worker := server.NewWorker("taskcafe_worker", 10) worker := server.NewWorker("taskcafe_worker", 10)
log.Info("starting task queue worker") log.Info("starting task queue worker")

View File

@ -1,217 +0,0 @@
package config
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/go-redis/redis/v8"
mConfig "github.com/RichardKnop/machinery/v1/config"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
const (
ServerHostname = "server.hostname"
DatabaseHost = "database.host"
DatabaseName = "database.name"
DatabaseUser = "database.user"
DatabasePassword = "database.password"
DatabasePort = "database.port"
DatabaseSslMode = "database.sslmode"
SecurityTokenExpiration = "security.token_expiration"
SecuritySecret = "security.secret"
JobEnabled = "job.enabled"
JobBroker = "job.broker"
JobStore = "job.store"
JobQueueName = "job.queue_name"
MessageQueue = "message.queue"
SmtpFrom = "smtp.from"
SmtpHost = "smtp.host"
SmtpPort = "smtp.port"
SmtpUsername = "smtp.username"
SmtpPassword = "smtp.password"
SmtpSkipVerify = "false"
)
var defaults = map[string]interface{}{
ServerHostname: "0.0.0.0:3333",
DatabaseHost: "127.0.0.1",
DatabaseName: "taskcafe",
DatabaseUser: "taskcafe",
DatabasePassword: "taskcafe_test",
DatabasePort: "5432",
DatabaseSslMode: "disable",
SecurityTokenExpiration: "15m",
SecuritySecret: "",
MessageQueue: "localhost:6379",
JobEnabled: false,
JobBroker: "redis://localhost:6379",
JobStore: "redis://localhost:6379",
JobQueueName: "taskcafe_tasks",
SmtpFrom: "no-reply@example.com",
SmtpHost: "localhost",
SmtpPort: "587",
SmtpUsername: "",
SmtpPassword: "",
SmtpSkipVerify: false,
}
func InitDefaults() {
for key, value := range defaults {
viper.SetDefault(key, value)
}
}
type AppConfig struct {
Email EmailConfig
Security SecurityConfig
Database DatabaseConfig
Job JobConfig
MessageQueue MessageQueueConfig
}
type MessageQueueConfig struct {
URI string
}
type JobConfig struct {
Enabled bool
Broker string
QueueName string
Store string
}
func GetJobConfig() JobConfig {
return JobConfig{
Enabled: viper.GetBool(JobEnabled),
Broker: viper.GetString(JobBroker),
QueueName: viper.GetString(JobQueueName),
Store: viper.GetString(JobStore),
}
}
func (cfg *JobConfig) GetJobConfig() mConfig.Config {
return mConfig.Config{
Broker: cfg.Broker,
DefaultQueue: cfg.QueueName,
ResultBackend: cfg.Store,
/*
AMQP: &mConfig.AMQPConfig{
Exchange: "machinery_exchange",
ExchangeType: "direct",
BindingKey: "machinery_task",
} */
}
}
type EmailConfig struct {
Host string
Port int
From string
Username string
Password string
SiteURL string
InsecureSkipVerify bool
}
type DatabaseConfig struct {
Host string
Port string
Name string
Username string
Password string
SslMode string
}
func (cfg DatabaseConfig) GetDatabaseConnectionUri() string {
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=%s",
cfg.Username,
cfg.Password,
cfg.Host,
cfg.Name,
cfg.Port,
cfg.SslMode,
)
return connection
}
type SecurityConfig struct {
AccessTokenExpiration time.Duration
Secret []byte
}
func GetAppConfig() (AppConfig, error) {
secret := viper.GetString(SecuritySecret)
if strings.TrimSpace(secret) == "" {
log.Warn("server.secret is not set, generating a random secret")
secret = uuid.New().String()
}
securityCfg, err := GetSecurityConfig(viper.GetString(SecurityTokenExpiration), []byte(secret))
if err != nil {
return AppConfig{}, err
}
jobCfg := GetJobConfig()
databaseCfg := GetDatabaseConfig()
emailCfg := GetEmailConfig()
messageCfg := MessageQueueConfig{URI: viper.GetString("message.queue")}
return AppConfig{
Email: emailCfg,
Security: securityCfg,
Database: databaseCfg,
Job: jobCfg,
MessageQueue: messageCfg,
}, err
}
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {
exp, err := time.ParseDuration(accessTokenExp)
if err != nil {
log.WithError(err).Error("issue parsing duration")
return SecurityConfig{}, err
}
return SecurityConfig{AccessTokenExpiration: exp, Secret: secret}, nil
}
func (c MessageQueueConfig) GetMessageQueueClient() (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: c.URI,
})
_, err := client.Ping(context.Background()).Result()
if !errors.Is(err, nil) {
return nil, err
}
return client, nil
}
func GetEmailConfig() EmailConfig {
return EmailConfig{
From: viper.GetString(SmtpFrom),
Host: viper.GetString(SmtpHost),
Port: viper.GetInt(SmtpPort),
Username: viper.GetString(SmtpUsername),
Password: viper.GetString(SmtpPassword),
InsecureSkipVerify: viper.GetBool(SmtpSkipVerify),
}
}
func GetDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
Username: viper.GetString(DatabaseUser),
Password: viper.GetString(DatabasePassword),
Port: viper.GetString(DatabasePort),
SslMode: viper.GetString(DatabaseSslMode),
Name: viper.GetString(DatabaseName),
Host: viper.GetString(DatabaseHost),
}
}

View File

@ -10,34 +10,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type AccountSetting struct {
AccountSettingID string `json:"account_setting_id"`
Constrained bool `json:"constrained"`
DataType string `json:"data_type"`
ConstrainedDefaultValue sql.NullString `json:"constrained_default_value"`
UnconstrainedDefaultValue sql.NullString `json:"unconstrained_default_value"`
}
type AccountSettingAllowedValue struct {
AllowedValueID uuid.UUID `json:"allowed_value_id"`
SettingID int32 `json:"setting_id"`
ItemValue string `json:"item_value"`
}
type AccountSettingDataType struct {
DataTypeID string `json:"data_type_id"`
}
type AccountSettingValue struct {
AccountSettingID uuid.UUID `json:"account_setting_id"`
UserID uuid.UUID `json:"user_id"`
SettingID int32 `json:"setting_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AllowedValueID uuid.UUID `json:"allowed_value_id"`
UnconstrainedValue sql.NullString `json:"unconstrained_value"`
}
type AuthToken struct { type AuthToken struct {
TokenID uuid.UUID `json:"token_id"` TokenID uuid.UUID `json:"token_id"`
UserID uuid.UUID `json:"user_id"` UserID uuid.UUID `json:"user_id"`
@ -54,18 +26,18 @@ type LabelColor struct {
type Notification struct { type Notification struct {
NotificationID uuid.UUID `json:"notification_id"` NotificationID uuid.UUID `json:"notification_id"`
CausedBy uuid.UUID `json:"caused_by"` NotificationObjectID uuid.UUID `json:"notification_object_id"`
ActionType string `json:"action_type"` NotifierID uuid.UUID `json:"notifier_id"`
Data json.RawMessage `json:"data"` Read bool `json:"read"`
CreatedOn time.Time `json:"created_on"`
} }
type NotificationNotified struct { type NotificationObject struct {
NotifiedID uuid.UUID `json:"notified_id"` NotificationObjectID uuid.UUID `json:"notification_object_id"`
NotificationID uuid.UUID `json:"notification_id"` EntityID uuid.UUID `json:"entity_id"`
UserID uuid.UUID `json:"user_id"` ActionType int32 `json:"action_type"`
Read bool `json:"read"` ActorID uuid.UUID `json:"actor_id"`
ReadAt sql.NullTime `json:"read_at"` EntityType int32 `json:"entity_type"`
CreatedOn time.Time `json:"created_on"`
} }
type Organization struct { type Organization struct {
@ -86,7 +58,6 @@ type Project struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Name string `json:"name"` Name string `json:"name"`
PublicOn sql.NullTime `json:"public_on"` PublicOn sql.NullTime `json:"public_on"`
ShortID string `json:"short_id"`
} }
type ProjectLabel struct { type ProjectLabel struct {
@ -133,7 +104,6 @@ type Task struct {
Complete bool `json:"complete"` Complete bool `json:"complete"`
CompletedAt sql.NullTime `json:"completed_at"` CompletedAt sql.NullTime `json:"completed_at"`
HasTime bool `json:"has_time"` HasTime bool `json:"has_time"`
ShortID string `json:"short_id"`
} }
type TaskActivity struct { type TaskActivity struct {
@ -187,18 +157,6 @@ type TaskComment struct {
Message string `json:"message"` Message string `json:"message"`
} }
type TaskDueDateReminder struct {
DueDateReminderID uuid.UUID `json:"due_date_reminder_id"`
TaskID uuid.UUID `json:"task_id"`
Period int32 `json:"period"`
Duration string `json:"duration"`
RemindAt time.Time `json:"remind_at"`
}
type TaskDueDateReminderDuration struct {
Code string `json:"code"`
}
type TaskGroup struct { type TaskGroup struct {
TaskGroupID uuid.UUID `json:"task_group_id"` TaskGroupID uuid.UUID `json:"task_group_id"`
ProjectID uuid.UUID `json:"project_id"` ProjectID uuid.UUID `json:"project_id"`
@ -214,13 +172,6 @@ type TaskLabel struct {
AssignedDate time.Time `json:"assigned_date"` AssignedDate time.Time `json:"assigned_date"`
} }
type TaskWatcher struct {
TaskWatcherID uuid.UUID `json:"task_watcher_id"`
TaskID uuid.UUID `json:"task_id"`
UserID uuid.UUID `json:"user_id"`
WatchedAt time.Time `json:"watched_at"`
}
type Team struct { type Team struct {
TeamID uuid.UUID `json:"team_id"` TeamID uuid.UUID `json:"team_id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`

View File

@ -5,198 +5,86 @@ package db
import ( import (
"context" "context"
"database/sql"
"encoding/json"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lib/pq"
) )
const createNotification = `-- name: CreateNotification :one const createNotification = `-- name: CreateNotification :one
INSERT INTO notification (caused_by, data, action_type, created_on) INSERT INTO notification(notification_object_id, notifier_id)
VALUES ($1, $2, $3, $4) RETURNING notification_id, caused_by, action_type, data, created_on VALUES ($1, $2) RETURNING notification_id, notification_object_id, notifier_id, read
` `
type CreateNotificationParams struct { type CreateNotificationParams struct {
CausedBy uuid.UUID `json:"caused_by"` NotificationObjectID uuid.UUID `json:"notification_object_id"`
Data json.RawMessage `json:"data"` NotifierID uuid.UUID `json:"notifier_id"`
ActionType string `json:"action_type"`
CreatedOn time.Time `json:"created_on"`
} }
func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) { func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) {
row := q.db.QueryRowContext(ctx, createNotification, row := q.db.QueryRowContext(ctx, createNotification, arg.NotificationObjectID, arg.NotifierID)
arg.CausedBy,
arg.Data,
arg.ActionType,
arg.CreatedOn,
)
var i Notification var i Notification
err := row.Scan( err := row.Scan(
&i.NotificationID, &i.NotificationID,
&i.CausedBy, &i.NotificationObjectID,
&i.ActionType, &i.NotifierID,
&i.Data, &i.Read,
&i.CreatedOn,
) )
return i, err return i, err
} }
const createNotificationNotifed = `-- name: CreateNotificationNotifed :one const createNotificationObject = `-- name: CreateNotificationObject :one
INSERT INTO notification_notified (notification_id, user_id) VALUES ($1, $2) RETURNING notified_id, notification_id, user_id, read, read_at INSERT INTO notification_object(entity_type, action_type, entity_id, created_on, actor_id)
VALUES ($1, $2, $3, $4, $5) RETURNING notification_object_id, entity_id, action_type, actor_id, entity_type, created_on
` `
type CreateNotificationNotifedParams struct { type CreateNotificationObjectParams struct {
NotificationID uuid.UUID `json:"notification_id"` EntityType int32 `json:"entity_type"`
UserID uuid.UUID `json:"user_id"` ActionType int32 `json:"action_type"`
EntityID uuid.UUID `json:"entity_id"`
CreatedOn time.Time `json:"created_on"`
ActorID uuid.UUID `json:"actor_id"`
} }
func (q *Queries) CreateNotificationNotifed(ctx context.Context, arg CreateNotificationNotifedParams) (NotificationNotified, error) { func (q *Queries) CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error) {
row := q.db.QueryRowContext(ctx, createNotificationNotifed, arg.NotificationID, arg.UserID) row := q.db.QueryRowContext(ctx, createNotificationObject,
var i NotificationNotified arg.EntityType,
arg.ActionType,
arg.EntityID,
arg.CreatedOn,
arg.ActorID,
)
var i NotificationObject
err := row.Scan( err := row.Scan(
&i.NotifiedID, &i.NotificationObjectID,
&i.NotificationID, &i.EntityID,
&i.UserID, &i.ActionType,
&i.Read, &i.ActorID,
&i.ReadAt, &i.EntityType,
&i.CreatedOn,
) )
return i, err return i, err
} }
const getAllNotificationsForUserID = `-- name: GetAllNotificationsForUserID :many const getAllNotificationsForUserID = `-- name: GetAllNotificationsForUserID :many
SELECT notified_id, nn.notification_id, user_id, read, read_at, n.notification_id, caused_by, action_type, data, created_on FROM notification_notified AS nn SELECT n.notification_id, n.notification_object_id, n.notifier_id, n.read FROM notification as n
INNER JOIN notification AS n ON n.notification_id = nn.notification_id INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE nn.user_id = $1 WHERE n.notifier_id = $1 ORDER BY no.created_on DESC
` `
type GetAllNotificationsForUserIDRow struct { func (q *Queries) GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error) {
NotifiedID uuid.UUID `json:"notified_id"` rows, err := q.db.QueryContext(ctx, getAllNotificationsForUserID, notifierID)
NotificationID uuid.UUID `json:"notification_id"`
UserID uuid.UUID `json:"user_id"`
Read bool `json:"read"`
ReadAt sql.NullTime `json:"read_at"`
NotificationID_2 uuid.UUID `json:"notification_id_2"`
CausedBy uuid.UUID `json:"caused_by"`
ActionType string `json:"action_type"`
Data json.RawMessage `json:"data"`
CreatedOn time.Time `json:"created_on"`
}
func (q *Queries) GetAllNotificationsForUserID(ctx context.Context, userID uuid.UUID) ([]GetAllNotificationsForUserIDRow, error) {
rows, err := q.db.QueryContext(ctx, getAllNotificationsForUserID, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []GetAllNotificationsForUserIDRow var items []Notification
for rows.Next() { for rows.Next() {
var i GetAllNotificationsForUserIDRow
if err := rows.Scan(
&i.NotifiedID,
&i.NotificationID,
&i.UserID,
&i.Read,
&i.ReadAt,
&i.NotificationID_2,
&i.CausedBy,
&i.ActionType,
&i.Data,
&i.CreatedOn,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getNotificationByID = `-- name: GetNotificationByID :one
SELECT notification_id, caused_by, action_type, data, created_on FROM notification WHERE notification_id = $1
`
func (q *Queries) GetNotificationByID(ctx context.Context, notificationID uuid.UUID) (Notification, error) {
row := q.db.QueryRowContext(ctx, getNotificationByID, notificationID)
var i Notification var i Notification
err := row.Scan(
&i.NotificationID,
&i.CausedBy,
&i.ActionType,
&i.Data,
&i.CreatedOn,
)
return i, err
}
const getNotificationsForUserIDCursor = `-- name: GetNotificationsForUserIDCursor :many
SELECT n.notification_id, n.caused_by, n.action_type, n.data, n.created_on, nn.notified_id, nn.notification_id, nn.user_id, nn.read, nn.read_at FROM notification_notified AS nn
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
WHERE (n.created_on, n.notification_id) < ($1::timestamptz, $2::uuid)
AND nn.user_id = $3::uuid
AND ($4::boolean = false OR nn.read = false)
AND ($5::boolean = false OR n.action_type = ANY($6::text[]))
ORDER BY n.created_on DESC
LIMIT $7::int
`
type GetNotificationsForUserIDCursorParams struct {
CreatedOn time.Time `json:"created_on"`
NotificationID uuid.UUID `json:"notification_id"`
UserID uuid.UUID `json:"user_id"`
EnableUnread bool `json:"enable_unread"`
EnableActionType bool `json:"enable_action_type"`
ActionType []string `json:"action_type"`
LimitRows int32 `json:"limit_rows"`
}
type GetNotificationsForUserIDCursorRow struct {
NotificationID uuid.UUID `json:"notification_id"`
CausedBy uuid.UUID `json:"caused_by"`
ActionType string `json:"action_type"`
Data json.RawMessage `json:"data"`
CreatedOn time.Time `json:"created_on"`
NotifiedID uuid.UUID `json:"notified_id"`
NotificationID_2 uuid.UUID `json:"notification_id_2"`
UserID uuid.UUID `json:"user_id"`
Read bool `json:"read"`
ReadAt sql.NullTime `json:"read_at"`
}
func (q *Queries) GetNotificationsForUserIDCursor(ctx context.Context, arg GetNotificationsForUserIDCursorParams) ([]GetNotificationsForUserIDCursorRow, error) {
rows, err := q.db.QueryContext(ctx, getNotificationsForUserIDCursor,
arg.CreatedOn,
arg.NotificationID,
arg.UserID,
arg.EnableUnread,
arg.EnableActionType,
pq.Array(arg.ActionType),
arg.LimitRows,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetNotificationsForUserIDCursorRow
for rows.Next() {
var i GetNotificationsForUserIDCursorRow
if err := rows.Scan( if err := rows.Scan(
&i.NotificationID, &i.NotificationID,
&i.CausedBy, &i.NotificationObjectID,
&i.ActionType, &i.NotifierID,
&i.Data,
&i.CreatedOn,
&i.NotifiedID,
&i.NotificationID_2,
&i.UserID,
&i.Read, &i.Read,
&i.ReadAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -211,173 +99,79 @@ func (q *Queries) GetNotificationsForUserIDCursor(ctx context.Context, arg GetNo
return items, nil return items, nil
} }
const getNotificationsForUserIDPaged = `-- name: GetNotificationsForUserIDPaged :many const getEntityForNotificationID = `-- name: GetEntityForNotificationID :one
SELECT n.notification_id, n.caused_by, n.action_type, n.data, n.created_on, nn.notified_id, nn.notification_id, nn.user_id, nn.read, nn.read_at FROM notification_notified AS nn SELECT no.created_on, no.entity_id, no.entity_type, no.action_type, no.actor_id FROM notification as n
INNER JOIN notification AS n ON n.notification_id = nn.notification_id INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE nn.user_id = $1::uuid WHERE n.notification_id = $1
AND ($2::boolean = false OR nn.read = false)
AND ($3::boolean = false OR n.action_type = ANY($4::text[]))
ORDER BY n.created_on DESC
LIMIT $5::int
` `
type GetNotificationsForUserIDPagedParams struct { type GetEntityForNotificationIDRow struct {
UserID uuid.UUID `json:"user_id"`
EnableUnread bool `json:"enable_unread"`
EnableActionType bool `json:"enable_action_type"`
ActionType []string `json:"action_type"`
LimitRows int32 `json:"limit_rows"`
}
type GetNotificationsForUserIDPagedRow struct {
NotificationID uuid.UUID `json:"notification_id"`
CausedBy uuid.UUID `json:"caused_by"`
ActionType string `json:"action_type"`
Data json.RawMessage `json:"data"`
CreatedOn time.Time `json:"created_on"` CreatedOn time.Time `json:"created_on"`
NotifiedID uuid.UUID `json:"notified_id"` EntityID uuid.UUID `json:"entity_id"`
NotificationID_2 uuid.UUID `json:"notification_id_2"` EntityType int32 `json:"entity_type"`
UserID uuid.UUID `json:"user_id"` ActionType int32 `json:"action_type"`
Read bool `json:"read"` ActorID uuid.UUID `json:"actor_id"`
ReadAt sql.NullTime `json:"read_at"`
} }
func (q *Queries) GetNotificationsForUserIDPaged(ctx context.Context, arg GetNotificationsForUserIDPagedParams) ([]GetNotificationsForUserIDPagedRow, error) { func (q *Queries) GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error) {
rows, err := q.db.QueryContext(ctx, getNotificationsForUserIDPaged, row := q.db.QueryRowContext(ctx, getEntityForNotificationID, notificationID)
arg.UserID, var i GetEntityForNotificationIDRow
arg.EnableUnread,
arg.EnableActionType,
pq.Array(arg.ActionType),
arg.LimitRows,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetNotificationsForUserIDPagedRow
for rows.Next() {
var i GetNotificationsForUserIDPagedRow
if err := rows.Scan(
&i.NotificationID,
&i.CausedBy,
&i.ActionType,
&i.Data,
&i.CreatedOn,
&i.NotifiedID,
&i.NotificationID_2,
&i.UserID,
&i.Read,
&i.ReadAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getNotifiedByID = `-- name: GetNotifiedByID :one
SELECT notified_id, nn.notification_id, user_id, read, read_at, n.notification_id, caused_by, action_type, data, created_on FROM notification_notified as nn
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
WHERE notified_id = $1
`
type GetNotifiedByIDRow struct {
NotifiedID uuid.UUID `json:"notified_id"`
NotificationID uuid.UUID `json:"notification_id"`
UserID uuid.UUID `json:"user_id"`
Read bool `json:"read"`
ReadAt sql.NullTime `json:"read_at"`
NotificationID_2 uuid.UUID `json:"notification_id_2"`
CausedBy uuid.UUID `json:"caused_by"`
ActionType string `json:"action_type"`
Data json.RawMessage `json:"data"`
CreatedOn time.Time `json:"created_on"`
}
func (q *Queries) GetNotifiedByID(ctx context.Context, notifiedID uuid.UUID) (GetNotifiedByIDRow, error) {
row := q.db.QueryRowContext(ctx, getNotifiedByID, notifiedID)
var i GetNotifiedByIDRow
err := row.Scan( err := row.Scan(
&i.NotifiedID,
&i.NotificationID,
&i.UserID,
&i.Read,
&i.ReadAt,
&i.NotificationID_2,
&i.CausedBy,
&i.ActionType,
&i.Data,
&i.CreatedOn, &i.CreatedOn,
&i.EntityID,
&i.EntityType,
&i.ActionType,
&i.ActorID,
) )
return i, err return i, err
} }
const getNotifiedByIDNoExtra = `-- name: GetNotifiedByIDNoExtra :one const getEntityIDForNotificationID = `-- name: GetEntityIDForNotificationID :one
SELECT notified_id, notification_id, user_id, read, read_at FROM notification_notified as nn WHERE nn.notified_id = $1 SELECT no.entity_id FROM notification as n
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE n.notification_id = $1
` `
func (q *Queries) GetNotifiedByIDNoExtra(ctx context.Context, notifiedID uuid.UUID) (NotificationNotified, error) { func (q *Queries) GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getNotifiedByIDNoExtra, notifiedID) row := q.db.QueryRowContext(ctx, getEntityIDForNotificationID, notificationID)
var i NotificationNotified var entity_id uuid.UUID
err := row.Scan(&entity_id)
return entity_id, err
}
const getNotificationForNotificationID = `-- name: GetNotificationForNotificationID :one
SELECT n.notification_id, n.notification_object_id, n.notifier_id, n.read, no.notification_object_id, no.entity_id, no.action_type, no.actor_id, no.entity_type, no.created_on FROM notification as n
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE n.notification_id = $1
`
type GetNotificationForNotificationIDRow struct {
NotificationID uuid.UUID `json:"notification_id"`
NotificationObjectID uuid.UUID `json:"notification_object_id"`
NotifierID uuid.UUID `json:"notifier_id"`
Read bool `json:"read"`
NotificationObjectID_2 uuid.UUID `json:"notification_object_id_2"`
EntityID uuid.UUID `json:"entity_id"`
ActionType int32 `json:"action_type"`
ActorID uuid.UUID `json:"actor_id"`
EntityType int32 `json:"entity_type"`
CreatedOn time.Time `json:"created_on"`
}
func (q *Queries) GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error) {
row := q.db.QueryRowContext(ctx, getNotificationForNotificationID, notificationID)
var i GetNotificationForNotificationIDRow
err := row.Scan( err := row.Scan(
&i.NotifiedID,
&i.NotificationID, &i.NotificationID,
&i.UserID, &i.NotificationObjectID,
&i.NotifierID,
&i.Read, &i.Read,
&i.ReadAt, &i.NotificationObjectID_2,
&i.EntityID,
&i.ActionType,
&i.ActorID,
&i.EntityType,
&i.CreatedOn,
) )
return i, err return i, err
} }
const hasUnreadNotification = `-- name: HasUnreadNotification :one
SELECT EXISTS (SELECT 1 FROM notification_notified WHERE read = false AND user_id = $1)
`
func (q *Queries) HasUnreadNotification(ctx context.Context, userID uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, hasUnreadNotification, userID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const markAllNotificationsRead = `-- name: MarkAllNotificationsRead :exec
UPDATE notification_notified SET read = true, read_at = $2 WHERE user_id = $1
`
type MarkAllNotificationsReadParams struct {
UserID uuid.UUID `json:"user_id"`
ReadAt sql.NullTime `json:"read_at"`
}
func (q *Queries) MarkAllNotificationsRead(ctx context.Context, arg MarkAllNotificationsReadParams) error {
_, err := q.db.ExecContext(ctx, markAllNotificationsRead, arg.UserID, arg.ReadAt)
return err
}
const markNotificationAsRead = `-- name: MarkNotificationAsRead :exec
UPDATE notification_notified SET read = $3, read_at = $2 WHERE user_id = $1 AND notified_id = $4
`
type MarkNotificationAsReadParams struct {
UserID uuid.UUID `json:"user_id"`
ReadAt sql.NullTime `json:"read_at"`
Read bool `json:"read"`
NotifiedID uuid.UUID `json:"notified_id"`
}
func (q *Queries) MarkNotificationAsRead(ctx context.Context, arg MarkNotificationAsReadParams) error {
_, err := q.db.ExecContext(ctx, markNotificationAsRead,
arg.UserID,
arg.ReadAt,
arg.Read,
arg.NotifiedID,
)
return err
}

View File

@ -12,7 +12,7 @@ import (
) )
const createPersonalProject = `-- name: CreatePersonalProject :one const createPersonalProject = `-- name: CreatePersonalProject :one
INSERT INTO project(team_id, created_at, name) VALUES (null, $1, $2) RETURNING project_id, team_id, created_at, name, public_on, short_id INSERT INTO project(team_id, created_at, name) VALUES (null, $1, $2) RETURNING project_id, team_id, created_at, name, public_on
` `
type CreatePersonalProjectParams struct { type CreatePersonalProjectParams struct {
@ -29,7 +29,6 @@ func (q *Queries) CreatePersonalProject(ctx context.Context, arg CreatePersonalP
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
) )
return i, err return i, err
} }
@ -81,7 +80,7 @@ func (q *Queries) CreateProjectMember(ctx context.Context, arg CreateProjectMemb
} }
const createTeamProject = `-- name: CreateTeamProject :one const createTeamProject = `-- name: CreateTeamProject :one
INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING project_id, team_id, created_at, name, public_on, short_id INSERT INTO project(team_id, created_at, name) VALUES ($1, $2, $3) RETURNING project_id, team_id, created_at, name, public_on
` `
type CreateTeamProjectParams struct { type CreateTeamProjectParams struct {
@ -99,7 +98,6 @@ func (q *Queries) CreateTeamProject(ctx context.Context, arg CreateTeamProjectPa
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
) )
return i, err return i, err
} }
@ -137,7 +135,7 @@ func (q *Queries) DeleteProjectMember(ctx context.Context, arg DeleteProjectMemb
} }
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
SELECT project_id, team_id, created_at, name, public_on, short_id FROM project WHERE team_id = $1 SELECT project_id, team_id, created_at, name, public_on FROM project WHERE team_id = $1
` `
func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) { func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) {
@ -155,7 +153,6 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -171,7 +168,7 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) (
} }
const getAllTeamProjects = `-- name: GetAllTeamProjects :many const getAllTeamProjects = `-- name: GetAllTeamProjects :many
SELECT project_id, team_id, created_at, name, public_on, short_id FROM project WHERE team_id IS NOT null SELECT project_id, team_id, created_at, name, public_on FROM project WHERE team_id IS NOT null
` `
func (q *Queries) GetAllTeamProjects(ctx context.Context) ([]Project, error) { func (q *Queries) GetAllTeamProjects(ctx context.Context) ([]Project, error) {
@ -189,7 +186,6 @@ func (q *Queries) GetAllTeamProjects(ctx context.Context) ([]Project, error) {
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -205,7 +201,7 @@ func (q *Queries) GetAllTeamProjects(ctx context.Context) ([]Project, error) {
} }
const getAllVisibleProjectsForUserID = `-- name: GetAllVisibleProjectsForUserID :many const getAllVisibleProjectsForUserID = `-- name: GetAllVisibleProjectsForUserID :many
SELECT project.project_id, project.team_id, project.created_at, project.name, project.public_on, project.short_id FROM project LEFT JOIN SELECT project.project_id, project.team_id, project.created_at, project.name, project.public_on FROM project LEFT JOIN
project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1 project_member ON project_member.project_id = project.project_id WHERE project_member.user_id = $1
` `
@ -224,7 +220,6 @@ func (q *Queries) GetAllVisibleProjectsForUserID(ctx context.Context, userID uui
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -303,7 +298,7 @@ func (q *Queries) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.
} }
const getPersonalProjectsForUserID = `-- name: GetPersonalProjectsForUserID :many const getPersonalProjectsForUserID = `-- name: GetPersonalProjectsForUserID :many
SELECT project.project_id, project.team_id, project.created_at, project.name, project.public_on, project.short_id FROM project SELECT project.project_id, project.team_id, project.created_at, project.name, project.public_on FROM project
LEFT JOIN personal_project ON personal_project.project_id = project.project_id LEFT JOIN personal_project ON personal_project.project_id = project.project_id
WHERE personal_project.user_id = $1 WHERE personal_project.user_id = $1
` `
@ -323,7 +318,6 @@ func (q *Queries) GetPersonalProjectsForUserID(ctx context.Context, userID uuid.
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -339,7 +333,7 @@ func (q *Queries) GetPersonalProjectsForUserID(ctx context.Context, userID uuid.
} }
const getProjectByID = `-- name: GetProjectByID :one const getProjectByID = `-- name: GetProjectByID :one
SELECT project_id, team_id, created_at, name, public_on, short_id FROM project WHERE project_id = $1 SELECT project_id, team_id, created_at, name, public_on FROM project WHERE project_id = $1
` `
func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) { func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) {
@ -351,22 +345,10 @@ func (q *Queries) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Proj
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
) )
return i, err return i, err
} }
const getProjectIDByShortID = `-- name: GetProjectIDByShortID :one
SELECT project_id FROM project WHERE short_id = $1
`
func (q *Queries) GetProjectIDByShortID(ctx context.Context, shortID string) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getProjectIDByShortID, shortID)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getProjectMemberInvitedIDByEmail = `-- name: GetProjectMemberInvitedIDByEmail :one const getProjectMemberInvitedIDByEmail = `-- name: GetProjectMemberInvitedIDByEmail :one
SELECT email, invited_on, project_member_invited_id FROM user_account_invited AS uai SELECT email, invited_on, project_member_invited_id FROM user_account_invited AS uai
inner join project_member_invited AS pmi inner join project_member_invited AS pmi
@ -507,7 +489,7 @@ func (q *Queries) GetUserRolesForProject(ctx context.Context, arg GetUserRolesFo
} }
const setPublicOn = `-- name: SetPublicOn :one const setPublicOn = `-- name: SetPublicOn :one
UPDATE project SET public_on = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, public_on, short_id UPDATE project SET public_on = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, public_on
` `
type SetPublicOnParams struct { type SetPublicOnParams struct {
@ -524,7 +506,6 @@ func (q *Queries) SetPublicOn(ctx context.Context, arg SetPublicOnParams) (Proje
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
) )
return i, err return i, err
} }
@ -554,7 +535,7 @@ func (q *Queries) UpdateProjectMemberRole(ctx context.Context, arg UpdateProject
} }
const updateProjectNameByID = `-- name: UpdateProjectNameByID :one const updateProjectNameByID = `-- name: UpdateProjectNameByID :one
UPDATE project SET name = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, public_on, short_id UPDATE project SET name = $2 WHERE project_id = $1 RETURNING project_id, team_id, created_at, name, public_on
` `
type UpdateProjectNameByIDParams struct { type UpdateProjectNameByIDParams struct {
@ -571,7 +552,6 @@ func (q *Queries) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNa
&i.CreatedAt, &i.CreatedAt,
&i.Name, &i.Name,
&i.PublicOn, &i.PublicOn,
&i.ShortID,
) )
return i, err return i, err
} }

View File

@ -5,7 +5,6 @@ package db
import ( import (
"context" "context"
"database/sql" "database/sql"
"time"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -13,12 +12,11 @@ import (
type Querier interface { type Querier interface {
CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error) CreateAuthToken(ctx context.Context, arg CreateAuthTokenParams) (AuthToken, error)
CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error) CreateConfirmToken(ctx context.Context, email string) (UserAccountConfirmToken, error)
CreateDueDateReminder(ctx context.Context, arg CreateDueDateReminderParams) (TaskDueDateReminder, error)
CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error) CreateInvitedProjectMember(ctx context.Context, arg CreateInvitedProjectMemberParams) (ProjectMemberInvited, error)
CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error) CreateInvitedUser(ctx context.Context, email string) (UserAccountInvited, error)
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error) CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error)
CreateNotificationNotifed(ctx context.Context, arg CreateNotificationNotifedParams) (NotificationNotified, error) CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error)
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
CreatePersonalProject(ctx context.Context, arg CreatePersonalProjectParams) (Project, error) CreatePersonalProject(ctx context.Context, arg CreatePersonalProjectParams) (Project, error)
CreatePersonalProjectLink(ctx context.Context, arg CreatePersonalProjectLinkParams) (PersonalProject, error) CreatePersonalProjectLink(ctx context.Context, arg CreatePersonalProjectLinkParams) (PersonalProject, error)
@ -34,7 +32,6 @@ type Querier interface {
CreateTaskComment(ctx context.Context, arg CreateTaskCommentParams) (TaskComment, error) CreateTaskComment(ctx context.Context, arg CreateTaskCommentParams) (TaskComment, error)
CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error) CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error)
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error) CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherParams) (TaskWatcher, error)
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error) CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error) CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error) CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
@ -42,7 +39,6 @@ type Querier interface {
DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error DeleteAuthTokenByID(ctx context.Context, tokenID uuid.UUID) error
DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error DeleteAuthTokenByUserID(ctx context.Context, userID uuid.UUID) error
DeleteConfirmTokenForEmail(ctx context.Context, email string) error DeleteConfirmTokenForEmail(ctx context.Context, email string) error
DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error
DeleteExpiredTokens(ctx context.Context) error DeleteExpiredTokens(ctx context.Context) error
DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error DeleteInvitedProjectMemberByID(ctx context.Context, projectMemberInvitedID uuid.UUID) error
DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error) DeleteInvitedUserAccount(ctx context.Context, userAccountInvitedID uuid.UUID) (UserAccountInvited, error)
@ -58,15 +54,13 @@ type Querier interface {
DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error) DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error
DeleteTaskLabelForTaskByProjectLabelID(ctx context.Context, arg DeleteTaskLabelForTaskByProjectLabelIDParams) error DeleteTaskLabelForTaskByProjectLabelID(ctx context.Context, arg DeleteTaskLabelForTaskByProjectLabelIDParams) error
DeleteTaskWatcher(ctx context.Context, arg DeleteTaskWatcherParams) error
DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error DeleteUserAccountInvitedForEmail(ctx context.Context, email string) error
DoesUserExist(ctx context.Context, arg DoesUserExistParams) (bool, error)
GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error) GetActivityForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskActivity, error)
GetAllNotificationsForUserID(ctx context.Context, userID uuid.UUID) ([]GetAllNotificationsForUserIDRow, error) GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
GetAllOrganizations(ctx context.Context) ([]Organization, error) GetAllOrganizations(ctx context.Context) ([]Organization, error)
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error) GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error)
@ -79,13 +73,11 @@ type Querier interface {
GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error) GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetAssignedTasksDueDateForUserIDParams) ([]Task, error)
GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetAssignedTasksProjectForUserIDParams) ([]Task, error)
GetAuthTokenByID(ctx context.Context, tokenID uuid.UUID) (AuthToken, error) GetAuthTokenByID(ctx context.Context, tokenID uuid.UUID) (AuthToken, error)
GetCommentCountForTask(ctx context.Context, taskID uuid.UUID) (int64, error)
GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskComment, error)
GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error) GetConfirmTokenByEmail(ctx context.Context, email string) (UserAccountConfirmToken, error)
GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error) GetConfirmTokenByID(ctx context.Context, confirmTokenID uuid.UUID) (UserAccountConfirmToken, error)
GetDueDateReminderByID(ctx context.Context, dueDateReminderID uuid.UUID) (TaskDueDateReminder, error) GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
GetDueDateRemindersForDuration(ctx context.Context, startAt time.Time) ([]TaskDueDateReminder, error) GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error)
GetDueDateRemindersForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskDueDateReminder, error)
GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error) GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error)
GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error) GetInvitedUserAccounts(ctx context.Context) ([]UserAccountInvited, error)
GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error) GetInvitedUserByEmail(ctx context.Context, email string) (UserAccountInvited, error)
@ -95,20 +87,14 @@ type Querier interface {
GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error) GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error)
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetNotificationByID(ctx context.Context, notificationID uuid.UUID) (Notification, error) GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error)
GetNotificationsForUserIDCursor(ctx context.Context, arg GetNotificationsForUserIDCursorParams) ([]GetNotificationsForUserIDCursorRow, error)
GetNotificationsForUserIDPaged(ctx context.Context, arg GetNotificationsForUserIDPagedParams) ([]GetNotificationsForUserIDPagedRow, error)
GetNotifiedByID(ctx context.Context, notifiedID uuid.UUID) (GetNotifiedByIDRow, error)
GetNotifiedByIDNoExtra(ctx context.Context, notifiedID uuid.UUID) (NotificationNotified, error)
GetPersonalProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error) GetPersonalProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetProjectIDByShortID(ctx context.Context, shortID string) (uuid.UUID, error)
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error) GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
GetProjectIDForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) (uuid.UUID, error) GetProjectIDForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) (uuid.UUID, error)
GetProjectIDForTaskChecklistItem(ctx context.Context, taskChecklistItemID uuid.UUID) (uuid.UUID, error) GetProjectIDForTaskChecklistItem(ctx context.Context, taskChecklistItemID uuid.UUID) (uuid.UUID, error)
GetProjectIDForTaskGroup(ctx context.Context, taskGroupID uuid.UUID) (uuid.UUID, error) GetProjectIDForTaskGroup(ctx context.Context, taskGroupID uuid.UUID) (uuid.UUID, error)
GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID) ([]GetProjectIdMappingsRow, error) GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID) ([]GetProjectIdMappingsRow, error)
GetProjectInfoForTask(ctx context.Context, taskID uuid.UUID) (GetProjectInfoForTaskRow, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error) GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error)
@ -126,15 +112,11 @@ type Querier interface {
GetTaskChecklistItemByID(ctx context.Context, taskChecklistItemID uuid.UUID) (TaskChecklistItem, error) GetTaskChecklistItemByID(ctx context.Context, taskChecklistItemID uuid.UUID) (TaskChecklistItem, error)
GetTaskChecklistItemsForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) ([]TaskChecklistItem, error) GetTaskChecklistItemsForTaskChecklist(ctx context.Context, taskChecklistID uuid.UUID) ([]TaskChecklistItem, error)
GetTaskChecklistsForTask(ctx context.Context, taskID uuid.UUID) ([]TaskChecklist, error) GetTaskChecklistsForTask(ctx context.Context, taskID uuid.UUID) ([]TaskChecklist, error)
GetTaskForDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) (Task, error)
GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error) GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error)
GetTaskGroupsForProject(ctx context.Context, projectID uuid.UUID) ([]TaskGroup, error) GetTaskGroupsForProject(ctx context.Context, projectID uuid.UUID) ([]TaskGroup, error)
GetTaskIDByShortID(ctx context.Context, shortID string) (uuid.UUID, error)
GetTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) (TaskLabel, error) GetTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) (TaskLabel, error)
GetTaskLabelForTaskByProjectLabelID(ctx context.Context, arg GetTaskLabelForTaskByProjectLabelIDParams) (TaskLabel, error) GetTaskLabelForTaskByProjectLabelID(ctx context.Context, arg GetTaskLabelForTaskByProjectLabelIDParams) (TaskLabel, error)
GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error) GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error)
GetTaskWatcher(ctx context.Context, arg GetTaskWatcherParams) (TaskWatcher, error)
GetTaskWatchersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskWatcher, error)
GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error) GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
GetTeamMemberByID(ctx context.Context, arg GetTeamMemberByIDParams) (TeamMember, error) GetTeamMemberByID(ctx context.Context, arg GetTeamMemberByIDParams) (TeamMember, error)
@ -150,9 +132,6 @@ type Querier interface {
GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error) GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error)
HasActiveUser(ctx context.Context) (bool, error) HasActiveUser(ctx context.Context) (bool, error)
HasAnyUser(ctx context.Context) (bool, error) HasAnyUser(ctx context.Context) (bool, error)
HasUnreadNotification(ctx context.Context, userID uuid.UUID) (bool, error)
MarkAllNotificationsRead(ctx context.Context, arg MarkAllNotificationsReadParams) error
MarkNotificationAsRead(ctx context.Context, arg MarkNotificationAsReadParams) error
SetFirstUserActive(ctx context.Context) (UserAccount, error) SetFirstUserActive(ctx context.Context) (UserAccount, error)
SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error
SetPublicOn(ctx context.Context, arg SetPublicOnParams) (Project, error) SetPublicOn(ctx context.Context, arg SetPublicOnParams) (Project, error)
@ -161,8 +140,6 @@ type Querier interface {
SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error) SetTaskGroupName(ctx context.Context, arg SetTaskGroupNameParams) (TaskGroup, error)
SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error) SetUserActiveByEmail(ctx context.Context, email string) (UserAccount, error)
SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error) SetUserPassword(ctx context.Context, arg SetUserPasswordParams) (UserAccount, error)
UpdateDueDateReminder(ctx context.Context, arg UpdateDueDateReminderParams) (TaskDueDateReminder, error)
UpdateDueDateReminderRemindAt(ctx context.Context, arg UpdateDueDateReminderRemindAtParams) (TaskDueDateReminder, error)
UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error) UpdateProjectLabel(ctx context.Context, arg UpdateProjectLabelParams) (ProjectLabel, error)
UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error) UpdateProjectLabelColor(ctx context.Context, arg UpdateProjectLabelColorParams) (ProjectLabel, error)
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)

View File

@ -1,50 +1,27 @@
-- name: GetAllNotificationsForUserID :many -- name: GetAllNotificationsForUserID :many
SELECT * FROM notification_notified AS nn SELECT n.* FROM notification as n
INNER JOIN notification AS n ON n.notification_id = nn.notification_id INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE nn.user_id = $1; WHERE n.notifier_id = $1 ORDER BY no.created_on DESC;
-- name: GetNotifiedByID :one -- name: GetNotificationForNotificationID :one
SELECT * FROM notification_notified as nn SELECT n.*, no.* FROM notification as n
INNER JOIN notification AS n ON n.notification_id = nn.notification_id INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE notified_id = $1; WHERE n.notification_id = $1;
-- name: GetNotifiedByIDNoExtra :one -- name: CreateNotificationObject :one
SELECT * FROM notification_notified as nn WHERE nn.notified_id = $1; INSERT INTO notification_object(entity_type, action_type, entity_id, created_on, actor_id)
VALUES ($1, $2, $3, $4, $5) RETURNING *;
-- name: HasUnreadNotification :one -- name: GetEntityIDForNotificationID :one
SELECT EXISTS (SELECT 1 FROM notification_notified WHERE read = false AND user_id = $1); SELECT no.entity_id FROM notification as n
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
WHERE n.notification_id = $1;
-- name: MarkNotificationAsRead :exec -- name: GetEntityForNotificationID :one
UPDATE notification_notified SET read = $3, read_at = $2 WHERE user_id = $1 AND notified_id = $4; SELECT no.created_on, no.entity_id, no.entity_type, no.action_type, no.actor_id FROM notification as n
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
-- name: MarkAllNotificationsRead :exec WHERE n.notification_id = $1;
UPDATE notification_notified SET read = true, read_at = $2 WHERE user_id = $1;
-- name: CreateNotification :one -- name: CreateNotification :one
INSERT INTO notification (caused_by, data, action_type, created_on) INSERT INTO notification(notification_object_id, notifier_id)
VALUES ($1, $2, $3, $4) RETURNING *; VALUES ($1, $2) RETURNING *;
-- name: CreateNotificationNotifed :one
INSERT INTO notification_notified (notification_id, user_id) VALUES ($1, $2) RETURNING *;
-- name: GetNotificationByID :one
SELECT * FROM notification WHERE notification_id = $1;
-- name: GetNotificationsForUserIDPaged :many
SELECT n.*, nn.* FROM notification_notified AS nn
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
WHERE nn.user_id = @user_id::uuid
AND (@enable_unread::boolean = false OR nn.read = false)
AND (@enable_action_type::boolean = false OR n.action_type = ANY(@action_type::text[]))
ORDER BY n.created_on DESC
LIMIT @limit_rows::int;
-- name: GetNotificationsForUserIDCursor :many
SELECT n.*, nn.* FROM notification_notified AS nn
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
WHERE (n.created_on, n.notification_id) < (@created_on::timestamptz, @notification_id::uuid)
AND nn.user_id = @user_id::uuid
AND (@enable_unread::boolean = false OR nn.read = false)
AND (@enable_action_type::boolean = false OR n.action_type = ANY(@action_type::text[]))
ORDER BY n.created_on DESC
LIMIT @limit_rows::int;

View File

@ -1,9 +1,6 @@
-- name: GetAllTeamProjects :many -- name: GetAllTeamProjects :many
SELECT * FROM project WHERE team_id IS NOT null; SELECT * FROM project WHERE team_id IS NOT null;
-- name: GetProjectIDByShortID :one
SELECT project_id FROM project WHERE short_id = $1;
-- name: GetAllProjectsForTeam :many -- name: GetAllProjectsForTeam :many
SELECT * FROM project WHERE team_id = $1; SELECT * FROM project WHERE team_id = $1;

View File

@ -1,18 +1,3 @@
-- name: GetTaskWatcher :one
SELECT * FROM task_watcher WHERE user_id = $1 AND task_id = $2;
-- name: GetTaskWatchersForTask :many
SELECT * FROM task_watcher WHERE task_id = $1;
-- name: CreateTaskWatcher :one
INSERT INTO task_watcher (user_id, task_id, watched_at) VALUES ($1, $2, $3) RETURNING *;
-- name: GetTaskIDByShortID :one
SELECT task_id FROM task WHERE short_id = $1;
-- name: DeleteTaskWatcher :exec
DELETE FROM task_watcher WHERE user_id = $1 AND task_id = $2;
-- name: CreateTask :one -- name: CreateTask :one
INSERT INTO task (task_group_id, created_at, name, position) INSERT INTO task (task_group_id, created_at, name, position)
VALUES($1, $2, $3, $4) RETURNING *; VALUES($1, $2, $3, $4) RETURNING *;
@ -59,12 +44,6 @@ SELECT project_id FROM task
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
WHERE task_id = $1; WHERE task_id = $1;
-- name: GetProjectInfoForTask :one
SELECT project.short_id AS project_short_id, project.name, task.short_id AS task_short_id FROM task
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
INNER JOIN project ON task_group.project_id = project.project_id
WHERE task_id = $1;
-- name: CreateTaskComment :one -- name: CreateTaskComment :one
INSERT INTO task_comment (task_id, message, created_at, created_by) INSERT INTO task_comment (task_id, message, created_at, created_by)
VALUES ($1, $2, $3, $4) RETURNING *; VALUES ($1, $2, $3, $4) RETURNING *;
@ -116,34 +95,3 @@ SELECT task.* FROM task_assigned
) )
) )
ORDER BY task.due_date DESC, task_group.project_id DESC; ORDER BY task.due_date DESC, task_group.project_id DESC;
-- name: GetCommentCountForTask :one
SELECT COUNT(*) FROM task_comment WHERE task_id = $1;
-- name: CreateDueDateReminder :one
INSERT INTO task_due_date_reminder (task_id, period, duration, remind_at) VALUES ($1, $2, $3, $4) RETURNING *;
-- name: UpdateDueDateReminder :one
UPDATE task_due_date_reminder SET remind_at = $4, period = $2, duration = $3 WHERE due_date_reminder_id = $1 RETURNING *;
-- name: GetTaskForDueDateReminder :one
SELECT task.* FROM task_due_date_reminder
INNER JOIN task ON task.task_id = task_due_date_reminder.task_id
WHERE task_due_date_reminder.due_date_reminder_id = $1;
-- name: UpdateDueDateReminderRemindAt :one
UPDATE task_due_date_reminder SET remind_at = $2 WHERE due_date_reminder_id = $1 RETURNING *;
-- name: GetDueDateRemindersForTaskID :many
SELECT * FROM task_due_date_reminder WHERE task_id = $1;
-- name: GetDueDateReminderByID :one
SELECT * FROM task_due_date_reminder WHERE due_date_reminder_id = $1;
-- name: DeleteDueDateReminder :exec
DELETE FROM task_due_date_reminder WHERE due_date_reminder_id = $1;
-- name: GetDueDateRemindersForDuration :many
SELECT * FROM task_due_date_reminder WHERE remind_at >= @start_at::timestamptz;

View File

@ -63,9 +63,6 @@ SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system');
-- name: HasActiveUser :one -- name: HasActiveUser :one
SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true); SELECT EXISTS(SELECT 1 FROM user_account WHERE username != 'system' AND active = true);
-- name: DoesUserExist :one
SELECT EXISTS(SELECT 1 FROM user_account WHERE email = $1 OR username = $2);
-- name: CreateConfirmToken :one -- name: CreateConfirmToken :one
INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *; INSERT INTO user_account_confirm_token (email) VALUES ($1) RETURNING *;

View File

@ -12,38 +12,9 @@ import (
"github.com/lib/pq" "github.com/lib/pq"
) )
const createDueDateReminder = `-- name: CreateDueDateReminder :one
INSERT INTO task_due_date_reminder (task_id, period, duration, remind_at) VALUES ($1, $2, $3, $4) RETURNING due_date_reminder_id, task_id, period, duration, remind_at
`
type CreateDueDateReminderParams struct {
TaskID uuid.UUID `json:"task_id"`
Period int32 `json:"period"`
Duration string `json:"duration"`
RemindAt time.Time `json:"remind_at"`
}
func (q *Queries) CreateDueDateReminder(ctx context.Context, arg CreateDueDateReminderParams) (TaskDueDateReminder, error) {
row := q.db.QueryRowContext(ctx, createDueDateReminder,
arg.TaskID,
arg.Period,
arg.Duration,
arg.RemindAt,
)
var i TaskDueDateReminder
err := row.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
&i.RemindAt,
)
return i, err
}
const createTask = `-- name: CreateTask :one const createTask = `-- name: CreateTask :one
INSERT INTO task (task_group_id, created_at, name, position) INSERT INTO task (task_group_id, created_at, name, position)
VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id VALUES($1, $2, $3, $4) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type CreateTaskParams struct { type CreateTaskParams struct {
@ -72,14 +43,13 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
const createTaskAll = `-- name: CreateTaskAll :one const createTaskAll = `-- name: CreateTaskAll :one
INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date) INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type CreateTaskAllParams struct { type CreateTaskAllParams struct {
@ -114,7 +84,6 @@ func (q *Queries) CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (T
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
@ -151,37 +120,6 @@ func (q *Queries) CreateTaskComment(ctx context.Context, arg CreateTaskCommentPa
return i, err return i, err
} }
const createTaskWatcher = `-- name: CreateTaskWatcher :one
INSERT INTO task_watcher (user_id, task_id, watched_at) VALUES ($1, $2, $3) RETURNING task_watcher_id, task_id, user_id, watched_at
`
type CreateTaskWatcherParams struct {
UserID uuid.UUID `json:"user_id"`
TaskID uuid.UUID `json:"task_id"`
WatchedAt time.Time `json:"watched_at"`
}
func (q *Queries) CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherParams) (TaskWatcher, error) {
row := q.db.QueryRowContext(ctx, createTaskWatcher, arg.UserID, arg.TaskID, arg.WatchedAt)
var i TaskWatcher
err := row.Scan(
&i.TaskWatcherID,
&i.TaskID,
&i.UserID,
&i.WatchedAt,
)
return i, err
}
const deleteDueDateReminder = `-- name: DeleteDueDateReminder :exec
DELETE FROM task_due_date_reminder WHERE due_date_reminder_id = $1
`
func (q *Queries) DeleteDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteDueDateReminder, dueDateReminderID)
return err
}
const deleteTaskByID = `-- name: DeleteTaskByID :exec const deleteTaskByID = `-- name: DeleteTaskByID :exec
DELETE FROM task WHERE task_id = $1 DELETE FROM task WHERE task_id = $1
` `
@ -210,20 +148,6 @@ func (q *Queries) DeleteTaskCommentByID(ctx context.Context, taskCommentID uuid.
return i, err return i, err
} }
const deleteTaskWatcher = `-- name: DeleteTaskWatcher :exec
DELETE FROM task_watcher WHERE user_id = $1 AND task_id = $2
`
type DeleteTaskWatcherParams struct {
UserID uuid.UUID `json:"user_id"`
TaskID uuid.UUID `json:"task_id"`
}
func (q *Queries) DeleteTaskWatcher(ctx context.Context, arg DeleteTaskWatcherParams) error {
_, err := q.db.ExecContext(ctx, deleteTaskWatcher, arg.UserID, arg.TaskID)
return err
}
const deleteTasksByTaskGroupID = `-- name: DeleteTasksByTaskGroupID :execrows const deleteTasksByTaskGroupID = `-- name: DeleteTasksByTaskGroupID :execrows
DELETE FROM task where task_group_id = $1 DELETE FROM task where task_group_id = $1
` `
@ -237,7 +161,7 @@ func (q *Queries) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid
} }
const getAllTasks = `-- name: GetAllTasks :many const getAllTasks = `-- name: GetAllTasks :many
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id FROM task SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task
` `
func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) { func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
@ -260,7 +184,6 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -276,7 +199,7 @@ func (q *Queries) GetAllTasks(ctx context.Context) ([]Task, error) {
} }
const getAssignedTasksDueDateForUserID = `-- name: GetAssignedTasksDueDateForUserID :many const getAssignedTasksDueDateForUserID = `-- name: GetAssignedTasksDueDateForUserID :many
SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time, task.short_id FROM task_assigned SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned
INNER JOIN task ON task.task_id = task_assigned.task_id INNER JOIN task ON task.task_id = task_assigned.task_id
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
WHERE user_id = $1 WHERE user_id = $1
@ -320,7 +243,6 @@ func (q *Queries) GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetA
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -336,7 +258,7 @@ func (q *Queries) GetAssignedTasksDueDateForUserID(ctx context.Context, arg GetA
} }
const getAssignedTasksProjectForUserID = `-- name: GetAssignedTasksProjectForUserID :many const getAssignedTasksProjectForUserID = `-- name: GetAssignedTasksProjectForUserID :many
SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time, task.short_id FROM task_assigned SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned
INNER JOIN task ON task.task_id = task_assigned.task_id INNER JOIN task ON task.task_id = task_assigned.task_id
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
WHERE user_id = $1 WHERE user_id = $1
@ -380,7 +302,6 @@ func (q *Queries) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetA
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -395,17 +316,6 @@ func (q *Queries) GetAssignedTasksProjectForUserID(ctx context.Context, arg GetA
return items, nil return items, nil
} }
const getCommentCountForTask = `-- name: GetCommentCountForTask :one
SELECT COUNT(*) FROM task_comment WHERE task_id = $1
`
func (q *Queries) GetCommentCountForTask(ctx context.Context, taskID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, getCommentCountForTask, taskID)
var count int64
err := row.Scan(&count)
return count, err
}
const getCommentsForTaskID = `-- name: GetCommentsForTaskID :many const getCommentsForTaskID = `-- name: GetCommentsForTaskID :many
SELECT task_comment_id, task_id, created_at, updated_at, created_by, pinned, message FROM task_comment WHERE task_id = $1 ORDER BY created_at SELECT task_comment_id, task_id, created_at, updated_at, created_by, pinned, message FROM task_comment WHERE task_id = $1 ORDER BY created_at
` `
@ -441,89 +351,6 @@ func (q *Queries) GetCommentsForTaskID(ctx context.Context, taskID uuid.UUID) ([
return items, nil return items, nil
} }
const getDueDateReminderByID = `-- name: GetDueDateReminderByID :one
SELECT due_date_reminder_id, task_id, period, duration, remind_at FROM task_due_date_reminder WHERE due_date_reminder_id = $1
`
func (q *Queries) GetDueDateReminderByID(ctx context.Context, dueDateReminderID uuid.UUID) (TaskDueDateReminder, error) {
row := q.db.QueryRowContext(ctx, getDueDateReminderByID, dueDateReminderID)
var i TaskDueDateReminder
err := row.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
&i.RemindAt,
)
return i, err
}
const getDueDateRemindersForDuration = `-- name: GetDueDateRemindersForDuration :many
SELECT due_date_reminder_id, task_id, period, duration, remind_at FROM task_due_date_reminder WHERE remind_at >= $1::timestamptz
`
func (q *Queries) GetDueDateRemindersForDuration(ctx context.Context, startAt time.Time) ([]TaskDueDateReminder, error) {
rows, err := q.db.QueryContext(ctx, getDueDateRemindersForDuration, startAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskDueDateReminder
for rows.Next() {
var i TaskDueDateReminder
if err := rows.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
&i.RemindAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDueDateRemindersForTaskID = `-- name: GetDueDateRemindersForTaskID :many
SELECT due_date_reminder_id, task_id, period, duration, remind_at FROM task_due_date_reminder WHERE task_id = $1
`
func (q *Queries) GetDueDateRemindersForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskDueDateReminder, error) {
rows, err := q.db.QueryContext(ctx, getDueDateRemindersForTaskID, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskDueDateReminder
for rows.Next() {
var i TaskDueDateReminder
if err := rows.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
&i.RemindAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProjectIDForTask = `-- name: GetProjectIDForTask :one const getProjectIDForTask = `-- name: GetProjectIDForTask :one
SELECT project_id FROM task SELECT project_id FROM task
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
@ -571,28 +398,8 @@ func (q *Queries) GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID
return items, nil return items, nil
} }
const getProjectInfoForTask = `-- name: GetProjectInfoForTask :one
SELECT project.short_id AS project_short_id, project.name, task.short_id AS task_short_id FROM task
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
INNER JOIN project ON task_group.project_id = project.project_id
WHERE task_id = $1
`
type GetProjectInfoForTaskRow struct {
ProjectShortID string `json:"project_short_id"`
Name string `json:"name"`
TaskShortID string `json:"task_short_id"`
}
func (q *Queries) GetProjectInfoForTask(ctx context.Context, taskID uuid.UUID) (GetProjectInfoForTaskRow, error) {
row := q.db.QueryRowContext(ctx, getProjectInfoForTask, taskID)
var i GetProjectInfoForTaskRow
err := row.Scan(&i.ProjectShortID, &i.Name, &i.TaskShortID)
return i, err
}
const getRecentlyAssignedTaskForUserID = `-- name: GetRecentlyAssignedTaskForUserID :many const getRecentlyAssignedTaskForUserID = `-- name: GetRecentlyAssignedTaskForUserID :many
SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time, task.short_id FROM task_assigned INNER JOIN SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned INNER JOIN
task ON task.task_id = task_assigned.task_id WHERE user_id = $1 task ON task.task_id = task_assigned.task_id WHERE user_id = $1
AND $4::boolean = true OR ( AND $4::boolean = true OR (
$4::boolean = false AND complete = $2 AND ( $4::boolean = false AND complete = $2 AND (
@ -634,7 +441,6 @@ func (q *Queries) GetRecentlyAssignedTaskForUserID(ctx context.Context, arg GetR
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -650,7 +456,7 @@ func (q *Queries) GetRecentlyAssignedTaskForUserID(ctx context.Context, arg GetR
} }
const getTaskByID = `-- name: GetTaskByID :one const getTaskByID = `-- name: GetTaskByID :one
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id FROM task WHERE task_id = $1 SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task WHERE task_id = $1
` `
func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error) { func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error) {
@ -667,102 +473,12 @@ func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, erro
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
const getTaskForDueDateReminder = `-- name: GetTaskForDueDateReminder :one
SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time, task.short_id FROM task_due_date_reminder
INNER JOIN task ON task.task_id = task_due_date_reminder.task_id
WHERE task_due_date_reminder.due_date_reminder_id = $1
`
func (q *Queries) GetTaskForDueDateReminder(ctx context.Context, dueDateReminderID uuid.UUID) (Task, error) {
row := q.db.QueryRowContext(ctx, getTaskForDueDateReminder, dueDateReminderID)
var i Task
err := row.Scan(
&i.TaskID,
&i.TaskGroupID,
&i.CreatedAt,
&i.Name,
&i.Position,
&i.Description,
&i.DueDate,
&i.Complete,
&i.CompletedAt,
&i.HasTime,
&i.ShortID,
)
return i, err
}
const getTaskIDByShortID = `-- name: GetTaskIDByShortID :one
SELECT task_id FROM task WHERE short_id = $1
`
func (q *Queries) GetTaskIDByShortID(ctx context.Context, shortID string) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getTaskIDByShortID, shortID)
var task_id uuid.UUID
err := row.Scan(&task_id)
return task_id, err
}
const getTaskWatcher = `-- name: GetTaskWatcher :one
SELECT task_watcher_id, task_id, user_id, watched_at FROM task_watcher WHERE user_id = $1 AND task_id = $2
`
type GetTaskWatcherParams struct {
UserID uuid.UUID `json:"user_id"`
TaskID uuid.UUID `json:"task_id"`
}
func (q *Queries) GetTaskWatcher(ctx context.Context, arg GetTaskWatcherParams) (TaskWatcher, error) {
row := q.db.QueryRowContext(ctx, getTaskWatcher, arg.UserID, arg.TaskID)
var i TaskWatcher
err := row.Scan(
&i.TaskWatcherID,
&i.TaskID,
&i.UserID,
&i.WatchedAt,
)
return i, err
}
const getTaskWatchersForTask = `-- name: GetTaskWatchersForTask :many
SELECT task_watcher_id, task_id, user_id, watched_at FROM task_watcher WHERE task_id = $1
`
func (q *Queries) GetTaskWatchersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskWatcher, error) {
rows, err := q.db.QueryContext(ctx, getTaskWatchersForTask, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TaskWatcher
for rows.Next() {
var i TaskWatcher
if err := rows.Scan(
&i.TaskWatcherID,
&i.TaskID,
&i.UserID,
&i.WatchedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id FROM task WHERE task_group_id = $1 SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task WHERE task_group_id = $1
` `
func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) { func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) {
@ -785,7 +501,6 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -801,7 +516,7 @@ func (q *Queries) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.U
} }
const setTaskComplete = `-- name: SetTaskComplete :one const setTaskComplete = `-- name: SetTaskComplete :one
UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id UPDATE task SET complete = $2, completed_at = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type SetTaskCompleteParams struct { type SetTaskCompleteParams struct {
@ -824,58 +539,6 @@ func (q *Queries) SetTaskComplete(ctx context.Context, arg SetTaskCompleteParams
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
)
return i, err
}
const updateDueDateReminder = `-- name: UpdateDueDateReminder :one
UPDATE task_due_date_reminder SET remind_at = $4, period = $2, duration = $3 WHERE due_date_reminder_id = $1 RETURNING due_date_reminder_id, task_id, period, duration, remind_at
`
type UpdateDueDateReminderParams struct {
DueDateReminderID uuid.UUID `json:"due_date_reminder_id"`
Period int32 `json:"period"`
Duration string `json:"duration"`
RemindAt time.Time `json:"remind_at"`
}
func (q *Queries) UpdateDueDateReminder(ctx context.Context, arg UpdateDueDateReminderParams) (TaskDueDateReminder, error) {
row := q.db.QueryRowContext(ctx, updateDueDateReminder,
arg.DueDateReminderID,
arg.Period,
arg.Duration,
arg.RemindAt,
)
var i TaskDueDateReminder
err := row.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
&i.RemindAt,
)
return i, err
}
const updateDueDateReminderRemindAt = `-- name: UpdateDueDateReminderRemindAt :one
UPDATE task_due_date_reminder SET remind_at = $2 WHERE due_date_reminder_id = $1 RETURNING due_date_reminder_id, task_id, period, duration, remind_at
`
type UpdateDueDateReminderRemindAtParams struct {
DueDateReminderID uuid.UUID `json:"due_date_reminder_id"`
RemindAt time.Time `json:"remind_at"`
}
func (q *Queries) UpdateDueDateReminderRemindAt(ctx context.Context, arg UpdateDueDateReminderRemindAtParams) (TaskDueDateReminder, error) {
row := q.db.QueryRowContext(ctx, updateDueDateReminderRemindAt, arg.DueDateReminderID, arg.RemindAt)
var i TaskDueDateReminder
err := row.Scan(
&i.DueDateReminderID,
&i.TaskID,
&i.Period,
&i.Duration,
&i.RemindAt,
) )
return i, err return i, err
} }
@ -906,7 +569,7 @@ func (q *Queries) UpdateTaskComment(ctx context.Context, arg UpdateTaskCommentPa
} }
const updateTaskDescription = `-- name: UpdateTaskDescription :one const updateTaskDescription = `-- name: UpdateTaskDescription :one
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id UPDATE task SET description = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskDescriptionParams struct { type UpdateTaskDescriptionParams struct {
@ -928,13 +591,12 @@ func (q *Queries) UpdateTaskDescription(ctx context.Context, arg UpdateTaskDescr
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
const updateTaskDueDate = `-- name: UpdateTaskDueDate :one const updateTaskDueDate = `-- name: UpdateTaskDueDate :one
UPDATE task SET due_date = $2, has_time = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id UPDATE task SET due_date = $2, has_time = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskDueDateParams struct { type UpdateTaskDueDateParams struct {
@ -957,13 +619,12 @@ func (q *Queries) UpdateTaskDueDate(ctx context.Context, arg UpdateTaskDueDatePa
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
const updateTaskLocation = `-- name: UpdateTaskLocation :one const updateTaskLocation = `-- name: UpdateTaskLocation :one
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskLocationParams struct { type UpdateTaskLocationParams struct {
@ -986,13 +647,12 @@ func (q *Queries) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocation
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
const updateTaskName = `-- name: UpdateTaskName :one const updateTaskName = `-- name: UpdateTaskName :one
UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id UPDATE task SET name = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskNameParams struct { type UpdateTaskNameParams struct {
@ -1014,13 +674,12 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams)
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }
const updateTaskPosition = `-- name: UpdateTaskPosition :one const updateTaskPosition = `-- name: UpdateTaskPosition :one
UPDATE task SET position = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time, short_id UPDATE task SET position = $2 WHERE task_id = $1 RETURNING task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time
` `
type UpdateTaskPositionParams struct { type UpdateTaskPositionParams struct {
@ -1042,7 +701,6 @@ func (q *Queries) UpdateTaskPosition(ctx context.Context, arg UpdateTaskPosition
&i.Complete, &i.Complete,
&i.CompletedAt, &i.CompletedAt,
&i.HasTime, &i.HasTime,
&i.ShortID,
) )
return i, err return i, err
} }

View File

@ -157,22 +157,6 @@ func (q *Queries) DeleteUserAccountInvitedForEmail(ctx context.Context, email st
return err return err
} }
const doesUserExist = `-- name: DoesUserExist :one
SELECT EXISTS(SELECT 1 FROM user_account WHERE email = $1 OR username = $2)
`
type DoesUserExistParams struct {
Email string `json:"email"`
Username string `json:"username"`
}
func (q *Queries) DoesUserExist(ctx context.Context, arg DoesUserExistParams) (bool, error) {
row := q.db.QueryRowContext(ctx, doesUserExist, arg.Email, arg.Username)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const getAllUserAccounts = `-- name: GetAllUserAccounts :many const getAllUserAccounts = `-- name: GetAllUserAccounts :many
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username != 'system' SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM user_account WHERE username != 'system'
` `

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ import (
"os" "os"
"reflect" "reflect"
"strings" "strings"
"sync"
"time" "time"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
@ -17,37 +16,21 @@ import (
"github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
"github.com/go-redis/redis/v8"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/jobs"
"github.com/jordanknott/taskcafe/internal/logger" "github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils" "github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
) )
type NotificationObservers struct {
Mu sync.Mutex
Subscribers map[string]map[string]chan *Notified
}
// NewHandler returns a new graphql endpoint handler. // NewHandler returns a new graphql endpoint handler.
func NewHandler(repo db.Repository, appConfig config.AppConfig, jobQueue jobs.JobQueue, redisClient *redis.Client) http.Handler { func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler {
resolver := &Resolver{
Repository: repo,
Redis: redisClient,
AppConfig: appConfig,
Job: jobQueue,
Notifications: &NotificationObservers{
Mu: sync.Mutex{},
Subscribers: make(map[string]map[string]chan *Notified),
},
}
resolver.SubscribeRedis()
c := Config{ c := Config{
Resolvers: resolver, Resolvers: &Resolver{
Repository: repo,
EmailConfig: emailConfig,
},
} }
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) { c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
userID, ok := GetUser(ctx) userID, ok := GetUser(ctx)
@ -239,6 +222,26 @@ func ConvertToRoleCode(r string) RoleCode {
return RoleCodeObserver return RoleCodeObserver
} }
// GetEntityType converts integer to EntityType enum
func GetEntityType(entityType int32) EntityType {
switch entityType {
case 1:
return EntityTypeTask
default:
panic("Not a valid entity type!")
}
}
// GetActionType converts integer to ActionType enum
func GetActionType(actionType int32) ActionType {
switch actionType {
case 1:
return ActionTypeTaskMemberAdded
default:
panic("Not a valid entity type!")
}
}
type MemberType string type MemberType string
const ( const (

View File

@ -3,12 +3,8 @@ package graph
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
log "github.com/sirupsen/logrus"
) )
// GetOwnedList todo: remove this // GetOwnedList todo: remove this
@ -16,57 +12,6 @@ func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*O
return &OwnedList{}, nil return &OwnedList{}, nil
} }
type CreateNotificationParams struct {
NotifiedList []uuid.UUID
ActionType ActionType
CausedBy uuid.UUID
Data map[string]string
}
func (r *Resolver) CreateNotification(ctx context.Context, data CreateNotificationParams) error {
now := time.Now().UTC()
raw, err := json.Marshal(NotifiedData{Data: data.Data})
if err != nil {
log.WithError(err).Error("error while marshal json data for notification")
return err
}
log.WithField("ActionType", data.ActionType).Info("creating notification object")
n, err := r.Repository.CreateNotification(ctx, db.CreateNotificationParams{
CausedBy: data.CausedBy,
ActionType: data.ActionType.String(),
CreatedOn: now,
Data: json.RawMessage(raw),
})
if err != nil {
log.WithError(err).Error("error while creating notification")
return err
}
for _, nn := range data.NotifiedList {
log.WithFields(log.Fields{"UserID": nn, "NotificationID": n.NotificationID}).Info("creating notification notified object")
notified, err := r.Repository.CreateNotificationNotifed(ctx, db.CreateNotificationNotifedParams{
UserID: nn,
NotificationID: n.NotificationID,
})
if err != nil {
log.WithError(err).Error("error while creating notification notified object")
return err
}
for ouid, observers := range r.Notifications.Subscribers {
log.WithField("ouid", ouid).Info("checking user subscribers")
for oid, ochan := range observers {
log.WithField("ouid", ouid).WithField("oid", oid).Info("checking user subscriber")
ochan <- &Notified{
ID: notified.NotifiedID,
Read: notified.Read,
ReadAt: &notified.ReadAt.Time,
Notification: &n,
}
}
}
}
return nil
}
// GetMemberList returns a list of projects the user is a member of // GetMemberList returns a list of projects the user is a member of
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) { func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID) projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)
@ -100,7 +45,3 @@ func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*
type ActivityData struct { type ActivityData struct {
Data map[string]string Data map[string]string
} }
type NotifiedData struct {
Data map[string]string
}

View File

@ -33,11 +33,6 @@ type ChecklistBadge struct {
Total int `json:"total"` Total int `json:"total"`
} }
type CommentsBadge struct {
Total int `json:"total"`
Unread bool `json:"unread"`
}
type CreateTaskChecklist struct { type CreateTaskChecklist struct {
TaskID uuid.UUID `json:"taskID"` TaskID uuid.UUID `json:"taskID"`
Name string `json:"name"` Name string `json:"name"`
@ -60,16 +55,6 @@ type CreateTaskCommentPayload struct {
Comment *db.TaskComment `json:"comment"` Comment *db.TaskComment `json:"comment"`
} }
type CreateTaskDueDateNotification struct {
TaskID uuid.UUID `json:"taskID"`
Period int `json:"period"`
Duration DueDateNotificationDuration `json:"duration"`
}
type CreateTaskDueDateNotificationsResult struct {
Notifications []DueDateNotification `json:"notifications"`
}
type CreateTeamMember struct { type CreateTeamMember struct {
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"` TeamID uuid.UUID `json:"teamID"`
@ -154,14 +139,6 @@ type DeleteTaskCommentPayload struct {
CommentID uuid.UUID `json:"commentID"` CommentID uuid.UUID `json:"commentID"`
} }
type DeleteTaskDueDateNotification struct {
ID uuid.UUID `json:"id"`
}
type DeleteTaskDueDateNotificationsResult struct {
Notifications []uuid.UUID `json:"notifications"`
}
type DeleteTaskGroupInput struct { type DeleteTaskGroupInput struct {
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
} }
@ -221,17 +198,6 @@ type DeleteUserAccountPayload struct {
UserAccount *db.UserAccount `json:"userAccount"` UserAccount *db.UserAccount `json:"userAccount"`
} }
type DueDate struct {
At *time.Time `json:"at"`
Notifications []DueDateNotification `json:"notifications"`
}
type DueDateNotification struct {
ID uuid.UUID `json:"id"`
Period int `json:"period"`
Duration DueDateNotificationDuration `json:"duration"`
}
type DuplicateTaskGroup struct { type DuplicateTaskGroup struct {
ProjectID uuid.UUID `json:"projectID"` ProjectID uuid.UUID `json:"projectID"`
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
@ -244,13 +210,11 @@ type DuplicateTaskGroupPayload struct {
} }
type FindProject struct { type FindProject struct {
ProjectID *uuid.UUID `json:"projectID"` ProjectID uuid.UUID `json:"projectID"`
ProjectShortID *string `json:"projectShortID"`
} }
type FindTask struct { type FindTask struct {
TaskID *uuid.UUID `json:"taskID"` TaskID uuid.UUID `json:"taskID"`
TaskShortID *string `json:"taskShortID"`
} }
type FindTeam struct { type FindTeam struct {
@ -261,10 +225,6 @@ type FindUser struct {
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
} }
type HasUnreadNotificationsResult struct {
Unread bool `json:"unread"`
}
type InviteProjectMembers struct { type InviteProjectMembers struct {
ProjectID uuid.UUID `json:"projectID"` ProjectID uuid.UUID `json:"projectID"`
Members []MemberInvite `json:"members"` Members []MemberInvite `json:"members"`
@ -391,42 +351,16 @@ type NewUserAccount struct {
RoleCode string `json:"roleCode"` RoleCode string `json:"roleCode"`
} }
type NotificationCausedBy struct { type NotificationActor struct {
Fullname string `json:"fullname"`
Username string `json:"username"`
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Type ActorType `json:"type"`
Name string `json:"name"`
} }
type NotificationData struct { type NotificationEntity struct {
Key string `json:"key"`
Value string `json:"value"`
}
type NotificationMarkAllAsReadResult struct {
Success bool `json:"success"`
}
type NotificationToggleReadInput struct {
NotifiedID uuid.UUID `json:"notifiedID"`
}
type Notified struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Notification *db.Notification `json:"notification"` Type EntityType `json:"type"`
Read bool `json:"read"` Name string `json:"name"`
ReadAt *time.Time `json:"readAt"`
}
type NotifiedInput struct {
Limit int `json:"limit"`
Cursor *string `json:"cursor"`
Filter NotificationFilter `json:"filter"`
}
type NotifiedResult struct {
TotalCount int `json:"totalCount"`
Notified []Notified `json:"notified"`
PageInfo *PageInfo `json:"pageInfo"`
} }
type OwnedList struct { type OwnedList struct {
@ -439,11 +373,6 @@ type OwnersList struct {
Teams []uuid.UUID `json:"teams"` Teams []uuid.UUID `json:"teams"`
} }
type PageInfo struct {
EndCursor *string `json:"endCursor"`
HasNextPage bool `json:"hasNextPage"`
}
type ProfileIcon struct { type ProfileIcon struct {
URL *string `json:"url"` URL *string `json:"url"`
Initials *string `json:"initials"` Initials *string `json:"initials"`
@ -502,7 +431,6 @@ type TaskActivityData struct {
type TaskBadges struct { type TaskBadges struct {
Checklist *ChecklistBadge `json:"checklist"` Checklist *ChecklistBadge `json:"checklist"`
Comments *CommentsBadge `json:"comments"`
} }
type TaskPositionUpdate struct { type TaskPositionUpdate struct {
@ -539,10 +467,6 @@ type ToggleTaskLabelPayload struct {
Task *db.Task `json:"task"` Task *db.Task `json:"task"`
} }
type ToggleTaskWatch struct {
TaskID uuid.UUID `json:"taskID"`
}
type UnassignTaskInput struct { type UnassignTaskInput struct {
TaskID uuid.UUID `json:"taskID"` TaskID uuid.UUID `json:"taskID"`
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
@ -632,16 +556,6 @@ type UpdateTaskDueDate struct {
DueDate *time.Time `json:"dueDate"` DueDate *time.Time `json:"dueDate"`
} }
type UpdateTaskDueDateNotification struct {
ID uuid.UUID `json:"id"`
Period int `json:"period"`
Duration DueDateNotificationDuration `json:"duration"`
}
type UpdateTaskDueDateNotificationsResult struct {
Notifications []DueDateNotification `json:"notifications"`
}
type UpdateTaskGroupName struct { type UpdateTaskGroupName struct {
TaskGroupID uuid.UUID `json:"taskGroupID"` TaskGroupID uuid.UUID `json:"taskGroupID"`
Name string `json:"name"` Name string `json:"name"`
@ -745,44 +659,16 @@ func (e ActionLevel) MarshalGQL(w io.Writer) {
type ActionType string type ActionType string
const ( const (
ActionTypeTeamAdded ActionType = "TEAM_ADDED" ActionTypeTaskMemberAdded ActionType = "TASK_MEMBER_ADDED"
ActionTypeTeamRemoved ActionType = "TEAM_REMOVED"
ActionTypeProjectAdded ActionType = "PROJECT_ADDED"
ActionTypeProjectRemoved ActionType = "PROJECT_REMOVED"
ActionTypeProjectArchived ActionType = "PROJECT_ARCHIVED"
ActionTypeDueDateAdded ActionType = "DUE_DATE_ADDED"
ActionTypeDueDateRemoved ActionType = "DUE_DATE_REMOVED"
ActionTypeDueDateChanged ActionType = "DUE_DATE_CHANGED"
ActionTypeDueDateReminder ActionType = "DUE_DATE_REMINDER"
ActionTypeTaskAssigned ActionType = "TASK_ASSIGNED"
ActionTypeTaskMoved ActionType = "TASK_MOVED"
ActionTypeTaskArchived ActionType = "TASK_ARCHIVED"
ActionTypeTaskAttachmentUploaded ActionType = "TASK_ATTACHMENT_UPLOADED"
ActionTypeCommentMentioned ActionType = "COMMENT_MENTIONED"
ActionTypeCommentOther ActionType = "COMMENT_OTHER"
) )
var AllActionType = []ActionType{ var AllActionType = []ActionType{
ActionTypeTeamAdded, ActionTypeTaskMemberAdded,
ActionTypeTeamRemoved,
ActionTypeProjectAdded,
ActionTypeProjectRemoved,
ActionTypeProjectArchived,
ActionTypeDueDateAdded,
ActionTypeDueDateRemoved,
ActionTypeDueDateChanged,
ActionTypeDueDateReminder,
ActionTypeTaskAssigned,
ActionTypeTaskMoved,
ActionTypeTaskArchived,
ActionTypeTaskAttachmentUploaded,
ActionTypeCommentMentioned,
ActionTypeCommentOther,
} }
func (e ActionType) IsValid() bool { func (e ActionType) IsValid() bool {
switch e { switch e {
case ActionTypeTeamAdded, ActionTypeTeamRemoved, ActionTypeProjectAdded, ActionTypeProjectRemoved, ActionTypeProjectArchived, ActionTypeDueDateAdded, ActionTypeDueDateRemoved, ActionTypeDueDateChanged, ActionTypeDueDateReminder, ActionTypeTaskAssigned, ActionTypeTaskMoved, ActionTypeTaskArchived, ActionTypeTaskAttachmentUploaded, ActionTypeCommentMentioned, ActionTypeCommentOther: case ActionTypeTaskMemberAdded:
return true return true
} }
return false return false
@ -866,48 +752,81 @@ func (e ActivityType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
type DueDateNotificationDuration string type ActorType string
const ( const (
DueDateNotificationDurationMinute DueDateNotificationDuration = "MINUTE" ActorTypeUser ActorType = "USER"
DueDateNotificationDurationHour DueDateNotificationDuration = "HOUR"
DueDateNotificationDurationDay DueDateNotificationDuration = "DAY"
DueDateNotificationDurationWeek DueDateNotificationDuration = "WEEK"
) )
var AllDueDateNotificationDuration = []DueDateNotificationDuration{ var AllActorType = []ActorType{
DueDateNotificationDurationMinute, ActorTypeUser,
DueDateNotificationDurationHour,
DueDateNotificationDurationDay,
DueDateNotificationDurationWeek,
} }
func (e DueDateNotificationDuration) IsValid() bool { func (e ActorType) IsValid() bool {
switch e { switch e {
case DueDateNotificationDurationMinute, DueDateNotificationDurationHour, DueDateNotificationDurationDay, DueDateNotificationDurationWeek: case ActorTypeUser:
return true return true
} }
return false return false
} }
func (e DueDateNotificationDuration) String() string { func (e ActorType) String() string {
return string(e) return string(e)
} }
func (e *DueDateNotificationDuration) UnmarshalGQL(v interface{}) error { func (e *ActorType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string) str, ok := v.(string)
if !ok { if !ok {
return fmt.Errorf("enums must be strings") return fmt.Errorf("enums must be strings")
} }
*e = DueDateNotificationDuration(str) *e = ActorType(str)
if !e.IsValid() { if !e.IsValid() {
return fmt.Errorf("%s is not a valid DueDateNotificationDuration", str) return fmt.Errorf("%s is not a valid ActorType", str)
} }
return nil return nil
} }
func (e DueDateNotificationDuration) MarshalGQL(w io.Writer) { func (e ActorType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type EntityType string
const (
EntityTypeTask EntityType = "TASK"
)
var AllEntityType = []EntityType{
EntityTypeTask,
}
func (e EntityType) IsValid() bool {
switch e {
case EntityTypeTask:
return true
}
return false
}
func (e EntityType) String() string {
return string(e)
}
func (e *EntityType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = EntityType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid EntityType", str)
}
return nil
}
func (e EntityType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
@ -1007,51 +926,6 @@ func (e MyTasksStatus) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
type NotificationFilter string
const (
NotificationFilterAll NotificationFilter = "ALL"
NotificationFilterUnread NotificationFilter = "UNREAD"
NotificationFilterAssigned NotificationFilter = "ASSIGNED"
NotificationFilterMentioned NotificationFilter = "MENTIONED"
)
var AllNotificationFilter = []NotificationFilter{
NotificationFilterAll,
NotificationFilterUnread,
NotificationFilterAssigned,
NotificationFilterMentioned,
}
func (e NotificationFilter) IsValid() bool {
switch e {
case NotificationFilterAll, NotificationFilterUnread, NotificationFilterAssigned, NotificationFilterMentioned:
return true
}
return false
}
func (e NotificationFilter) String() string {
return string(e)
}
func (e *NotificationFilter) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = NotificationFilter(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid NotificationFilter", str)
}
return nil
}
func (e NotificationFilter) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type ObjectType string type ObjectType string
const ( const (

View File

@ -1,384 +0,0 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"database/sql"
"encoding/json"
"errors"
"time"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus"
)
func (r *mutationResolver) NotificationToggleRead(ctx context.Context, input NotificationToggleReadInput) (*Notified, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &Notified{}, errors.New("unknown user ID")
}
notified, err := r.Repository.GetNotifiedByID(ctx, input.NotifiedID)
if err != nil {
log.WithError(err).Error("error while getting notified by ID")
return &Notified{}, err
}
readAt := time.Now().UTC()
read := true
if notified.Read {
read = false
err = r.Repository.MarkNotificationAsRead(ctx, db.MarkNotificationAsReadParams{
UserID: userID,
NotifiedID: input.NotifiedID,
Read: false,
ReadAt: sql.NullTime{
Valid: false,
Time: time.Time{},
},
})
} else {
err = r.Repository.MarkNotificationAsRead(ctx, db.MarkNotificationAsReadParams{
UserID: userID,
Read: true,
NotifiedID: input.NotifiedID,
ReadAt: sql.NullTime{
Valid: true,
Time: readAt,
},
})
}
if err != nil {
log.WithError(err).Error("error while marking notification as read")
return &Notified{}, err
}
return &Notified{
ID: notified.NotifiedID,
Read: read,
ReadAt: &readAt,
Notification: &db.Notification{
NotificationID: notified.NotificationID,
CausedBy: notified.CausedBy,
ActionType: notified.ActionType,
Data: notified.Data,
CreatedOn: notified.CreatedOn,
},
}, nil
}
func (r *mutationResolver) NotificationMarkAllRead(ctx context.Context) (*NotificationMarkAllAsReadResult, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &NotificationMarkAllAsReadResult{}, errors.New("invalid user ID")
}
now := time.Now().UTC()
err := r.Repository.MarkAllNotificationsRead(ctx, db.MarkAllNotificationsReadParams{UserID: userID, ReadAt: sql.NullTime{Valid: true, Time: now}})
if err != nil {
return &NotificationMarkAllAsReadResult{}, err
}
return &NotificationMarkAllAsReadResult{Success: false}, nil
}
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
return obj.NotificationID, nil
}
func (r *notificationResolver) ActionType(ctx context.Context, obj *db.Notification) (ActionType, error) {
actionType := ActionType(obj.ActionType)
if !actionType.IsValid() {
log.WithField("ActionType", obj.ActionType).Error("ActionType is invalid")
return actionType, errors.New("ActionType is invalid")
}
return ActionType(obj.ActionType), nil // TODO
}
func (r *notificationResolver) CausedBy(ctx context.Context, obj *db.Notification) (*NotificationCausedBy, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.CausedBy)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.WithError(err).Error("error while resolving Notification.CausedBy")
return &NotificationCausedBy{}, err
}
return &NotificationCausedBy{
Fullname: user.FullName,
Username: user.Username,
ID: obj.CausedBy,
}, nil
}
func (r *notificationResolver) Data(ctx context.Context, obj *db.Notification) ([]NotificationData, error) {
notifiedData := NotifiedData{}
err := json.Unmarshal(obj.Data, &notifiedData)
if err != nil {
return []NotificationData{}, err
}
data := []NotificationData{}
for key, value := range notifiedData.Data {
data = append(data, NotificationData{Key: key, Value: value})
}
return data, nil
}
func (r *notificationResolver) CreatedAt(ctx context.Context, obj *db.Notification) (*time.Time, error) {
return &obj.CreatedOn, nil
}
func (r *queryResolver) Notifications(ctx context.Context) ([]Notified, error) {
userID, ok := GetUserID(ctx)
logger.New(ctx).Info("fetching notifications")
if !ok {
return []Notified{}, nil
}
notifications, err := r.Repository.GetAllNotificationsForUserID(ctx, userID)
if err == sql.ErrNoRows {
return []Notified{}, nil
} else if err != nil {
return []Notified{}, err
}
userNotifications := []Notified{}
for _, notified := range notifications {
var readAt *time.Time
if notified.ReadAt.Valid {
readAt = &notified.ReadAt.Time
}
n := Notified{
ID: notified.NotifiedID,
Read: notified.Read,
ReadAt: readAt,
Notification: &db.Notification{
NotificationID: notified.NotificationID,
CausedBy: notified.CausedBy,
ActionType: notified.ActionType,
Data: notified.Data,
CreatedOn: notified.CreatedOn,
},
}
userNotifications = append(userNotifications, n)
}
return userNotifications, nil
}
func (r *queryResolver) Notified(ctx context.Context, input NotifiedInput) (*NotifiedResult, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &NotifiedResult{}, errors.New("userID is not found")
}
log.WithField("userID", userID).Info("fetching notified")
if input.Cursor != nil {
t, id, err := utils.DecodeCursor(*input.Cursor)
if err != nil {
log.WithError(err).Error("error decoding cursor")
return &NotifiedResult{}, err
}
enableRead := false
enableActionType := false
actionTypes := []string{}
switch input.Filter {
case NotificationFilterUnread:
enableRead = true
break
case NotificationFilterMentioned:
enableActionType = true
actionTypes = []string{"COMMENT_MENTIONED"}
break
case NotificationFilterAssigned:
enableActionType = true
actionTypes = []string{"TASK_ASSIGNED"}
break
}
n, err := r.Repository.GetNotificationsForUserIDCursor(ctx, db.GetNotificationsForUserIDCursorParams{
CreatedOn: t,
NotificationID: id,
LimitRows: int32(input.Limit + 1),
UserID: userID,
EnableUnread: enableRead,
EnableActionType: enableActionType,
ActionType: actionTypes,
})
if err != nil {
log.WithError(err).Error("error decoding fetching notifications")
return &NotifiedResult{}, err
}
hasNextPage := false
log.WithFields(log.Fields{
"nLen": len(n),
"cursorTime": t,
"cursorId": id,
"limit": input.Limit,
}).Info("fetched notified")
var endCursor *db.GetNotificationsForUserIDCursorRow
if len(n) != 0 {
endCursor = &n[len(n)-1]
if len(n) == input.Limit+1 {
hasNextPage = true
n = n[:len(n)-1]
endCursor = &n[len(n)-1]
}
}
userNotifications := []Notified{}
for _, notified := range n {
var readAt *time.Time
if notified.ReadAt.Valid {
readAt = &notified.ReadAt.Time
}
n := Notified{
ID: notified.NotifiedID,
Read: notified.Read,
ReadAt: readAt,
Notification: &db.Notification{
NotificationID: notified.NotificationID,
CausedBy: notified.CausedBy,
ActionType: notified.ActionType,
Data: notified.Data,
CreatedOn: notified.CreatedOn,
},
}
userNotifications = append(userNotifications, n)
}
var endCursorEncoded *string
if endCursor != nil {
eCur := utils.EncodeCursor(endCursor.CreatedOn, endCursor.NotificationID)
endCursorEncoded = &eCur
}
pageInfo := &PageInfo{
HasNextPage: hasNextPage,
EndCursor: endCursorEncoded,
}
log.WithField("pageInfo", pageInfo).Info("created page info")
return &NotifiedResult{
TotalCount: len(n) - 1,
PageInfo: pageInfo,
Notified: userNotifications,
}, nil
}
enableRead := false
enableActionType := false
actionTypes := []string{}
switch input.Filter {
case NotificationFilterUnread:
enableRead = true
break
case NotificationFilterMentioned:
enableActionType = true
actionTypes = []string{"COMMENT_MENTIONED"}
break
case NotificationFilterAssigned:
enableActionType = true
actionTypes = []string{"TASK_ASSIGNED"}
break
}
n, err := r.Repository.GetNotificationsForUserIDPaged(ctx, db.GetNotificationsForUserIDPagedParams{
LimitRows: int32(input.Limit + 1),
EnableUnread: enableRead,
EnableActionType: enableActionType,
ActionType: actionTypes,
UserID: userID,
})
if err != nil {
log.WithError(err).Error("error decoding fetching notifications")
return &NotifiedResult{}, err
}
hasNextPage := false
log.WithFields(log.Fields{
"nLen": len(n),
"limit": input.Limit,
}).Info("fetched notified")
var endCursor *db.GetNotificationsForUserIDPagedRow
if len(n) != 0 {
endCursor = &n[len(n)-1]
if len(n) == input.Limit+1 {
hasNextPage = true
n = n[:len(n)-1]
endCursor = &n[len(n)-1]
}
}
userNotifications := []Notified{}
for _, notified := range n {
var readAt *time.Time
if notified.ReadAt.Valid {
readAt = &notified.ReadAt.Time
}
n := Notified{
ID: notified.NotifiedID,
Read: notified.Read,
ReadAt: readAt,
Notification: &db.Notification{
NotificationID: notified.NotificationID,
CausedBy: notified.CausedBy,
ActionType: notified.ActionType,
Data: notified.Data,
CreatedOn: notified.CreatedOn,
},
}
userNotifications = append(userNotifications, n)
}
var endCursorEncoded *string
if endCursor != nil {
eCur := utils.EncodeCursor(endCursor.CreatedOn, endCursor.NotificationID)
endCursorEncoded = &eCur
}
pageInfo := &PageInfo{
HasNextPage: hasNextPage,
EndCursor: endCursorEncoded,
}
log.WithField("pageInfo", pageInfo).Info("created page info")
return &NotifiedResult{
TotalCount: len(n),
PageInfo: pageInfo,
Notified: userNotifications,
}, nil
}
func (r *queryResolver) HasUnreadNotifications(ctx context.Context) (*HasUnreadNotificationsResult, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &HasUnreadNotificationsResult{}, errors.New("userID is missing")
}
unread, err := r.Repository.HasUnreadNotification(ctx, userID)
if err != nil {
log.WithError(err).Error("error while fetching unread notifications")
return &HasUnreadNotificationsResult{}, err
}
return &HasUnreadNotificationsResult{
Unread: unread,
}, nil
}
func (r *subscriptionResolver) NotificationAdded(ctx context.Context) (<-chan *Notified, error) {
notified := make(chan *Notified, 1)
userID, ok := GetUserID(ctx)
if !ok {
return notified, errors.New("userID is not found")
}
id := uuid.New().String()
go func() {
<-ctx.Done()
r.Notifications.Mu.Lock()
if _, ok := r.Notifications.Subscribers[userID.String()]; ok {
delete(r.Notifications.Subscribers[userID.String()], id)
}
r.Notifications.Mu.Unlock()
}()
r.Notifications.Mu.Lock()
if _, ok := r.Notifications.Subscribers[userID.String()]; !ok {
r.Notifications.Subscribers[userID.String()] = make(map[string]chan *Notified)
}
log.WithField("userID", userID).WithField("id", id).Info("adding new channel")
r.Notifications.Subscribers[userID.String()][id] = notified
r.Notifications.Mu.Unlock()
return notified, nil
}
// Notification returns NotificationResolver implementation.
func (r *Resolver) Notification() NotificationResolver { return &notificationResolver{r} }
type notificationResolver struct{ *Resolver }

View File

@ -1,439 +0,0 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
return obj.LabelColorID, nil
}
func (r *mutationResolver) CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*db.ProjectLabel, error) {
createdAt := time.Now().UTC()
var name sql.NullString
if input.Name != nil {
name = sql.NullString{
*input.Name,
true,
}
} else {
name = sql.NullString{
"",
false,
}
}
projectLabel, err := r.Repository.CreateProjectLabel(ctx, db.CreateProjectLabelParams{input.ProjectID, input.LabelColorID, createdAt, name})
return &projectLabel, err
}
func (r *mutationResolver) DeleteProjectLabel(ctx context.Context, input DeleteProjectLabel) (*db.ProjectLabel, error) {
label, err := r.Repository.GetProjectLabelByID(ctx, input.ProjectLabelID)
if err != nil {
return &db.ProjectLabel{}, err
}
err = r.Repository.DeleteProjectLabelByID(ctx, input.ProjectLabelID)
return &label, err
}
func (r *mutationResolver) UpdateProjectLabel(ctx context.Context, input UpdateProjectLabel) (*db.ProjectLabel, error) {
label, err := r.Repository.UpdateProjectLabel(ctx, db.UpdateProjectLabelParams{ProjectLabelID: input.ProjectLabelID, LabelColorID: input.LabelColorID, Name: sql.NullString{String: input.Name, Valid: true}})
return &label, err
}
func (r *mutationResolver) UpdateProjectLabelName(ctx context.Context, input UpdateProjectLabelName) (*db.ProjectLabel, error) {
label, err := r.Repository.UpdateProjectLabelName(ctx, db.UpdateProjectLabelNameParams{ProjectLabelID: input.ProjectLabelID, Name: sql.NullString{String: input.Name, Valid: true}})
return &label, err
}
func (r *mutationResolver) UpdateProjectLabelColor(ctx context.Context, input UpdateProjectLabelColor) (*db.ProjectLabel, error) {
label, err := r.Repository.UpdateProjectLabelColor(ctx, db.UpdateProjectLabelColorParams{ProjectLabelID: input.ProjectLabelID, LabelColorID: input.LabelColorID})
return &label, err
}
func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input InviteProjectMembers) (*InviteProjectMembersPayload, error) {
members := []Member{}
invitedMembers := []InvitedMember{}
for _, invitedMember := range input.Members {
if invitedMember.Email != nil && invitedMember.UserID != nil {
return &InviteProjectMembersPayload{Ok: false}, &gqlerror.Error{
Message: "Both email and userID can not be used to invite a project member",
Extensions: map[string]interface{}{
"code": "403",
},
}
} else if invitedMember.Email == nil && invitedMember.UserID == nil {
return &InviteProjectMembersPayload{Ok: false}, &gqlerror.Error{
Message: "Either email or userID must be set to invite a project member",
Extensions: map[string]interface{}{
"code": "403",
},
}
}
if invitedMember.UserID != nil {
// Invite by user ID
addedAt := time.Now().UTC()
_, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: *invitedMember.UserID, AddedAt: addedAt, RoleCode: "member"})
if err != nil {
return &InviteProjectMembersPayload{Ok: false}, err
}
user, err := r.Repository.GetUserAccountByID(ctx, *invitedMember.UserID)
if err != nil && err != sql.ErrNoRows {
return &InviteProjectMembersPayload{Ok: false}, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: *invitedMember.UserID, ProjectID: input.ProjectID})
if err != nil {
return &InviteProjectMembersPayload{Ok: false}, err
}
members = append(members, Member{
ID: *invitedMember.UserID,
FullName: user.FullName,
Username: user.Username,
ProfileIcon: profileIcon,
Role: &db.Role{Code: role.Code, Name: role.Name},
})
} else {
// Invite by email
// if invited user does not exist, create entry
invitedUser, err := r.Repository.GetInvitedUserByEmail(ctx, *invitedMember.Email)
now := time.Now().UTC()
if err != nil {
if err == sql.ErrNoRows {
invitedUser, err = r.Repository.CreateInvitedUser(ctx, *invitedMember.Email)
if err != nil {
return &InviteProjectMembersPayload{Ok: false}, err
}
confirmToken, err := r.Repository.CreateConfirmToken(ctx, *invitedMember.Email)
if err != nil {
return &InviteProjectMembersPayload{Ok: false}, err
}
invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()}
err = utils.SendEmailInvite(r.AppConfig.Email, invite)
if err != nil {
logger.New(ctx).WithError(err).Error("issue sending email")
return &InviteProjectMembersPayload{Ok: false}, err
}
} else {
return &InviteProjectMembersPayload{Ok: false}, err
}
}
_, err = r.Repository.CreateInvitedProjectMember(ctx, db.CreateInvitedProjectMemberParams{
ProjectID: input.ProjectID,
UserAccountInvitedID: invitedUser.UserAccountInvitedID,
})
if err != nil {
return &InviteProjectMembersPayload{Ok: false}, err
}
logger.New(ctx).Info("adding invited member")
invitedMembers = append(invitedMembers, InvitedMember{Email: *invitedMember.Email, InvitedOn: now})
}
}
return &InviteProjectMembersPayload{Ok: false, ProjectID: input.ProjectID, Members: members, InvitedMembers: invitedMembers}, nil
}
func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) {
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if err != nil {
return &DeleteProjectMemberPayload{Ok: false}, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: input.UserID, ProjectID: input.ProjectID})
if err != nil {
return &DeleteProjectMemberPayload{Ok: false}, err
}
err = r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{UserID: input.UserID, ProjectID: input.ProjectID})
if err != nil {
return &DeleteProjectMemberPayload{Ok: false}, err
}
return &DeleteProjectMemberPayload{Ok: true, Member: &Member{
ID: input.UserID,
FullName: user.FullName,
ProfileIcon: profileIcon,
Role: &db.Role{Code: role.Code, Name: role.Name},
}, ProjectID: input.ProjectID}, nil
}
func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) {
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if err != nil {
logger.New(ctx).WithError(err).Error("get user account")
return &UpdateProjectMemberRolePayload{Ok: false}, err
}
_, err = r.Repository.UpdateProjectMemberRole(ctx, db.UpdateProjectMemberRoleParams{ProjectID: input.ProjectID,
UserID: input.UserID, RoleCode: input.RoleCode.String()})
if err != nil {
logger.New(ctx).WithError(err).Error("update project member role")
return &UpdateProjectMemberRolePayload{Ok: false}, err
}
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: input.ProjectID})
if err != nil {
logger.New(ctx).WithError(err).Error("get role for project member")
return &UpdateProjectMemberRolePayload{Ok: false}, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
member := Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
Role: &db.Role{Code: role.Code, Name: role.Name},
}
return &UpdateProjectMemberRolePayload{Ok: true, Member: &member}, err
}
func (r *mutationResolver) DeleteInvitedProjectMember(ctx context.Context, input DeleteInvitedProjectMember) (*DeleteInvitedProjectMemberPayload, error) {
member, err := r.Repository.GetProjectMemberInvitedIDByEmail(ctx, input.Email)
if err != nil {
return &DeleteInvitedProjectMemberPayload{}, err
}
err = r.Repository.DeleteInvitedProjectMemberByID(ctx, member.ProjectMemberInvitedID)
if err != nil {
return &DeleteInvitedProjectMemberPayload{}, err
}
return &DeleteInvitedProjectMemberPayload{
InvitedMember: &InvitedMember{Email: member.Email, InvitedOn: member.InvitedOn},
}, nil
}
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &db.Project{}, errors.New("user id is missing")
}
createdAt := time.Now().UTC()
logger.New(ctx).WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project")
var project db.Project
var err error
if input.TeamID == nil {
project, err = r.Repository.CreatePersonalProject(ctx, db.CreatePersonalProjectParams{
CreatedAt: createdAt,
Name: input.Name,
})
if err != nil {
logger.New(ctx).WithError(err).Error("error while creating project")
return &db.Project{}, err
}
logger.New(ctx).WithField("projectID", project.ProjectID).Info("creating personal project link")
} else {
project, err = r.Repository.CreateTeamProject(ctx, db.CreateTeamProjectParams{
CreatedAt: createdAt,
Name: input.Name,
TeamID: *input.TeamID,
})
if err != nil {
logger.New(ctx).WithError(err).Error("error while creating project")
return &db.Project{}, err
}
}
_, err = r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: project.ProjectID, UserID: userID, AddedAt: createdAt, RoleCode: "admin"})
if err != nil {
logger.New(ctx).WithError(err).Error("error while creating initial project member")
return &db.Project{}, err
}
return &project, nil
}
func (r *mutationResolver) DeleteProject(ctx context.Context, input DeleteProject) (*DeleteProjectPayload, error) {
project, err := r.Repository.GetProjectByID(ctx, input.ProjectID)
if err != nil {
return &DeleteProjectPayload{Ok: false}, err
}
err = r.Repository.DeleteProjectByID(ctx, input.ProjectID)
if err != nil {
return &DeleteProjectPayload{Ok: false}, err
}
return &DeleteProjectPayload{Project: &project, Ok: true}, err
}
func (r *mutationResolver) UpdateProjectName(ctx context.Context, input *UpdateProjectName) (*db.Project, error) {
project, err := r.Repository.UpdateProjectNameByID(ctx, db.UpdateProjectNameByIDParams{ProjectID: input.ProjectID, Name: input.Name})
if err != nil {
return &db.Project{}, err
}
return &project, nil
}
func (r *mutationResolver) ToggleProjectVisibility(ctx context.Context, input ToggleProjectVisibility) (*ToggleProjectVisibilityPayload, error) {
if input.IsPublic {
project, err := r.Repository.SetPublicOn(ctx, db.SetPublicOnParams{ProjectID: input.ProjectID, PublicOn: sql.NullTime{Valid: true, Time: time.Now().UTC()}})
return &ToggleProjectVisibilityPayload{Project: &project}, err
}
project, err := r.Repository.SetPublicOn(ctx, db.SetPublicOnParams{ProjectID: input.ProjectID, PublicOn: sql.NullTime{Valid: false, Time: time.Time{}}})
return &ToggleProjectVisibilityPayload{Project: &project}, err
}
func (r *projectResolver) ID(ctx context.Context, obj *db.Project) (uuid.UUID, error) {
return obj.ProjectID, nil
}
func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, error) {
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
logger.New(ctx).WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
return &team, err
}
return &team, nil
}
func (r *projectResolver) TaskGroups(ctx context.Context, obj *db.Project) ([]db.TaskGroup, error) {
return r.Repository.GetTaskGroupsForProject(ctx, obj.ProjectID)
}
func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Member, error) {
members := []Member{}
projectMembers, err := r.Repository.GetProjectMembersForProjectID(ctx, obj.ProjectID)
if err != nil {
logger.New(ctx).WithError(err).Error("get project members for project id")
return members, err
}
for _, projectMember := range projectMembers {
user, err := r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
if err != nil {
logger.New(ctx).WithError(err).Error("get user account by ID")
return members, err
}
var url *string
if user.ProfileAvatarUrl.Valid {
url = &user.ProfileAvatarUrl.String
}
role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: obj.ProjectID})
if err != nil {
logger.New(ctx).WithError(err).Error("get role for projet member by user ID")
return members, err
}
profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
Username: user.Username, Role: &db.Role{Code: role.Code, Name: role.Name},
})
}
return members, nil
}
func (r *projectResolver) InvitedMembers(ctx context.Context, obj *db.Project) ([]InvitedMember, error) {
members, err := r.Repository.GetInvitedMembersForProjectID(ctx, obj.ProjectID)
if err != nil && err == sql.ErrNoRows {
return []InvitedMember{}, nil
}
invited := []InvitedMember{}
for _, member := range members {
invited = append(invited, InvitedMember{Email: member.Email, InvitedOn: member.InvitedOn})
}
return invited, err
}
func (r *projectResolver) PublicOn(ctx context.Context, obj *db.Project) (*time.Time, error) {
if obj.PublicOn.Valid {
return &obj.PublicOn.Time, nil
}
return nil, nil
}
func (r *projectResolver) Permission(ctx context.Context, obj *db.Project) (*ProjectPermission, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *projectResolver) Labels(ctx context.Context, obj *db.Project) ([]db.ProjectLabel, error) {
labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID)
return labels, err
}
func (r *projectLabelResolver) ID(ctx context.Context, obj *db.ProjectLabel) (uuid.UUID, error) {
return obj.ProjectLabelID, nil
}
func (r *projectLabelResolver) LabelColor(ctx context.Context, obj *db.ProjectLabel) (*db.LabelColor, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
if err != nil {
return &db.LabelColor{}, err
}
return &labelColor, nil
}
func (r *projectLabelResolver) Name(ctx context.Context, obj *db.ProjectLabel) (*string, error) {
var name *string
if obj.Name.Valid {
name = &obj.Name.String
}
return name, nil
}
func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) {
_, isLoggedIn := GetUser(ctx)
var projectID uuid.UUID
var err error
if input.ProjectID != nil {
projectID = *input.ProjectID
} else if input.ProjectShortID != nil {
projectID, err = r.Repository.GetProjectIDByShortID(ctx, *input.ProjectShortID)
if err != nil {
log.WithError(err).Error("error while getting project id by short id")
return &db.Project{}, err
}
} else {
return &db.Project{}, errors.New("FindProject requires either ProjectID or ProjectShortID to be set")
}
if !isLoggedIn {
isPublic, _ := IsProjectPublic(ctx, r.Repository, projectID)
if !isPublic {
return &db.Project{}, NotAuthorized()
}
}
project, err := r.Repository.GetProjectByID(ctx, projectID)
if err == sql.ErrNoRows {
return &db.Project{}, &gqlerror.Error{
Message: "Project not found",
Extensions: map[string]interface{}{
"code": "NOT_FOUND",
},
}
}
return &project, nil
}
// LabelColor returns LabelColorResolver implementation.
func (r *Resolver) LabelColor() LabelColorResolver { return &labelColorResolver{r} }
// Project returns ProjectResolver implementation.
func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} }
// ProjectLabel returns ProjectLabelResolver implementation.
func (r *Resolver) ProjectLabel() ProjectLabelResolver { return &projectLabelResolver{r} }
type labelColorResolver struct{ *Resolver }
type projectResolver struct{ *Resolver }
type projectLabelResolver struct{ *Resolver }

View File

@ -4,67 +4,15 @@
package graph package graph
import ( import (
"context" "sync"
"encoding/json"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/jobs"
"github.com/jordanknott/taskcafe/internal/utils" "github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus"
) )
// Resolver handles resolving GraphQL queries & mutations // Resolver handles resolving GraphQL queries & mutations
type Resolver struct { type Resolver struct {
Repository db.Repository Repository db.Repository
AppConfig config.AppConfig EmailConfig utils.EmailConfig
Notifications *NotificationObservers mu sync.Mutex
Job jobs.JobQueue
Redis *redis.Client
}
func (r Resolver) SubscribeRedis() {
ctx := context.Background()
go func() {
subscriber := r.Redis.Subscribe(ctx, "notification-created")
log.Info("Stream starting...")
for {
msg, err := subscriber.ReceiveMessage(ctx)
if err != nil {
log.WithError(err).Error("while receiving message")
panic(err)
}
var message utils.NotificationCreatedMessage
if err := json.Unmarshal([]byte(msg.Payload), &message); err != nil {
log.WithError(err).Error("while unmarshalling notifiction created message")
panic(err)
}
log.WithField("notID", message.NotifiedID).Info("received notification message")
notified, err := r.Repository.GetNotifiedByIDNoExtra(ctx, uuid.MustParse(message.NotifiedID))
if err != nil {
log.WithError(err).Error("while getting notified by id")
panic(err)
}
notification, err := r.Repository.GetNotificationByID(ctx, uuid.MustParse(message.NotificationID))
if err != nil {
log.WithError(err).Error("while getting notified by id")
panic(err)
}
for _, observers := range r.Notifications.Subscribers {
for _, ochan := range observers {
ochan <- &Notified{
ID: notified.NotifiedID,
Read: notified.Read,
ReadAt: &notified.ReadAt.Time,
Notification: &notification,
}
}
}
}
}()
} }

View File

@ -0,0 +1,997 @@
scalar Time
scalar UUID
scalar Upload
enum RoleCode {
owner
admin
member
observer
}
type ProjectLabel {
id: ID!
createdDate: Time!
labelColor: LabelColor!
name: String
}
type LabelColor {
id: ID!
name: String!
position: Float!
colorHex: String!
}
type TaskLabel {
id: ID!
projectLabel: ProjectLabel!
assignedDate: Time!
}
type ProfileIcon {
url: String
initials: String
bgColor: String
}
type OwnersList {
projects: [UUID!]!
teams: [UUID!]!
}
type Member {
id: ID!
role: Role!
fullName: String!
username: String!
profileIcon: ProfileIcon!
owned: OwnedList!
member: MemberList!
}
type Role {
code: String!
name: String!
}
type OwnedList {
teams: [Team!]!
projects: [Project!]!
}
type MemberList {
teams: [Team!]!
projects: [Project!]!
}
type UserAccount {
id: ID!
email: String!
createdAt: Time!
fullName: String!
initials: String!
bio: String!
role: Role!
username: String!
profileIcon: ProfileIcon!
owned: OwnedList!
member: MemberList!
}
type InvitedUserAccount {
id: ID!
email: String!
invitedOn: Time!
member: MemberList!
}
type Team {
id: ID!
createdAt: Time!
name: String!
permission: TeamPermission!
members: [Member!]!
}
type InvitedMember {
email: String!
invitedOn: Time!
}
type TeamPermission {
team: RoleCode!
org: RoleCode!
}
type ProjectPermission {
team: RoleCode!
project: RoleCode!
org: RoleCode!
}
type Project {
id: ID!
createdAt: Time!
name: String!
team: Team
taskGroups: [TaskGroup!]!
members: [Member!]!
invitedMembers: [InvitedMember!]!
publicOn: Time
permission: ProjectPermission!
labels: [ProjectLabel!]!
}
type TaskGroup {
id: ID!
projectID: String!
createdAt: Time!
name: String!
position: Float!
tasks: [Task!]!
}
type ChecklistBadge {
complete: Int!
total: Int!
}
type TaskBadges {
checklist: ChecklistBadge
}
type CausedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon
}
type TaskActivityData {
name: String!
value: String!
}
enum ActivityType {
TASK_ADDED
TASK_MOVED
TASK_MARKED_COMPLETE
TASK_MARKED_INCOMPLETE
TASK_DUE_DATE_CHANGED
TASK_DUE_DATE_ADDED
TASK_DUE_DATE_REMOVED
TASK_CHECKLIST_CHANGED
TASK_CHECKLIST_ADDED
TASK_CHECKLIST_REMOVED
}
type TaskActivity {
id: ID!
type: ActivityType!
data: [TaskActivityData!]!
causedBy: CausedBy!
createdAt: Time!
}
type Task {
id: ID!
taskGroup: TaskGroup!
createdAt: Time!
name: String!
position: Float!
description: String
dueDate: Time
hasTime: Boolean!
complete: Boolean!
completedAt: Time
assigned: [Member!]!
labels: [TaskLabel!]!
checklists: [TaskChecklist!]!
badges: TaskBadges!
activity: [TaskActivity!]!
comments: [TaskComment!]!
}
type CreatedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon!
}
type TaskComment {
id: ID!
createdAt: Time!
updatedAt: Time
message: String!
createdBy: CreatedBy!
pinned: Boolean!
}
type Organization {
id: ID!
name: String!
}
type TaskChecklistItem {
id: ID!
name: String!
taskChecklistID: UUID!
complete: Boolean!
position: Float!
dueDate: Time!
}
type TaskChecklist {
id: ID!
name: String!
position: Float!
items: [TaskChecklistItem!]!
}
enum ShareStatus {
INVITED
JOINED
}
enum RoleLevel {
ADMIN
MEMBER
}
enum ActionLevel {
ORG
TEAM
PROJECT
}
enum ObjectType {
ORG
TEAM
PROJECT
TASK
TASK_GROUP
TASK_CHECKLIST
TASK_CHECKLIST_ITEM
}
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
directive @requiresUser on FIELD_DEFINITION
type Query {
organizations: [Organization!]!
users: [UserAccount!]!
invitedUsers: [InvitedUserAccount!]!
findUser(input: FindUser!): UserAccount!
findProject(input: FindProject!):
Project!
findTask(input: FindTask!): Task!
projects(input: ProjectsFilter): [Project!]!
findTeam(input: FindTeam!): Team!
teams: [Team!]!
myTasks(input: MyTasks!): MyTasksPayload!
labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]!
me: MePayload
}
type Mutation
enum MyTasksStatus {
ALL
INCOMPLETE
COMPLETE_ALL
COMPLETE_TODAY
COMPLETE_YESTERDAY
COMPLETE_ONE_WEEK
COMPLETE_TWO_WEEK
COMPLETE_THREE_WEEK
}
enum MyTasksSort {
NONE
PROJECT
DUE_DATE
}
input MyTasks {
status: MyTasksStatus!
sort: MyTasksSort!
}
type ProjectTaskMapping {
projectID: UUID!
taskID: UUID!
}
type MyTasksPayload {
tasks: [Task!]!
projects: [ProjectTaskMapping!]!
}
type TeamRole {
teamID: UUID!
roleCode: RoleCode!
}
type ProjectRole {
projectID: UUID!
roleCode: RoleCode!
}
type MePayload {
user: UserAccount!
organization: RoleCode
teamRoles: [TeamRole!]!
projectRoles: [ProjectRole!]!
}
input ProjectsFilter {
teamID: UUID
}
input FindUser {
userID: UUID!
}
input FindProject {
projectID: UUID!
}
input FindTask {
taskID: UUID!
}
input FindTeam {
teamID: UUID!
}
extend type Query {
notifications: [Notification!]!
}
enum EntityType {
TASK
}
enum ActorType {
USER
}
enum ActionType {
TASK_MEMBER_ADDED
}
type NotificationActor {
id: UUID!
type: ActorType!
name: String!
}
type NotificationEntity {
id: UUID!
type: EntityType!
name: String!
}
type Notification {
id: ID!
entity: NotificationEntity!
actionType: ActionType!
actor: NotificationActor!
read: Boolean!
createdAt: Time!
}
extend type Mutation {
createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
deleteProject(input: DeleteProject!):
DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectName(input: UpdateProjectName):
Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
toggleProjectVisibility(input: ToggleProjectVisibility!): ToggleProjectVisibilityPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input ToggleProjectVisibility {
projectID: UUID!
isPublic: Boolean!
}
type ToggleProjectVisibilityPayload {
project: Project!
}
input NewProject {
teamID: UUID
name: String!
}
input UpdateProjectName {
projectID: UUID!
name: String!
}
input DeleteProject {
projectID: UUID!
}
type DeleteProjectPayload {
ok: Boolean!
project: Project!
}
extend type Mutation {
createProjectLabel(input: NewProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
deleteProjectLabel(input: DeleteProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
updateProjectLabel(input: UpdateProjectLabel!):
ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
updateProjectLabelName(input: UpdateProjectLabelName!):
ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
updateProjectLabelColor(input: UpdateProjectLabelColor!):
ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
}
input NewProjectLabel {
projectID: UUID!
labelColorID: UUID!
name: String
}
input DeleteProjectLabel {
projectLabelID: UUID!
}
input UpdateProjectLabelName {
projectLabelID: UUID!
name: String!
}
input UpdateProjectLabel {
projectLabelID: UUID!
labelColorID: UUID!
name: String!
}
input UpdateProjectLabelColor {
projectLabelID: UUID!
labelColorID: UUID!
}
extend type Mutation {
inviteProjectMembers(input: InviteProjectMembers!):
InviteProjectMembersPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteProjectMember(input: DeleteProjectMember!):
DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
updateProjectMemberRole(input: UpdateProjectMemberRole!):
UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
deleteInvitedProjectMember(input: DeleteInvitedProjectMember!):
DeleteInvitedProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
}
input DeleteInvitedProjectMember {
projectID: UUID!
email: String!
}
type DeleteInvitedProjectMemberPayload {
invitedMember: InvitedMember!
}
input MemberInvite {
userID: UUID
email: String
}
input InviteProjectMembers {
projectID: UUID!
members: [MemberInvite!]!
}
type InviteProjectMembersPayload {
ok: Boolean!
projectID: UUID!
members: [Member!]!
invitedMembers: [InvitedMember!]!
}
input DeleteProjectMember {
projectID: UUID!
userID: UUID!
}
type DeleteProjectMemberPayload {
ok: Boolean!
member: Member!
projectID: UUID!
}
input UpdateProjectMemberRole {
projectID: UUID!
userID: UUID!
roleCode: RoleCode!
}
type UpdateProjectMemberRolePayload {
ok: Boolean!
member: Member!
}
extend type Mutation {
createTask(input: NewTask!):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
deleteTask(input: DeleteTaskInput!):
DeleteTaskPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
updateTaskDescription(input: UpdateTaskDescriptionInput!):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
updateTaskLocation(input: NewTaskLocation!):
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
updateTaskName(input: UpdateTaskName!):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
setTaskComplete(input: SetTaskComplete!):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
updateTaskDueDate(input: UpdateTaskDueDate!):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
assignTask(input: AssignTaskInput):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
unassignTask(input: UnassignTaskInput):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
}
input NewTask {
taskGroupID: UUID!
name: String!
position: Float!
assigned: [UUID!]
}
input AssignTaskInput {
taskID: UUID!
userID: UUID!
}
input UnassignTaskInput {
taskID: UUID!
userID: UUID!
}
input UpdateTaskDescriptionInput {
taskID: UUID!
description: String!
}
type UpdateTaskLocationPayload {
previousTaskGroupID: UUID!
task: Task!
}
input UpdateTaskDueDate {
taskID: UUID!
hasTime: Boolean!
dueDate: Time
}
input SetTaskComplete {
taskID: UUID!
complete: Boolean!
}
input NewTaskLocation {
taskID: UUID!
taskGroupID: UUID!
position: Float!
}
input DeleteTaskInput {
taskID: UUID!
}
type DeleteTaskPayload {
taskID: UUID!
}
input UpdateTaskName {
taskID: UUID!
name: String!
}
extend type Mutation {
createTaskChecklist(input: CreateTaskChecklist!):
TaskChecklist! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
deleteTaskChecklist(input: DeleteTaskChecklist!):
DeleteTaskChecklistPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST)
updateTaskChecklistName(input: UpdateTaskChecklistName!):
TaskChecklist! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST)
createTaskChecklistItem(input: CreateTaskChecklistItem!):
TaskChecklistItem! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST)
updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!):
UpdateTaskChecklistLocationPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST)
updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!):
TaskChecklistItem! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST_ITEM)
setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!):
TaskChecklistItem! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST_ITEM)
deleteTaskChecklistItem(input: DeleteTaskChecklistItem!):
DeleteTaskChecklistItemPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST_ITEM)
updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!):
UpdateTaskChecklistItemLocationPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_CHECKLIST_ITEM)
}
input UpdateTaskChecklistItemLocation {
taskChecklistID: UUID!
taskChecklistItemID: UUID!
position: Float!
}
type UpdateTaskChecklistItemLocationPayload {
taskChecklistID: UUID!
prevChecklistID: UUID!
checklistItem: TaskChecklistItem!
}
input UpdateTaskChecklistLocation {
taskChecklistID: UUID!
position: Float!
}
type UpdateTaskChecklistLocationPayload {
checklist: TaskChecklist!
}
input CreateTaskChecklist {
taskID: UUID!
name: String!
position: Float!
}
type DeleteTaskChecklistItemPayload {
ok: Boolean!
taskChecklistItem: TaskChecklistItem!
}
input CreateTaskChecklistItem {
taskChecklistID: UUID!
name: String!
position: Float!
}
input SetTaskChecklistItemComplete {
taskChecklistItemID: UUID!
complete: Boolean!
}
input DeleteTaskChecklistItem {
taskChecklistItemID: UUID!
}
input UpdateTaskChecklistItemName {
taskChecklistItemID: UUID!
name: String!
}
input UpdateTaskChecklistName {
taskChecklistID: UUID!
name: String!
}
input DeleteTaskChecklist {
taskChecklistID: UUID!
}
type DeleteTaskChecklistPayload {
ok: Boolean!
taskChecklist: TaskChecklist!
}
extend type Mutation {
createTaskComment(input: CreateTaskComment):
CreateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
deleteTaskComment(input: DeleteTaskComment):
DeleteTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
updateTaskComment(input: UpdateTaskComment):
UpdateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
}
input CreateTaskComment {
taskID: UUID!
message: String!
}
type CreateTaskCommentPayload {
taskID: UUID!
comment: TaskComment!
}
input UpdateTaskComment {
commentID: UUID!
message: String!
}
type UpdateTaskCommentPayload {
taskID: UUID!
comment: TaskComment!
}
input DeleteTaskComment {
commentID: UUID!
}
type DeleteTaskCommentPayload {
taskID: UUID!
commentID: UUID!
}
extend type Mutation {
createTaskGroup(input: NewTaskGroup!):
TaskGroup! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
updateTaskGroupLocation(input: NewTaskGroupLocation!):
TaskGroup! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
updateTaskGroupName(input: UpdateTaskGroupName!):
TaskGroup! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
deleteTaskGroup(input: DeleteTaskGroupInput!):
DeleteTaskGroupPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
duplicateTaskGroup(input: DuplicateTaskGroup!):
DuplicateTaskGroupPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
sortTaskGroup(input: SortTaskGroup!):
SortTaskGroupPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
deleteTaskGroupTasks(input: DeleteTaskGroupTasks!):
DeleteTaskGroupTasksPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK_GROUP)
}
input DeleteTaskGroupTasks {
taskGroupID: UUID!
}
type DeleteTaskGroupTasksPayload {
taskGroupID: UUID!
tasks: [UUID!]!
}
input TaskPositionUpdate {
taskID: UUID!
position: Float!
}
type SortTaskGroupPayload {
taskGroupID: UUID!
tasks: [Task!]!
}
input SortTaskGroup {
taskGroupID: UUID!
tasks: [TaskPositionUpdate!]!
}
input DuplicateTaskGroup {
projectID: UUID!
taskGroupID: UUID!
name: String!
position: Float!
}
type DuplicateTaskGroupPayload {
taskGroup: TaskGroup!
}
input NewTaskGroupLocation {
taskGroupID: UUID!
position: Float!
}
input UpdateTaskGroupName {
taskGroupID: UUID!
name: String!
}
input DeleteTaskGroupInput {
taskGroupID: UUID!
}
type DeleteTaskGroupPayload {
ok: Boolean!
affectedRows: Int!
taskGroup: TaskGroup!
}
input NewTaskGroup {
projectID: UUID!
name: String!
position: Float!
}
extend type Mutation {
addTaskLabel(input: AddTaskLabelInput):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
removeTaskLabel(input: RemoveTaskLabelInput):
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
toggleTaskLabel(input: ToggleTaskLabelInput!):
ToggleTaskLabelPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
}
input AddTaskLabelInput {
taskID: UUID!
projectLabelID: UUID!
}
input RemoveTaskLabelInput {
taskID: UUID!
taskLabelID: UUID!
}
input ToggleTaskLabelInput {
taskID: UUID!
projectLabelID: UUID!
}
type ToggleTaskLabelPayload {
active: Boolean!
task: Task!
}
extend type Mutation {
deleteTeam(input: DeleteTeam!):
DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
createTeam(input: NewTeam!):
Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
input NewTeam {
name: String!
organizationID: UUID!
}
input DeleteTeam {
teamID: UUID!
}
type DeleteTeamPayload {
ok: Boolean!
team: Team!
projects: [Project!]!
}
extend type Mutation {
createTeamMember(input: CreateTeamMember!):
CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
updateTeamMemberRole(input: UpdateTeamMemberRole!):
UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
deleteTeamMember(input: DeleteTeamMember!):
DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
}
input DeleteTeamMember {
teamID: UUID!
userID: UUID!
newOwnerID: UUID
}
type DeleteTeamMemberPayload {
teamID: UUID!
userID: UUID!
affectedProjects: [Project!]!
}
input CreateTeamMember {
userID: UUID!
teamID: UUID!
}
type CreateTeamMemberPayload {
team: Team!
teamMember: Member!
}
input UpdateTeamMemberRole {
teamID: UUID!
userID: UUID!
roleCode: RoleCode!
}
type UpdateTeamMemberRolePayload {
ok: Boolean!
teamID: UUID!
member: Member!
}
extend type Mutation {
createUserAccount(input: NewUserAccount!):
UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
deleteUserAccount(input: DeleteUserAccount!):
DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
deleteInvitedUserAccount(input: DeleteInvitedUserAccount!):
DeleteInvitedUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
logoutUser(input: LogoutUser!): Boolean!
clearProfileAvatar: UserAccount!
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!):
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
updateUserInfo(input: UpdateUserInfo!):
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
extend type Query {
searchMembers(input: MemberSearchFilter!): [MemberSearchResult!]!
}
input DeleteInvitedUserAccount {
invitedUserID: UUID!
}
type DeleteInvitedUserAccountPayload {
invitedUser: InvitedUserAccount!
}
input MemberSearchFilter {
searchFilter: String!
projectID: UUID
}
type MemberSearchResult {
similarity: Int!
id: String!
user: UserAccount
status: ShareStatus!
}
type UpdateUserInfoPayload {
user: UserAccount!
}
input UpdateUserInfo {
name: String!
initials: String!
email: String!
bio: String!
}
input UpdateUserPassword {
userID: UUID!
password: String!
}
type UpdateUserPasswordPayload {
ok: Boolean!
user: UserAccount!
}
input UpdateUserRole {
userID: UUID!
roleCode: RoleCode!
}
type UpdateUserRolePayload {
user: UserAccount!
}
input NewUserAccount {
username: String!
email: String!
fullName: String!
initials: String!
password: String!
roleCode: String!
}
input LogoutUser {
userID: UUID!
}
input DeleteUserAccount {
userID: UUID!
newOwnerID: UUID
}
type DeleteUserAccountPayload {
ok: Boolean!
userAccount: UserAccount!
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,230 @@
scalar Time
scalar UUID
scalar Upload
enum RoleCode {
owner
admin
member
observer
}
type ProjectLabel {
id: ID!
createdDate: Time!
labelColor: LabelColor!
name: String
}
type LabelColor {
id: ID!
name: String!
position: Float!
colorHex: String!
}
type TaskLabel {
id: ID!
projectLabel: ProjectLabel!
assignedDate: Time!
}
type ProfileIcon {
url: String
initials: String
bgColor: String
}
type OwnersList {
projects: [UUID!]!
teams: [UUID!]!
}
type Member {
id: ID!
role: Role!
fullName: String!
username: String!
profileIcon: ProfileIcon!
owned: OwnedList!
member: MemberList!
}
type Role {
code: String!
name: String!
}
type OwnedList {
teams: [Team!]!
projects: [Project!]!
}
type MemberList {
teams: [Team!]!
projects: [Project!]!
}
type UserAccount {
id: ID!
email: String!
createdAt: Time!
fullName: String!
initials: String!
bio: String!
role: Role!
username: String!
profileIcon: ProfileIcon!
owned: OwnedList!
member: MemberList!
}
type InvitedUserAccount {
id: ID!
email: String!
invitedOn: Time!
member: MemberList!
}
type Team {
id: ID!
createdAt: Time!
name: String!
permission: TeamPermission!
members: [Member!]!
}
type InvitedMember {
email: String!
invitedOn: Time!
}
type TeamPermission {
team: RoleCode!
org: RoleCode!
}
type ProjectPermission {
team: RoleCode!
project: RoleCode!
org: RoleCode!
}
type Project {
id: ID!
createdAt: Time!
name: String!
team: Team
taskGroups: [TaskGroup!]!
members: [Member!]!
invitedMembers: [InvitedMember!]!
publicOn: Time
permission: ProjectPermission!
labels: [ProjectLabel!]!
}
type TaskGroup {
id: ID!
projectID: String!
createdAt: Time!
name: String!
position: Float!
tasks: [Task!]!
}
type ChecklistBadge {
complete: Int!
total: Int!
}
type TaskBadges {
checklist: ChecklistBadge
}
type CausedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon
}
type TaskActivityData {
name: String!
value: String!
}
enum ActivityType {
TASK_ADDED
TASK_MOVED
TASK_MARKED_COMPLETE
TASK_MARKED_INCOMPLETE
TASK_DUE_DATE_CHANGED
TASK_DUE_DATE_ADDED
TASK_DUE_DATE_REMOVED
TASK_CHECKLIST_CHANGED
TASK_CHECKLIST_ADDED
TASK_CHECKLIST_REMOVED
}
type TaskActivity {
id: ID!
type: ActivityType!
data: [TaskActivityData!]!
causedBy: CausedBy!
createdAt: Time!
}
type Task {
id: ID!
taskGroup: TaskGroup!
createdAt: Time!
name: String!
position: Float!
description: String
dueDate: Time
hasTime: Boolean!
complete: Boolean!
completedAt: Time
assigned: [Member!]!
labels: [TaskLabel!]!
checklists: [TaskChecklist!]!
badges: TaskBadges!
activity: [TaskActivity!]!
comments: [TaskComment!]!
}
type CreatedBy {
id: ID!
fullName: String!
profileIcon: ProfileIcon!
}
type TaskComment {
id: ID!
createdAt: Time!
updatedAt: Time
message: String!
createdBy: CreatedBy!
pinned: Boolean!
}
type Organization {
id: ID!
name: String!
}
type TaskChecklistItem {
id: ID!
name: String!
taskChecklistID: UUID!
complete: Boolean!
position: Float!
dueDate: Time!
}
type TaskChecklist {
id: ID!
name: String!
position: Float!
items: [TaskChecklistItem!]!
}

View File

@ -1,45 +1,3 @@
scalar Time
scalar UUID
scalar Upload
enum RoleCode {
owner
admin
member
observer
}
type Role {
code: String!
name: String!
}
type ProfileIcon {
url: String
initials: String
bgColor: String
}
type OwnersList {
projects: [UUID!]!
teams: [UUID!]!
}
type OwnedList {
teams: [Team!]!
projects: [Project!]!
}
type MemberList {
teams: [Team!]!
projects: [Project!]!
}
type Organization {
id: ID!
name: String!
}
enum ShareStatus { enum ShareStatus {
INVITED INVITED
JOINED JOINED
@ -67,7 +25,6 @@ enum ObjectType {
} }
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
directive @requiresUser on FIELD_DEFINITION directive @requiresUser on FIELD_DEFINITION
type Query { type Query {
@ -75,9 +32,12 @@ type Query {
users: [UserAccount!]! users: [UserAccount!]!
invitedUsers: [InvitedUserAccount!]! invitedUsers: [InvitedUserAccount!]!
findUser(input: FindUser!): UserAccount! findUser(input: FindUser!): UserAccount!
findProject(input: FindProject!):
Project!
findTask(input: FindTask!): Task!
projects(input: ProjectsFilter): [Project!]! projects(input: ProjectsFilter): [Project!]!
teams: [Team!]!
findTeam(input: FindTeam!): Team! findTeam(input: FindTeam!): Team!
teams: [Team!]!
myTasks(input: MyTasks!): MyTasksPayload! myTasks(input: MyTasks!): MyTasksPayload!
labelColors: [LabelColor!]! labelColors: [LabelColor!]!
taskGroups: [TaskGroup!]! taskGroups: [TaskGroup!]!
@ -85,7 +45,6 @@ type Query {
} }
type Subscription
type Mutation type Mutation
enum MyTasksStatus { enum MyTasksStatus {
@ -145,6 +104,13 @@ input FindUser {
userID: UUID! userID: UUID!
} }
input FindProject {
projectID: UUID!
}
input FindTask {
taskID: UUID!
}
input FindTeam { input FindTeam {
teamID: UUID! teamID: UUID!

91
internal/graph/schema/notification.gql Executable file → Normal file
View File

@ -1,93 +1,36 @@
extend type Subscription {
notificationAdded: Notified!
}
extend type Query { extend type Query {
notifications: [Notified!]! notifications: [Notification!]!
notified(input: NotifiedInput!): NotifiedResult!
hasUnreadNotifications: HasUnreadNotificationsResult!
} }
extend type Mutation { enum EntityType {
notificationToggleRead(input: NotificationToggleReadInput!): Notified! TASK
notificationMarkAllRead: NotificationMarkAllAsReadResult!
}
type NotificationMarkAllAsReadResult {
success: Boolean!
} }
type HasUnreadNotificationsResult { enum ActorType {
unread: Boolean! USER
}
input NotificationToggleReadInput {
notifiedID: UUID!
}
input NotifiedInput {
limit: Int!
cursor: String
filter: NotificationFilter!
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
}
type NotifiedResult {
totalCount: Int!
notified: [Notified!]!
pageInfo: PageInfo!
} }
enum ActionType { enum ActionType {
TEAM_ADDED TASK_MEMBER_ADDED
TEAM_REMOVED
PROJECT_ADDED
PROJECT_REMOVED
PROJECT_ARCHIVED
DUE_DATE_ADDED
DUE_DATE_REMOVED
DUE_DATE_CHANGED
DUE_DATE_REMINDER
TASK_ASSIGNED
TASK_MOVED
TASK_ARCHIVED
TASK_ATTACHMENT_UPLOADED
COMMENT_MENTIONED
COMMENT_OTHER
} }
enum NotificationFilter { type NotificationActor {
ALL id: UUID!
UNREAD type: ActorType!
ASSIGNED name: String!
MENTIONED
} }
type NotificationData { type NotificationEntity {
key: String! id: UUID!
value: String! type: EntityType!
} name: String!
type NotificationCausedBy {
fullname: String!
username: String!
id: ID!
} }
type Notification { type Notification {
id: ID! id: ID!
entity: NotificationEntity!
actionType: ActionType! actionType: ActionType!
causedBy: NotificationCausedBy actor: NotificationActor!
data: [NotificationData!]! read: Boolean!
createdAt: Time! createdAt: Time!
} }
type Notified {
id: ID!
notification: Notification!
read: Boolean!
readAt: Time
}

Some files were not shown because too many files have changed in this diff Show More