Compare commits
11 Commits
feat/list-
...
feat/updat
Author | SHA1 | Date | |
---|---|---|---|
4a8d4a6ec3 | |||
e4d1e21304 | |||
f7c6ee470e | |||
227ce5966d | |||
aa5e1c0661 | |||
b603081691 | |||
e76ea9da63 | |||
923d7f7372 | |||
009d717d80 | |||
4272fefa28 | |||
25f5cad557 |
@ -3,11 +3,10 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
name: eslint
|
name: eslint
|
||||||
entry: go run cmd/mage/main.go frontend:lint
|
entry: scripts/lint.sh
|
||||||
language: system
|
language: system
|
||||||
files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
||||||
types: [file]
|
types: [file]
|
||||||
pass_filenames: false
|
|
||||||
- hooks:
|
- hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- Task sorting & filtering
|
- Task sorting & filtering
|
||||||
- Redesigned the Task Details UI
|
- Redesigned the Task Details UI
|
||||||
|
- Implement task group actions (duplicate/delete all tasks/sort)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- removed CORS middleware to fix security issue
|
- removed CORS middleware to fix security issue
|
||||||
|
@ -21,6 +21,8 @@ Was this project useful? Please consider <a href="https://www.buymeacoffee.com/j
|
|||||||
|
|
||||||
**Please note that this project is still in active development. Some options may not work yet! For updates on development, join the Discord server**
|
**Please note that this project is still in active development. Some options may not work yet! For updates on development, join the Discord server**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Currently Taskcafe only offers basic task tracking through a Kanban board.
|
Currently Taskcafe only offers basic task tracking through a Kanban board.
|
||||||
|
@ -171,7 +171,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
|
|||||||
|
|
||||||
const AdminRoute = () => {
|
const AdminRoute = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Taskcafé | Admin';
|
document.title = 'Admin | Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
const { loading, data } = useUsersQuery();
|
const { loading, data } = useUsersQuery();
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
|
@ -3,11 +3,20 @@ import styled from 'styled-components/macro';
|
|||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
import { getAccessToken } from 'shared/utils/accessToken';
|
import { getAccessToken } from 'shared/utils/accessToken';
|
||||||
import Settings from 'shared/components/Settings';
|
import Settings from 'shared/components/Settings';
|
||||||
import { useMeQuery, useClearProfileAvatarMutation, useUpdateUserPasswordMutation } from 'shared/generated/graphql';
|
import {
|
||||||
|
useMeQuery,
|
||||||
|
useClearProfileAvatarMutation,
|
||||||
|
useUpdateUserPasswordMutation,
|
||||||
|
useUpdateUserInfoMutation,
|
||||||
|
MeQuery,
|
||||||
|
MeDocument,
|
||||||
|
} from 'shared/generated/graphql';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
|
import produce from 'immer';
|
||||||
|
|
||||||
const MainContent = styled.div`
|
const MainContent = styled.div`
|
||||||
padding: 0 0 50px 80px;
|
padding: 0 0 50px 80px;
|
||||||
@ -19,6 +28,7 @@ const Projects = () => {
|
|||||||
const $fileUpload = useRef<HTMLInputElement>(null);
|
const $fileUpload = useRef<HTMLInputElement>(null);
|
||||||
const [clearProfileAvatar] = useClearProfileAvatarMutation();
|
const [clearProfileAvatar] = useClearProfileAvatarMutation();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const [updateUserInfo] = useUpdateUserInfoMutation();
|
||||||
const [updateUserPassword] = useUpdateUserPasswordMutation();
|
const [updateUserPassword] = useUpdateUserPasswordMutation();
|
||||||
const { loading, data, refetch } = useMeQuery();
|
const { loading, data, refetch } = useMeQuery();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -69,6 +79,13 @@ const Projects = () => {
|
|||||||
toast('Password was changed!');
|
toast('Password was changed!');
|
||||||
done();
|
done();
|
||||||
}}
|
}}
|
||||||
|
onChangeUserInfo={(d, done) => {
|
||||||
|
updateUserInfo({
|
||||||
|
variables: { name: d.full_name, bio: d.bio, email: d.email, initials: d.initials },
|
||||||
|
});
|
||||||
|
toast('User info was saved!');
|
||||||
|
done();
|
||||||
|
}}
|
||||||
onProfileAvatarRemove={() => {
|
onProfileAvatarRemove={() => {
|
||||||
clearProfileAvatar();
|
clearProfileAvatar();
|
||||||
}}
|
}}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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/components/Lists';
|
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
|
||||||
|
|
||||||
export const ActionsList = styled.ul`
|
export const ActionsList = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
useSetTaskCompleteMutation,
|
useSetTaskCompleteMutation,
|
||||||
useToggleTaskLabelMutation,
|
useToggleTaskLabelMutation,
|
||||||
useFindProjectQuery,
|
useFindProjectQuery,
|
||||||
|
useSortTaskGroupMutation,
|
||||||
useUpdateTaskGroupNameMutation,
|
useUpdateTaskGroupNameMutation,
|
||||||
useUpdateTaskNameMutation,
|
useUpdateTaskNameMutation,
|
||||||
useCreateTaskMutation,
|
useCreateTaskMutation,
|
||||||
@ -21,6 +22,10 @@ import {
|
|||||||
useUnassignTaskMutation,
|
useUnassignTaskMutation,
|
||||||
useUpdateTaskDueDateMutation,
|
useUpdateTaskDueDateMutation,
|
||||||
FindProjectQuery,
|
FindProjectQuery,
|
||||||
|
useDuplicateTaskGroupMutation,
|
||||||
|
DuplicateTaskGroupMutation,
|
||||||
|
DuplicateTaskGroupDocument,
|
||||||
|
useDeleteTaskGroupTasksMutation,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
|
|
||||||
import QuickCardEditor from 'shared/components/QuickCardEditor';
|
import QuickCardEditor from 'shared/components/QuickCardEditor';
|
||||||
@ -33,10 +38,8 @@ import SimpleLists, {
|
|||||||
TaskMeta,
|
TaskMeta,
|
||||||
TaskMetaMatch,
|
TaskMetaMatch,
|
||||||
TaskMetaFilters,
|
TaskMetaFilters,
|
||||||
TaskSorting,
|
|
||||||
TaskSortingType,
|
|
||||||
TaskSortingDirection,
|
|
||||||
} from 'shared/components/Lists';
|
} from 'shared/components/Lists';
|
||||||
|
import { TaskSorting, TaskSortingType, TaskSortingDirection, sortTasks } from 'shared/utils/sorting';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
import DueDateManager from 'shared/components/DueDateManager';
|
import DueDateManager from 'shared/components/DueDateManager';
|
||||||
@ -44,6 +47,7 @@ import EmptyBoard from 'shared/components/EmptyBoard';
|
|||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import LabelManagerEditor from 'Projects/Project/LabelManagerEditor';
|
import LabelManagerEditor from 'Projects/Project/LabelManagerEditor';
|
||||||
import Chip from 'shared/components/Chip';
|
import Chip from 'shared/components/Chip';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import FilterStatus from './FilterStatus';
|
import FilterStatus from './FilterStatus';
|
||||||
import FilterMeta from './FilterMeta';
|
import FilterMeta from './FilterMeta';
|
||||||
@ -263,6 +267,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
const [taskMetaFilters, setTaskMetaFilters] = useState(initTaskMetaFilters);
|
const [taskMetaFilters, setTaskMetaFilters] = useState(initTaskMetaFilters);
|
||||||
const [taskSorting, setTaskSorting] = useState(initTaskSorting);
|
const [taskSorting, setTaskSorting] = useState(initTaskSorting);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const [sortTaskGroup] = useSortTaskGroupMutation({
|
||||||
|
onCompleted: () => {
|
||||||
|
toast('List was sorted');
|
||||||
|
},
|
||||||
|
});
|
||||||
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
|
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
|
||||||
update: (client, deletedTaskGroupData) => {
|
update: (client, deletedTaskGroupData) => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
@ -314,6 +323,37 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
|
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
|
||||||
const { loading, data } = useFindProjectQuery({
|
const { loading, data } = useFindProjectQuery({
|
||||||
variables: { projectID },
|
variables: { projectID },
|
||||||
|
pollInterval: 5000,
|
||||||
|
});
|
||||||
|
const [deleteTaskGroupTasks] = useDeleteTaskGroupTasksMutation({
|
||||||
|
update: (client, resp) =>
|
||||||
|
updateApolloCache<FindProjectQuery>(
|
||||||
|
client,
|
||||||
|
FindProjectDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
const idx = cache.findProject.taskGroups.findIndex(
|
||||||
|
t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
|
||||||
|
);
|
||||||
|
if (idx !== -1) {
|
||||||
|
draftCache.findProject.taskGroups[idx].tasks = [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ projectID },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const [duplicateTaskGroup] = useDuplicateTaskGroupMutation({
|
||||||
|
update: (client, resp) => {
|
||||||
|
updateApolloCache<FindProjectQuery>(
|
||||||
|
client,
|
||||||
|
FindProjectDocument,
|
||||||
|
cache =>
|
||||||
|
produce(cache, draftCache => {
|
||||||
|
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
||||||
|
}),
|
||||||
|
{ projectID },
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
|
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
|
||||||
@ -624,15 +664,44 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
|
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
<Popup title="List actions" tab={0} onClose={() => hidePopup()}>
|
<ListActions
|
||||||
<ListActions
|
taskGroupID={taskGroupID}
|
||||||
taskGroupID={taskGroupID}
|
onDeleteTaskGroupTasks={() => {
|
||||||
onArchiveTaskGroup={tgID => {
|
deleteTaskGroupTasks({ variables: { taskGroupID } });
|
||||||
deleteTaskGroup({ variables: { taskGroupID: tgID } });
|
hidePopup();
|
||||||
|
}}
|
||||||
|
onSortTaskGroup={taskSort => {
|
||||||
|
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
||||||
|
if (taskGroup) {
|
||||||
|
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
|
||||||
|
.sort((a, b) => sortTasks(a, b, taskSort))
|
||||||
|
.reduce((prevTasks: Array<{ taskID: string; position: number }>, t, idx) => {
|
||||||
|
prevTasks.push({ taskID: t.id, position: (idx + 1) * 2048 });
|
||||||
|
return tasks;
|
||||||
|
}, []);
|
||||||
|
sortTaskGroup({ variables: { taskGroupID, tasks } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</Popup>,
|
onDuplicateTaskGroup={newName => {
|
||||||
|
const idx = data.findProject.taskGroups.findIndex(t => t.id === taskGroupID);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
|
||||||
|
const prevPos = taskGroups[idx].position;
|
||||||
|
const next = taskGroups[idx + 1];
|
||||||
|
let newPos = prevPos * 2;
|
||||||
|
if (next) {
|
||||||
|
newPos = (prevPos + next.position) / 2.0;
|
||||||
|
}
|
||||||
|
duplicateTaskGroup({ variables: { projectID, taskGroupID, name: newName, position: newPos } });
|
||||||
|
hidePopup();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onArchiveTaskGroup={tgID => {
|
||||||
|
deleteTaskGroup({ variables: { taskGroupID: tgID } });
|
||||||
|
hidePopup();
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -3,7 +3,7 @@ import Modal from 'shared/components/Modal';
|
|||||||
import TaskDetails from 'shared/components/TaskDetails';
|
import TaskDetails from 'shared/components/TaskDetails';
|
||||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
||||||
import MemberManager from 'shared/components/MemberManager';
|
import MemberManager from 'shared/components/MemberManager';
|
||||||
import { useRouteMatch, useHistory } from 'react-router';
|
import { useRouteMatch, useHistory, Redirect } from 'react-router';
|
||||||
import {
|
import {
|
||||||
useDeleteTaskChecklistMutation,
|
useDeleteTaskChecklistMutation,
|
||||||
useUpdateTaskChecklistNameMutation,
|
useUpdateTaskChecklistNameMutation,
|
||||||
@ -32,6 +32,7 @@ import Input from 'shared/components/Input';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
|
import hasNotFoundError from 'shared/utils/error';
|
||||||
|
|
||||||
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
|
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
|
||||||
const total = checklists.reduce((prev: any, next: any) => {
|
const total = checklists.reduce((prev: any, next: any) => {
|
||||||
@ -269,8 +270,8 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
|
const { loading, data, refetch, error } = useFindTaskQuery({ variables: { taskID }, pollInterval: 5000 });
|
||||||
const [setTaskComplete] = useSetTaskCompleteMutation();
|
const [setTaskComplete, { error: setTaskCompleteError }] = useSetTaskCompleteMutation();
|
||||||
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
refetch();
|
refetch();
|
||||||
@ -289,9 +290,13 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
refreshCache();
|
refreshCache();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (loading) {
|
if (hasNotFoundError(error, setTaskCompleteError)) {
|
||||||
return null;
|
return <Redirect to={projectURL} />;
|
||||||
}
|
}
|
||||||
|
if (setTaskCompleteError && setTaskCompleteError)
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -346,7 +351,11 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
onTaskNameChange={onTaskNameChange}
|
onTaskNameChange={onTaskNameChange}
|
||||||
onTaskDescriptionChange={onTaskDescriptionChange}
|
onTaskDescriptionChange={onTaskDescriptionChange}
|
||||||
onToggleTaskComplete={task => {
|
onToggleTaskComplete={task => {
|
||||||
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } }).catch(r => {
|
||||||
|
if (hasNotFoundError(r)) {
|
||||||
|
history.push(projectURL);
|
||||||
|
}
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onDeleteTask={onDeleteTask}
|
onDeleteTask={onDeleteTask}
|
||||||
onChangeItemName={(itemID, itemName) => {
|
onChangeItemName={(itemID, itemName) => {
|
||||||
|
@ -234,7 +234,7 @@ type ShowNewProject = {
|
|||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
|
const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only', pollInterval: 5000 });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Taskcafé';
|
document.title = 'Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
@ -260,11 +260,7 @@ const Projects = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
|
||||||
<>
|
|
||||||
<span>loading</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
||||||
|
@ -154,7 +154,7 @@ type TeamProjectsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
|
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
|
||||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
const { loading, data } = useGetTeamQuery({ variables: { teamID }, pollInterval: 5000 });
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <span>loading</span>;
|
return <span>loading</span>;
|
||||||
}
|
}
|
||||||
|
@ -85,18 +85,30 @@ type TeamsRouteProps = {
|
|||||||
const Teams = () => {
|
const Teams = () => {
|
||||||
const { teamID } = useParams<TeamsRouteProps>();
|
const { teamID } = useParams<TeamsRouteProps>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
|
const { loading, data } = useGetTeamQuery({
|
||||||
|
variables: { teamID },
|
||||||
|
onCompleted: resp => {
|
||||||
|
document.title = `${resp.findTeam.name} | Taskcafé`;
|
||||||
|
},
|
||||||
|
});
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'Teams | Taskcafé';
|
|
||||||
}, []);
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<GlobalTopNavbar
|
||||||
<span>loading</span>
|
menuType={[
|
||||||
</>
|
{ name: 'Projects', link: `${match.url}` },
|
||||||
|
{ name: 'Members', link: `${match.url}/members` },
|
||||||
|
]}
|
||||||
|
currentTab={currentTab}
|
||||||
|
onSetTab={tab => {
|
||||||
|
setCurrentTab(tab);
|
||||||
|
}}
|
||||||
|
onSaveProjectName={NOOP}
|
||||||
|
projectID={null}
|
||||||
|
name={null}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
|
@ -16,11 +16,12 @@ import {
|
|||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
type NameEditorProps = {
|
type NameEditorProps = {
|
||||||
|
buttonLabel?: string;
|
||||||
onSave: (listName: string) => void;
|
onSave: (listName: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
export const NameEditor: React.FC<NameEditorProps> = ({ onSave: handleSave, onCancel, buttonLabel = 'Save' }) => {
|
||||||
const $editorRef = useRef<HTMLTextAreaElement>(null);
|
const $editorRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [listName, setListName] = useState('');
|
const [listName, setListName] = useState('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -28,6 +29,11 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
|||||||
$editorRef.current.focus();
|
$editorRef.current.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const onSave = (newName: string) => {
|
||||||
|
if (newName.replace(/\s+/g, '') !== '') {
|
||||||
|
handleSave(newName);
|
||||||
|
}
|
||||||
|
};
|
||||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -60,7 +66,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{buttonLabel}
|
||||||
</AddListButton>
|
</AddListButton>
|
||||||
<CancelAdd onClick={() => onCancel()}>
|
<CancelAdd onClick={() => onCancel()}>
|
||||||
<Cross width={16} height={16} />
|
<Cross width={16} height={16} />
|
||||||
|
@ -557,6 +557,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<TabNavContent>
|
<TabNavContent>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
|
key={item.name}
|
||||||
onClick={(tab, top) => {
|
onClick={(tab, top) => {
|
||||||
if ($tabNav && $tabNav.current) {
|
if ($tabNav && $tabNav.current) {
|
||||||
const pos = $tabNav.current.getBoundingClientRect();
|
const pos = $tabNav.current.getBoundingClientRect();
|
||||||
|
@ -147,6 +147,11 @@ export const ListCardLabelText = styled.span`
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ListCardLabelsWrapper = styled.div`
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
||||||
${props =>
|
${props =>
|
||||||
props.variant === 'small'
|
props.variant === 'small'
|
||||||
@ -178,8 +183,6 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
ListCardLabels,
|
ListCardLabels,
|
||||||
ListCardLabel,
|
ListCardLabel,
|
||||||
ListCardLabelText,
|
ListCardLabelText,
|
||||||
|
ListCardLabelsWrapper,
|
||||||
ListCardOperation,
|
ListCardOperation,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardMembers,
|
CardMembers,
|
||||||
@ -158,35 +159,38 @@ const Card = React.forwardRef(
|
|||||||
</ListCardOperation>
|
</ListCardOperation>
|
||||||
)}
|
)}
|
||||||
<ListCardDetails complete={complete ?? false}>
|
<ListCardDetails complete={complete ?? false}>
|
||||||
<ListCardLabels
|
{labels && labels.length !== 0 && (
|
||||||
toggleLabels={toggleLabels}
|
<ListCardLabelsWrapper>
|
||||||
toggleDirection={toggleDirection}
|
<ListCardLabels
|
||||||
onClick={e => {
|
toggleLabels={toggleLabels}
|
||||||
e.stopPropagation();
|
toggleDirection={toggleDirection}
|
||||||
if (onCardLabelClick) {
|
onClick={e => {
|
||||||
onCardLabelClick();
|
e.stopPropagation();
|
||||||
}
|
if (onCardLabelClick) {
|
||||||
}}
|
onCardLabelClick();
|
||||||
>
|
}
|
||||||
{labels &&
|
}}
|
||||||
labels
|
>
|
||||||
.slice()
|
{labels
|
||||||
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
.slice()
|
||||||
.map(label => (
|
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
||||||
<ListCardLabel
|
.map(label => (
|
||||||
onAnimationEnd={() => {
|
<ListCardLabel
|
||||||
if (setToggleLabels) {
|
onAnimationEnd={() => {
|
||||||
setToggleLabels(false);
|
if (setToggleLabels) {
|
||||||
}
|
setToggleLabels(false);
|
||||||
}}
|
}
|
||||||
variant={labelVariant ?? 'large'}
|
}}
|
||||||
color={label.labelColor.colorHex}
|
variant={labelVariant ?? 'large'}
|
||||||
key={label.id}
|
color={label.labelColor.colorHex}
|
||||||
>
|
key={label.id}
|
||||||
<ListCardLabelText>{label.name}</ListCardLabelText>
|
>
|
||||||
</ListCardLabel>
|
<ListCardLabelText>{label.name}</ListCardLabelText>
|
||||||
))}
|
</ListCardLabel>
|
||||||
</ListCardLabels>
|
))}
|
||||||
|
</ListCardLabels>
|
||||||
|
</ListCardLabelsWrapper>
|
||||||
|
)}
|
||||||
{editable ? (
|
{editable ? (
|
||||||
<EditorContent>
|
<EditorContent>
|
||||||
{complete && <CompleteIcon width={16} height={16} />}
|
{complete && <CompleteIcon width={16} height={16} />}
|
||||||
|
@ -78,6 +78,7 @@ const Icon = styled.div`
|
|||||||
|
|
||||||
type InputProps = {
|
type InputProps = {
|
||||||
variant?: 'normal' | 'alternate';
|
variant?: 'normal' | 'alternate';
|
||||||
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
floatingLabel?: boolean;
|
floatingLabel?: boolean;
|
||||||
@ -116,6 +117,7 @@ function useCombinedRefs(...refs: any) {
|
|||||||
const Input = React.forwardRef(
|
const Input = React.forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
disabled = false,
|
||||||
width = 'auto',
|
width = 'auto',
|
||||||
variant = 'normal',
|
variant = 'normal',
|
||||||
type = 'text',
|
type = 'text',
|
||||||
@ -160,6 +162,7 @@ const Input = React.forwardRef(
|
|||||||
onChange={e => {
|
onChange={e => {
|
||||||
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
|
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
hasValue={hasValue}
|
hasValue={hasValue}
|
||||||
ref={combinedRef}
|
ref={combinedRef}
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import ListActions from '.';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: ListActions,
|
|
||||||
title: 'ListActions',
|
|
||||||
parameters: {
|
|
||||||
backgrounds: [
|
|
||||||
{ name: 'white', value: '#ffffff', default: true },
|
|
||||||
{ name: 'gray', value: '#f8f8f8' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default = () => {
|
|
||||||
return <ListActions taskGroupID="1" onArchiveTaskGroup={action('on archive task group')} />;
|
|
||||||
};
|
|
@ -1,50 +1,100 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
|
import { NameEditor } from 'shared/components/AddList';
|
||||||
|
import NOOP from 'shared/utils/noop';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { TaskSorting, TaskSortingDirection, TaskSortingType } from 'shared/utils/sorting';
|
||||||
import { InnerContent, ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparator } from './Styles';
|
import { InnerContent, ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparator } from './Styles';
|
||||||
|
|
||||||
|
const CopyWrapper = styled.div`
|
||||||
|
margin: 0 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
taskGroupID: string;
|
taskGroupID: string;
|
||||||
|
onDuplicateTaskGroup: (newTaskGroupName: string) => void;
|
||||||
|
onDeleteTaskGroupTasks: () => void;
|
||||||
onArchiveTaskGroup: (taskGroupID: string) => void;
|
onArchiveTaskGroup: (taskGroupID: string) => void;
|
||||||
|
onSortTaskGroup: (taskSorting: TaskSorting) => void;
|
||||||
};
|
};
|
||||||
const LabelManager: React.FC<Props> = ({ taskGroupID, onArchiveTaskGroup }) => {
|
|
||||||
|
const LabelManager: React.FC<Props> = ({
|
||||||
|
taskGroupID,
|
||||||
|
onDeleteTaskGroupTasks,
|
||||||
|
onDuplicateTaskGroup,
|
||||||
|
onArchiveTaskGroup,
|
||||||
|
onSortTaskGroup,
|
||||||
|
}) => {
|
||||||
|
const { setTab } = usePopup();
|
||||||
return (
|
return (
|
||||||
<InnerContent>
|
<>
|
||||||
<ListActionsWrapper>
|
<Popup tab={0} title={null}>
|
||||||
<ListActionItemWrapper>
|
<InnerContent>
|
||||||
<ListActionItem>Add card...</ListActionItem>
|
<ListActionsWrapper>
|
||||||
</ListActionItemWrapper>
|
<ListActionItemWrapper onClick={() => setTab(1)}>
|
||||||
<ListActionItemWrapper>
|
<ListActionItem>Duplicate</ListActionItem>
|
||||||
<ListActionItem>Copy List...</ListActionItem>
|
</ListActionItemWrapper>
|
||||||
</ListActionItemWrapper>
|
<ListActionItemWrapper onClick={() => setTab(2)}>
|
||||||
<ListActionItemWrapper>
|
<ListActionItem>Sort</ListActionItem>
|
||||||
<ListActionItem>Move card...</ListActionItem>
|
</ListActionItemWrapper>
|
||||||
</ListActionItemWrapper>
|
</ListActionsWrapper>
|
||||||
<ListActionItemWrapper>
|
<ListSeparator />
|
||||||
<ListActionItem>Watch</ListActionItem>
|
<ListActionsWrapper>
|
||||||
</ListActionItemWrapper>
|
<ListActionItemWrapper onClick={() => onDeleteTaskGroupTasks()}>
|
||||||
</ListActionsWrapper>
|
<ListActionItem>Delete All Tasks</ListActionItem>
|
||||||
<ListSeparator />
|
</ListActionItemWrapper>
|
||||||
<ListActionsWrapper>
|
</ListActionsWrapper>
|
||||||
<ListActionItemWrapper>
|
<ListSeparator />
|
||||||
<ListActionItem>Sort By...</ListActionItem>
|
<ListActionsWrapper>
|
||||||
</ListActionItemWrapper>
|
<ListActionItemWrapper onClick={() => onArchiveTaskGroup(taskGroupID)}>
|
||||||
</ListActionsWrapper>
|
<ListActionItem>Delete</ListActionItem>
|
||||||
<ListSeparator />
|
</ListActionItemWrapper>
|
||||||
<ListActionsWrapper>
|
</ListActionsWrapper>
|
||||||
<ListActionItemWrapper>
|
</InnerContent>
|
||||||
<ListActionItem>Move All Cards in This List...</ListActionItem>
|
</Popup>
|
||||||
</ListActionItemWrapper>
|
<Popup tab={1} title="Copy list" onClose={NOOP}>
|
||||||
<ListActionItemWrapper>
|
<CopyWrapper>
|
||||||
<ListActionItem>Archive All Cards in This List...</ListActionItem>
|
<NameEditor
|
||||||
</ListActionItemWrapper>
|
onCancel={NOOP}
|
||||||
</ListActionsWrapper>
|
onSave={listName => {
|
||||||
<ListSeparator />
|
onDuplicateTaskGroup(listName);
|
||||||
<ListActionsWrapper>
|
}}
|
||||||
<ListActionItemWrapper onClick={() => onArchiveTaskGroup(taskGroupID)}>
|
buttonLabel="Duplicate"
|
||||||
<ListActionItem>Archive This List</ListActionItem>
|
/>
|
||||||
</ListActionItemWrapper>
|
</CopyWrapper>
|
||||||
</ListActionsWrapper>
|
</Popup>
|
||||||
</InnerContent>
|
<Popup tab={2} title="Sort list" onClose={NOOP}>
|
||||||
|
<InnerContent>
|
||||||
|
<ListActionsWrapper>
|
||||||
|
<ListActionItemWrapper
|
||||||
|
onClick={() => onSortTaskGroup({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
|
||||||
|
>
|
||||||
|
<ListActionItem>Task title</ListActionItem>
|
||||||
|
</ListActionItemWrapper>
|
||||||
|
<ListActionItemWrapper
|
||||||
|
onClick={() => onSortTaskGroup({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
|
||||||
|
>
|
||||||
|
<ListActionItem>Due date</ListActionItem>
|
||||||
|
</ListActionItemWrapper>
|
||||||
|
<ListActionItemWrapper
|
||||||
|
onClick={() => onSortTaskGroup({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })}
|
||||||
|
>
|
||||||
|
<ListActionItem>Complete</ListActionItem>
|
||||||
|
</ListActionItemWrapper>
|
||||||
|
<ListActionItemWrapper
|
||||||
|
onClick={() => onSortTaskGroup({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })}
|
||||||
|
>
|
||||||
|
<ListActionItem>Labels</ListActionItem>
|
||||||
|
</ListActionItemWrapper>
|
||||||
|
<ListActionItemWrapper
|
||||||
|
onClick={() => onSortTaskGroup({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })}
|
||||||
|
>
|
||||||
|
<ListActionItem>Members</ListActionItem>
|
||||||
|
</ListActionItemWrapper>
|
||||||
|
</ListActionsWrapper>
|
||||||
|
</InnerContent>
|
||||||
|
</Popup>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default LabelManager;
|
export default LabelManager;
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
getAfterDropDraggableList,
|
getAfterDropDraggableList,
|
||||||
} from 'shared/utils/draggables';
|
} from 'shared/utils/draggables';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { TaskSorting, TaskSortingType, TaskSortingDirection, sortTasks } from 'shared/utils/sorting';
|
||||||
|
|
||||||
import { Container, BoardContainer, BoardWrapper } from './Styles';
|
import { Container, BoardContainer, BoardWrapper } from './Styles';
|
||||||
import shouldMetaFilter from './metaFilter';
|
import shouldMetaFilter from './metaFilter';
|
||||||
@ -94,127 +95,6 @@ export type TaskMetaFilters = {
|
|||||||
labels: Array<LabelMetaFilter>;
|
labels: Array<LabelMetaFilter>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum TaskSortingType {
|
|
||||||
NONE,
|
|
||||||
DUE_DATE,
|
|
||||||
MEMBERS,
|
|
||||||
LABELS,
|
|
||||||
TASK_TITLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TaskSortingDirection {
|
|
||||||
ASC,
|
|
||||||
DESC,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TaskSorting = {
|
|
||||||
type: TaskSortingType;
|
|
||||||
direction: TaskSortingDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
function sortString(a: string, b: string) {
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
|
|
||||||
if (taskSorting.type === TaskSortingType.TASK_TITLE) {
|
|
||||||
if (a.name < b.name) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.name > b.name) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (taskSorting.type === TaskSortingType.DUE_DATE) {
|
|
||||||
if (a.dueDate && !b.dueDate) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b.dueDate && !a.dueDate) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return moment(a.dueDate).diff(moment(b.dueDate));
|
|
||||||
}
|
|
||||||
if (taskSorting.type === TaskSortingType.LABELS) {
|
|
||||||
// sorts non-empty labels by name, then by empty label color name
|
|
||||||
let aLabels = [];
|
|
||||||
let bLabels = [];
|
|
||||||
let aLabelsEmpty = [];
|
|
||||||
let bLabelsEmpty = [];
|
|
||||||
if (a.labels) {
|
|
||||||
for (const aLabel of a.labels) {
|
|
||||||
if (aLabel.projectLabel.name && aLabel.projectLabel.name !== '') {
|
|
||||||
aLabels.push(aLabel.projectLabel.name);
|
|
||||||
} else {
|
|
||||||
aLabelsEmpty.push(aLabel.projectLabel.labelColor.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (b.labels) {
|
|
||||||
for (const bLabel of b.labels) {
|
|
||||||
if (bLabel.projectLabel.name && bLabel.projectLabel.name !== '') {
|
|
||||||
bLabels.push(bLabel.projectLabel.name);
|
|
||||||
} else {
|
|
||||||
bLabelsEmpty.push(bLabel.projectLabel.labelColor.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
aLabels = aLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
|
||||||
bLabels = bLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
|
||||||
aLabelsEmpty = aLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
|
||||||
bLabelsEmpty = bLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
|
||||||
if (aLabelsEmpty.length !== 0 || bLabelsEmpty.length !== 0) {
|
|
||||||
if (aLabelsEmpty.length > bLabelsEmpty.length) {
|
|
||||||
if (bLabels.length !== 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (aLabels.length < bLabels.length) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (aLabels.length > bLabels.length) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (taskSorting.type === TaskSortingType.MEMBERS) {
|
|
||||||
let aMembers = [];
|
|
||||||
let bMembers = [];
|
|
||||||
if (a.assigned) {
|
|
||||||
for (const aMember of a.assigned) {
|
|
||||||
if (aMember.fullName) {
|
|
||||||
aMembers.push(aMember.fullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (b.assigned) {
|
|
||||||
for (const bMember of b.assigned) {
|
|
||||||
if (bMember.fullName) {
|
|
||||||
bMembers.push(bMember.fullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
aMembers = aMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
|
||||||
bMembers = bMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
|
||||||
if (aMembers.length < bMembers.length) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (aMembers.length > bMembers.length) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
|
function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
|
||||||
if (filter.status === TaskStatus.ALL) {
|
if (filter.status === TaskStatus.ALL) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -27,6 +27,7 @@ export const Default = () => {
|
|||||||
<BaseStyles />
|
<BaseStyles />
|
||||||
<Settings
|
<Settings
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
onChangeUserInfo={action('change user info')}
|
||||||
onResetPassword={action('reset password')}
|
onResetPassword={action('reset password')}
|
||||||
onProfileAvatarRemove={action('remove')}
|
onProfileAvatarRemove={action('remove')}
|
||||||
onProfileAvatarChange={action('profile avatar change')}
|
onProfileAvatarChange={action('profile avatar change')}
|
||||||
|
@ -10,6 +10,11 @@ const PasswordInput = styled(Input)`
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const UserInfoInput = styled(Input)`
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
const FormError = styled.span`
|
const FormError = styled.span`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(${props => props.theme.colors.warning});
|
color: rgba(${props => props.theme.colors.warning});
|
||||||
@ -240,6 +245,7 @@ const SaveButton = styled(Button)`
|
|||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
onProfileAvatarChange: () => void;
|
onProfileAvatarChange: () => void;
|
||||||
onProfileAvatarRemove: () => void;
|
onProfileAvatarRemove: () => void;
|
||||||
|
onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
|
||||||
onResetPassword: (password: string, done: () => void) => void;
|
onResetPassword: (password: string, done: () => void) => void;
|
||||||
profile: TaskUser;
|
profile: TaskUser;
|
||||||
};
|
};
|
||||||
@ -300,9 +306,93 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UserInfoData = {
|
||||||
|
full_name: string;
|
||||||
|
bio: string;
|
||||||
|
initials: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
type UserInfoTabProps = {
|
||||||
|
profile: TaskUser;
|
||||||
|
onProfileAvatarChange: () => void;
|
||||||
|
onProfileAvatarRemove: () => void;
|
||||||
|
onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
|
||||||
|
const INITIALS_PATTERN = /^[a-zA-Z]{2,3}$/i;
|
||||||
|
|
||||||
|
const UserInfoTab: React.FC<UserInfoTabProps> = ({
|
||||||
|
profile,
|
||||||
|
onProfileAvatarRemove,
|
||||||
|
onProfileAvatarChange,
|
||||||
|
onChangeUserInfo,
|
||||||
|
}) => {
|
||||||
|
const [active, setActive] = useState(true);
|
||||||
|
const { register, handleSubmit, errors } = useForm<UserInfoData>();
|
||||||
|
const done = () => {
|
||||||
|
setActive(true);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AvatarSettings
|
||||||
|
onProfileAvatarRemove={onProfileAvatarRemove}
|
||||||
|
onProfileAvatarChange={onProfileAvatarChange}
|
||||||
|
profile={profile.profileIcon}
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(data => {
|
||||||
|
setActive(false);
|
||||||
|
onChangeUserInfo(data, done);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<UserInfoInput
|
||||||
|
ref={register({ required: 'Full name is required' })}
|
||||||
|
name="full_name"
|
||||||
|
defaultValue={profile.fullName}
|
||||||
|
width="100%"
|
||||||
|
label="Name"
|
||||||
|
/>
|
||||||
|
{errors.full_name && <FormError>{errors.full_name.message}</FormError>}
|
||||||
|
<UserInfoInput
|
||||||
|
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
||||||
|
ref={register({
|
||||||
|
required: 'Initials is required',
|
||||||
|
pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' },
|
||||||
|
})}
|
||||||
|
name="initials"
|
||||||
|
width="100%"
|
||||||
|
label="Initials "
|
||||||
|
/>
|
||||||
|
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||||
|
<UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " />
|
||||||
|
<UserInfoInput
|
||||||
|
width="100%"
|
||||||
|
name="email"
|
||||||
|
ref={register({
|
||||||
|
required: 'Email is required',
|
||||||
|
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||||
|
})}
|
||||||
|
defaultValue={profile.email ?? ''}
|
||||||
|
label="Email"
|
||||||
|
/>
|
||||||
|
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||||
|
<UserInfoInput width="100%" name="bio" ref={register()} defaultValue={profile.bio ?? ''} label="Bio" />
|
||||||
|
{errors.bio && <FormError>{errors.bio.message}</FormError>}
|
||||||
|
<SettingActions>
|
||||||
|
<SaveButton disabled={!active} type="submit">
|
||||||
|
Save Change
|
||||||
|
</SaveButton>
|
||||||
|
</SettingActions>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Settings: React.FC<SettingsProps> = ({
|
const Settings: React.FC<SettingsProps> = ({
|
||||||
onProfileAvatarRemove,
|
onProfileAvatarRemove,
|
||||||
onProfileAvatarChange,
|
onProfileAvatarChange,
|
||||||
|
onChangeUserInfo,
|
||||||
onResetPassword,
|
onResetPassword,
|
||||||
profile,
|
profile,
|
||||||
}) => {
|
}) => {
|
||||||
@ -315,6 +405,7 @@ const Settings: React.FC<SettingsProps> = ({
|
|||||||
<TabNavContent>
|
<TabNavContent>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
|
key={item.name}
|
||||||
onClick={(tab, top) => {
|
onClick={(tab, top) => {
|
||||||
if ($tabNav && $tabNav.current) {
|
if ($tabNav && $tabNav.current) {
|
||||||
const pos = $tabNav.current.getBoundingClientRect();
|
const pos = $tabNav.current.getBoundingClientRect();
|
||||||
@ -332,23 +423,12 @@ const Settings: React.FC<SettingsProps> = ({
|
|||||||
</TabNav>
|
</TabNav>
|
||||||
<TabContentWrapper>
|
<TabContentWrapper>
|
||||||
<Tab tab={0} currentTab={currentTab}>
|
<Tab tab={0} currentTab={currentTab}>
|
||||||
<AvatarSettings
|
<UserInfoTab
|
||||||
onProfileAvatarRemove={onProfileAvatarRemove}
|
|
||||||
onProfileAvatarChange={onProfileAvatarChange}
|
onProfileAvatarChange={onProfileAvatarChange}
|
||||||
profile={profile.profileIcon}
|
onProfileAvatarRemove={onProfileAvatarRemove}
|
||||||
|
profile={profile}
|
||||||
|
onChangeUserInfo={onChangeUserInfo}
|
||||||
/>
|
/>
|
||||||
<Input defaultValue={profile.fullName} width="100%" label="Name" />
|
|
||||||
<Input
|
|
||||||
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
|
||||||
width="100%"
|
|
||||||
label="Initials "
|
|
||||||
/>
|
|
||||||
<Input defaultValue={profile.username ?? ''} width="100%" label="Username " />
|
|
||||||
<Input width="100%" label="Email" />
|
|
||||||
<Input width="100%" label="Bio" />
|
|
||||||
<SettingActions>
|
|
||||||
<SaveButton>Save Change</SaveButton>
|
|
||||||
</SettingActions>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab tab={1} currentTab={currentTab}>
|
<Tab tab={1} currentTab={currentTab}>
|
||||||
<ResetPasswordTab onResetPassword={onResetPassword} />
|
<ResetPasswordTab onResetPassword={onResetPassword} />
|
||||||
|
@ -169,7 +169,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<LeftSidebarSection>
|
<LeftSidebarSection>
|
||||||
<SidebarTitle>TASK GROUP</SidebarTitle>
|
<SidebarTitle>TASK GROUP</SidebarTitle>
|
||||||
<SidebarButton>
|
<SidebarButton>
|
||||||
<SidebarButtonText>Release 0.1.0</SidebarButtonText>
|
<SidebarButtonText>{task.taskGroup.name}</SidebarButtonText>
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
<DueDateTitle>DUE DATE</DueDateTitle>
|
<DueDateTitle>DUE DATE</DueDateTitle>
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
|
@ -251,8 +251,8 @@ export const NavSeparator = styled.div`
|
|||||||
export const LogoContainer = styled(Link)`
|
export const LogoContainer = styled(Link)`
|
||||||
display: block;
|
display: block;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -105,6 +105,7 @@ export type UserAccount = {
|
|||||||
createdAt: Scalars['Time'];
|
createdAt: Scalars['Time'];
|
||||||
fullName: Scalars['String'];
|
fullName: Scalars['String'];
|
||||||
initials: Scalars['String'];
|
initials: Scalars['String'];
|
||||||
|
bio: Scalars['String'];
|
||||||
role: Role;
|
role: Role;
|
||||||
username: Scalars['String'];
|
username: Scalars['String'];
|
||||||
profileIcon: ProfileIcon;
|
profileIcon: ProfileIcon;
|
||||||
@ -208,7 +209,8 @@ export enum ObjectType {
|
|||||||
Org = 'ORG',
|
Org = 'ORG',
|
||||||
Team = 'TEAM',
|
Team = 'TEAM',
|
||||||
Project = 'PROJECT',
|
Project = 'PROJECT',
|
||||||
Task = 'TASK'
|
Task = 'TASK',
|
||||||
|
TaskGroup = 'TASK_GROUP'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
@ -275,13 +277,16 @@ export type Mutation = {
|
|||||||
deleteTaskChecklist: DeleteTaskChecklistPayload;
|
deleteTaskChecklist: DeleteTaskChecklistPayload;
|
||||||
deleteTaskChecklistItem: DeleteTaskChecklistItemPayload;
|
deleteTaskChecklistItem: DeleteTaskChecklistItemPayload;
|
||||||
deleteTaskGroup: DeleteTaskGroupPayload;
|
deleteTaskGroup: DeleteTaskGroupPayload;
|
||||||
|
deleteTaskGroupTasks: DeleteTaskGroupTasksPayload;
|
||||||
deleteTeam: DeleteTeamPayload;
|
deleteTeam: DeleteTeamPayload;
|
||||||
deleteTeamMember: DeleteTeamMemberPayload;
|
deleteTeamMember: DeleteTeamMemberPayload;
|
||||||
deleteUserAccount: DeleteUserAccountPayload;
|
deleteUserAccount: DeleteUserAccountPayload;
|
||||||
|
duplicateTaskGroup: DuplicateTaskGroupPayload;
|
||||||
logoutUser: Scalars['Boolean'];
|
logoutUser: Scalars['Boolean'];
|
||||||
removeTaskLabel: Task;
|
removeTaskLabel: Task;
|
||||||
setTaskChecklistItemComplete: TaskChecklistItem;
|
setTaskChecklistItemComplete: TaskChecklistItem;
|
||||||
setTaskComplete: Task;
|
setTaskComplete: Task;
|
||||||
|
sortTaskGroup: SortTaskGroupPayload;
|
||||||
toggleTaskLabel: ToggleTaskLabelPayload;
|
toggleTaskLabel: ToggleTaskLabelPayload;
|
||||||
unassignTask: Task;
|
unassignTask: Task;
|
||||||
updateProjectLabel: ProjectLabel;
|
updateProjectLabel: ProjectLabel;
|
||||||
@ -300,6 +305,7 @@ export type Mutation = {
|
|||||||
updateTaskLocation: UpdateTaskLocationPayload;
|
updateTaskLocation: UpdateTaskLocationPayload;
|
||||||
updateTaskName: Task;
|
updateTaskName: Task;
|
||||||
updateTeamMemberRole: UpdateTeamMemberRolePayload;
|
updateTeamMemberRole: UpdateTeamMemberRolePayload;
|
||||||
|
updateUserInfo: UpdateUserInfoPayload;
|
||||||
updateUserPassword: UpdateUserPasswordPayload;
|
updateUserPassword: UpdateUserPasswordPayload;
|
||||||
updateUserRole: UpdateUserRolePayload;
|
updateUserRole: UpdateUserRolePayload;
|
||||||
};
|
};
|
||||||
@ -405,6 +411,11 @@ export type MutationDeleteTaskGroupArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteTaskGroupTasksArgs = {
|
||||||
|
input: DeleteTaskGroupTasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteTeamArgs = {
|
export type MutationDeleteTeamArgs = {
|
||||||
input: DeleteTeam;
|
input: DeleteTeam;
|
||||||
};
|
};
|
||||||
@ -420,6 +431,11 @@ export type MutationDeleteUserAccountArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDuplicateTaskGroupArgs = {
|
||||||
|
input: DuplicateTaskGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationLogoutUserArgs = {
|
export type MutationLogoutUserArgs = {
|
||||||
input: LogoutUser;
|
input: LogoutUser;
|
||||||
};
|
};
|
||||||
@ -440,6 +456,11 @@ export type MutationSetTaskCompleteArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSortTaskGroupArgs = {
|
||||||
|
input: SortTaskGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationToggleTaskLabelArgs = {
|
export type MutationToggleTaskLabelArgs = {
|
||||||
input: ToggleTaskLabelInput;
|
input: ToggleTaskLabelInput;
|
||||||
};
|
};
|
||||||
@ -530,6 +551,11 @@ export type MutationUpdateTeamMemberRoleArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateUserInfoArgs = {
|
||||||
|
input: UpdateUserInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateUserPasswordArgs = {
|
export type MutationUpdateUserPasswordArgs = {
|
||||||
input: UpdateUserPassword;
|
input: UpdateUserPassword;
|
||||||
};
|
};
|
||||||
@ -697,7 +723,7 @@ export type UpdateProjectMemberRolePayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type NewTask = {
|
export type NewTask = {
|
||||||
taskGroupID: Scalars['String'];
|
taskGroupID: Scalars['UUID'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
position: Scalars['Float'];
|
position: Scalars['Float'];
|
||||||
};
|
};
|
||||||
@ -823,6 +849,44 @@ export type DeleteTaskChecklistPayload = {
|
|||||||
taskChecklist: TaskChecklist;
|
taskChecklist: TaskChecklist;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeleteTaskGroupTasks = {
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteTaskGroupTasksPayload = {
|
||||||
|
__typename?: 'DeleteTaskGroupTasksPayload';
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
tasks: Array<Scalars['UUID']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaskPositionUpdate = {
|
||||||
|
taskID: Scalars['UUID'];
|
||||||
|
position: Scalars['Float'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortTaskGroupPayload = {
|
||||||
|
__typename?: 'SortTaskGroupPayload';
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
tasks: Array<Task>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortTaskGroup = {
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
tasks: Array<TaskPositionUpdate>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DuplicateTaskGroup = {
|
||||||
|
projectID: Scalars['UUID'];
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
position: Scalars['Float'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DuplicateTaskGroupPayload = {
|
||||||
|
__typename?: 'DuplicateTaskGroupPayload';
|
||||||
|
taskGroup: TaskGroup;
|
||||||
|
};
|
||||||
|
|
||||||
export type NewTaskGroupLocation = {
|
export type NewTaskGroupLocation = {
|
||||||
taskGroupID: Scalars['UUID'];
|
taskGroupID: Scalars['UUID'];
|
||||||
position: Scalars['Float'];
|
position: Scalars['Float'];
|
||||||
@ -923,6 +987,18 @@ export type UpdateTeamMemberRolePayload = {
|
|||||||
member: Member;
|
member: Member;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateUserInfoPayload = {
|
||||||
|
__typename?: 'UpdateUserInfoPayload';
|
||||||
|
user: UserAccount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateUserInfo = {
|
||||||
|
name: Scalars['String'];
|
||||||
|
initials: Scalars['String'];
|
||||||
|
email: Scalars['String'];
|
||||||
|
bio: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateUserPassword = {
|
export type UpdateUserPassword = {
|
||||||
userID: Scalars['UUID'];
|
userID: Scalars['UUID'];
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
@ -1189,7 +1265,7 @@ export type FindTaskQuery = (
|
|||||||
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
|
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
|
||||||
& { taskGroup: (
|
& { taskGroup: (
|
||||||
{ __typename?: 'TaskGroup' }
|
{ __typename?: 'TaskGroup' }
|
||||||
& Pick<TaskGroup, 'id'>
|
& Pick<TaskGroup, 'id' | 'name'>
|
||||||
), badges: (
|
), badges: (
|
||||||
{ __typename?: 'TaskBadges' }
|
{ __typename?: 'TaskBadges' }
|
||||||
& { checklist?: Maybe<(
|
& { checklist?: Maybe<(
|
||||||
@ -1298,7 +1374,7 @@ export type MeQuery = (
|
|||||||
{ __typename?: 'MePayload' }
|
{ __typename?: 'MePayload' }
|
||||||
& { user: (
|
& { user: (
|
||||||
{ __typename?: 'UserAccount' }
|
{ __typename?: 'UserAccount' }
|
||||||
& Pick<UserAccount, 'id' | 'fullName'>
|
& Pick<UserAccount, 'id' | 'fullName' | 'username' | 'email' | 'bio'>
|
||||||
& { profileIcon: (
|
& { profileIcon: (
|
||||||
{ __typename?: 'ProfileIcon' }
|
{ __typename?: 'ProfileIcon' }
|
||||||
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
|
||||||
@ -1397,7 +1473,7 @@ export type UpdateProjectMemberRoleMutation = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type CreateTaskMutationVariables = {
|
export type CreateTaskMutationVariables = {
|
||||||
taskGroupID: Scalars['String'];
|
taskGroupID: Scalars['UUID'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
position: Scalars['Float'];
|
position: Scalars['Float'];
|
||||||
};
|
};
|
||||||
@ -1575,6 +1651,60 @@ export type UpdateTaskChecklistNameMutation = (
|
|||||||
) }
|
) }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type DeleteTaskGroupTasksMutationVariables = {
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type DeleteTaskGroupTasksMutation = (
|
||||||
|
{ __typename?: 'Mutation' }
|
||||||
|
& { deleteTaskGroupTasks: (
|
||||||
|
{ __typename?: 'DeleteTaskGroupTasksPayload' }
|
||||||
|
& Pick<DeleteTaskGroupTasksPayload, 'tasks' | 'taskGroupID'>
|
||||||
|
) }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type DuplicateTaskGroupMutationVariables = {
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
position: Scalars['Float'];
|
||||||
|
projectID: Scalars['UUID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type DuplicateTaskGroupMutation = (
|
||||||
|
{ __typename?: 'Mutation' }
|
||||||
|
& { duplicateTaskGroup: (
|
||||||
|
{ __typename?: 'DuplicateTaskGroupPayload' }
|
||||||
|
& { taskGroup: (
|
||||||
|
{ __typename?: 'TaskGroup' }
|
||||||
|
& Pick<TaskGroup, 'id' | 'name' | 'position'>
|
||||||
|
& { tasks: Array<(
|
||||||
|
{ __typename?: 'Task' }
|
||||||
|
& TaskFieldsFragment
|
||||||
|
)> }
|
||||||
|
) }
|
||||||
|
) }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SortTaskGroupMutationVariables = {
|
||||||
|
tasks: Array<TaskPositionUpdate>;
|
||||||
|
taskGroupID: Scalars['UUID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type SortTaskGroupMutation = (
|
||||||
|
{ __typename?: 'Mutation' }
|
||||||
|
& { sortTaskGroup: (
|
||||||
|
{ __typename?: 'SortTaskGroupPayload' }
|
||||||
|
& Pick<SortTaskGroupPayload, 'taskGroupID'>
|
||||||
|
& { tasks: Array<(
|
||||||
|
{ __typename?: 'Task' }
|
||||||
|
& Pick<Task, 'id' | 'position'>
|
||||||
|
)> }
|
||||||
|
) }
|
||||||
|
);
|
||||||
|
|
||||||
export type UpdateTaskGroupNameMutationVariables = {
|
export type UpdateTaskGroupNameMutationVariables = {
|
||||||
taskGroupID: Scalars['UUID'];
|
taskGroupID: Scalars['UUID'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
@ -1970,7 +2100,7 @@ export type CreateUserAccountMutation = (
|
|||||||
{ __typename?: 'Mutation' }
|
{ __typename?: 'Mutation' }
|
||||||
& { createUserAccount: (
|
& { createUserAccount: (
|
||||||
{ __typename?: 'UserAccount' }
|
{ __typename?: 'UserAccount' }
|
||||||
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username'>
|
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username' | 'bio'>
|
||||||
& { profileIcon: (
|
& { profileIcon: (
|
||||||
{ __typename?: 'ProfileIcon' }
|
{ __typename?: 'ProfileIcon' }
|
||||||
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
|
||||||
@ -2017,6 +2147,29 @@ export type DeleteUserAccountMutation = (
|
|||||||
) }
|
) }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type UpdateUserInfoMutationVariables = {
|
||||||
|
name: Scalars['String'];
|
||||||
|
initials: Scalars['String'];
|
||||||
|
email: Scalars['String'];
|
||||||
|
bio: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type UpdateUserInfoMutation = (
|
||||||
|
{ __typename?: 'Mutation' }
|
||||||
|
& { updateUserInfo: (
|
||||||
|
{ __typename?: 'UpdateUserInfoPayload' }
|
||||||
|
& { user: (
|
||||||
|
{ __typename?: 'UserAccount' }
|
||||||
|
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'bio'>
|
||||||
|
& { profileIcon: (
|
||||||
|
{ __typename?: 'ProfileIcon' }
|
||||||
|
& Pick<ProfileIcon, 'initials'>
|
||||||
|
) }
|
||||||
|
) }
|
||||||
|
) }
|
||||||
|
);
|
||||||
|
|
||||||
export type UpdateUserPasswordMutationVariables = {
|
export type UpdateUserPasswordMutationVariables = {
|
||||||
userID: Scalars['UUID'];
|
userID: Scalars['UUID'];
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
@ -2550,6 +2703,7 @@ export const FindTaskDocument = gql`
|
|||||||
complete
|
complete
|
||||||
taskGroup {
|
taskGroup {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
checklist {
|
checklist {
|
||||||
@ -2685,6 +2839,9 @@ export const MeDocument = gql`
|
|||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
|
username
|
||||||
|
email
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
initials
|
initials
|
||||||
bgColor
|
bgColor
|
||||||
@ -2888,7 +3045,7 @@ export type UpdateProjectMemberRoleMutationHookResult = ReturnType<typeof useUpd
|
|||||||
export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>;
|
export type UpdateProjectMemberRoleMutationResult = ApolloReactCommon.MutationResult<UpdateProjectMemberRoleMutation>;
|
||||||
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>;
|
export type UpdateProjectMemberRoleMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateProjectMemberRoleMutation, UpdateProjectMemberRoleMutationVariables>;
|
||||||
export const CreateTaskDocument = gql`
|
export const CreateTaskDocument = gql`
|
||||||
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
|
mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
|
||||||
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
|
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
|
||||||
...TaskFields
|
...TaskFields
|
||||||
}
|
}
|
||||||
@ -3292,6 +3449,118 @@ export function useUpdateTaskChecklistNameMutation(baseOptions?: ApolloReactHook
|
|||||||
export type UpdateTaskChecklistNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistNameMutation>;
|
export type UpdateTaskChecklistNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistNameMutation>;
|
||||||
export type UpdateTaskChecklistNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistNameMutation>;
|
export type UpdateTaskChecklistNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistNameMutation>;
|
||||||
export type UpdateTaskChecklistNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistNameMutation, UpdateTaskChecklistNameMutationVariables>;
|
export type UpdateTaskChecklistNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistNameMutation, UpdateTaskChecklistNameMutationVariables>;
|
||||||
|
export const DeleteTaskGroupTasksDocument = gql`
|
||||||
|
mutation deleteTaskGroupTasks($taskGroupID: UUID!) {
|
||||||
|
deleteTaskGroupTasks(input: {taskGroupID: $taskGroupID}) {
|
||||||
|
tasks
|
||||||
|
taskGroupID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type DeleteTaskGroupTasksMutationFn = ApolloReactCommon.MutationFunction<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDeleteTaskGroupTasksMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useDeleteTaskGroupTasksMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDeleteTaskGroupTasksMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [deleteTaskGroupTasksMutation, { data, loading, error }] = useDeleteTaskGroupTasksMutation({
|
||||||
|
* variables: {
|
||||||
|
* taskGroupID: // value for 'taskGroupID'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDeleteTaskGroupTasksMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>) {
|
||||||
|
return ApolloReactHooks.useMutation<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>(DeleteTaskGroupTasksDocument, baseOptions);
|
||||||
|
}
|
||||||
|
export type DeleteTaskGroupTasksMutationHookResult = ReturnType<typeof useDeleteTaskGroupTasksMutation>;
|
||||||
|
export type DeleteTaskGroupTasksMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupTasksMutation>;
|
||||||
|
export type DeleteTaskGroupTasksMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupTasksMutation, DeleteTaskGroupTasksMutationVariables>;
|
||||||
|
export const DuplicateTaskGroupDocument = gql`
|
||||||
|
mutation duplicateTaskGroup($taskGroupID: UUID!, $name: String!, $position: Float!, $projectID: UUID!) {
|
||||||
|
duplicateTaskGroup(input: {projectID: $projectID, taskGroupID: $taskGroupID, name: $name, position: $position}) {
|
||||||
|
taskGroup {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
position
|
||||||
|
tasks {
|
||||||
|
...TaskFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${TaskFieldsFragmentDoc}`;
|
||||||
|
export type DuplicateTaskGroupMutationFn = ApolloReactCommon.MutationFunction<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDuplicateTaskGroupMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useDuplicateTaskGroupMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDuplicateTaskGroupMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [duplicateTaskGroupMutation, { data, loading, error }] = useDuplicateTaskGroupMutation({
|
||||||
|
* variables: {
|
||||||
|
* taskGroupID: // value for 'taskGroupID'
|
||||||
|
* name: // value for 'name'
|
||||||
|
* position: // value for 'position'
|
||||||
|
* projectID: // value for 'projectID'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDuplicateTaskGroupMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>) {
|
||||||
|
return ApolloReactHooks.useMutation<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>(DuplicateTaskGroupDocument, baseOptions);
|
||||||
|
}
|
||||||
|
export type DuplicateTaskGroupMutationHookResult = ReturnType<typeof useDuplicateTaskGroupMutation>;
|
||||||
|
export type DuplicateTaskGroupMutationResult = ApolloReactCommon.MutationResult<DuplicateTaskGroupMutation>;
|
||||||
|
export type DuplicateTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DuplicateTaskGroupMutation, DuplicateTaskGroupMutationVariables>;
|
||||||
|
export const SortTaskGroupDocument = gql`
|
||||||
|
mutation sortTaskGroup($tasks: [TaskPositionUpdate!]!, $taskGroupID: UUID!) {
|
||||||
|
sortTaskGroup(input: {taskGroupID: $taskGroupID, tasks: $tasks}) {
|
||||||
|
taskGroupID
|
||||||
|
tasks {
|
||||||
|
id
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type SortTaskGroupMutationFn = ApolloReactCommon.MutationFunction<SortTaskGroupMutation, SortTaskGroupMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSortTaskGroupMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useSortTaskGroupMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSortTaskGroupMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [sortTaskGroupMutation, { data, loading, error }] = useSortTaskGroupMutation({
|
||||||
|
* variables: {
|
||||||
|
* tasks: // value for 'tasks'
|
||||||
|
* taskGroupID: // value for 'taskGroupID'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSortTaskGroupMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SortTaskGroupMutation, SortTaskGroupMutationVariables>) {
|
||||||
|
return ApolloReactHooks.useMutation<SortTaskGroupMutation, SortTaskGroupMutationVariables>(SortTaskGroupDocument, baseOptions);
|
||||||
|
}
|
||||||
|
export type SortTaskGroupMutationHookResult = ReturnType<typeof useSortTaskGroupMutation>;
|
||||||
|
export type SortTaskGroupMutationResult = ApolloReactCommon.MutationResult<SortTaskGroupMutation>;
|
||||||
|
export type SortTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<SortTaskGroupMutation, SortTaskGroupMutationVariables>;
|
||||||
export const UpdateTaskGroupNameDocument = gql`
|
export const UpdateTaskGroupNameDocument = gql`
|
||||||
mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) {
|
mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) {
|
||||||
updateTaskGroupName(input: {taskGroupID: $taskGroupID, name: $name}) {
|
updateTaskGroupName(input: {taskGroupID: $taskGroupID, name: $name}) {
|
||||||
@ -4049,6 +4318,7 @@ export const CreateUserAccountDocument = gql`
|
|||||||
fullName
|
fullName
|
||||||
initials
|
initials
|
||||||
username
|
username
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
url
|
url
|
||||||
initials
|
initials
|
||||||
@ -4147,6 +4417,49 @@ export function useDeleteUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
|
|||||||
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
|
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
|
||||||
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
|
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
|
||||||
export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
|
export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
|
||||||
|
export const UpdateUserInfoDocument = gql`
|
||||||
|
mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) {
|
||||||
|
updateUserInfo(input: {name: $name, initials: $initials, email: $email, bio: $bio}) {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
bio
|
||||||
|
profileIcon {
|
||||||
|
initials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type UpdateUserInfoMutationFn = ApolloReactCommon.MutationFunction<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpdateUserInfoMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpdateUserInfoMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpdateUserInfoMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [updateUserInfoMutation, { data, loading, error }] = useUpdateUserInfoMutation({
|
||||||
|
* variables: {
|
||||||
|
* name: // value for 'name'
|
||||||
|
* initials: // value for 'initials'
|
||||||
|
* email: // value for 'email'
|
||||||
|
* bio: // value for 'bio'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpdateUserInfoMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>) {
|
||||||
|
return ApolloReactHooks.useMutation<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>(UpdateUserInfoDocument, baseOptions);
|
||||||
|
}
|
||||||
|
export type UpdateUserInfoMutationHookResult = ReturnType<typeof useUpdateUserInfoMutation>;
|
||||||
|
export type UpdateUserInfoMutationResult = ApolloReactCommon.MutationResult<UpdateUserInfoMutation>;
|
||||||
|
export type UpdateUserInfoMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>;
|
||||||
export const UpdateUserPasswordDocument = gql`
|
export const UpdateUserPasswordDocument = gql`
|
||||||
mutation updateUserPassword($userID: UUID!, $password: String!) {
|
mutation updateUserPassword($userID: UUID!, $password: String!) {
|
||||||
updateUserPassword(input: {userID: $userID, password: $password}) {
|
updateUserPassword(input: {userID: $userID, password: $password}) {
|
||||||
|
@ -8,6 +8,7 @@ query findTask($taskID: UUID!) {
|
|||||||
complete
|
complete
|
||||||
taskGroup {
|
taskGroup {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
checklist {
|
checklist {
|
||||||
|
@ -3,6 +3,9 @@ query me {
|
|||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
|
username
|
||||||
|
email
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
initials
|
initials
|
||||||
bgColor
|
bgColor
|
||||||
|
@ -2,7 +2,7 @@ import gql from 'graphql-tag';
|
|||||||
import TASK_FRAGMENT from '../fragments/task';
|
import TASK_FRAGMENT from '../fragments/task';
|
||||||
|
|
||||||
const CREATE_TASK_MUTATION = gql`
|
const CREATE_TASK_MUTATION = gql`
|
||||||
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
|
mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
|
||||||
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
|
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
|
||||||
...TaskFields
|
...TaskFields
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
const DELETE_TASK_GROUP_TASKS_MUTATION = gql`
|
||||||
|
mutation deleteTaskGroupTasks($taskGroupID: UUID!) {
|
||||||
|
deleteTaskGroupTasks(input: { taskGroupID: $taskGroupID }) {
|
||||||
|
tasks
|
||||||
|
taskGroupID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
26
frontend/src/shared/graphql/taskGroup/duplicateTaskGroup.ts
Normal file
26
frontend/src/shared/graphql/taskGroup/duplicateTaskGroup.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
import TASK_FRAGMENT from '../fragments/task';
|
||||||
|
|
||||||
|
const DUPLICATE_TASK_GROUP_MUTATION = gql`
|
||||||
|
mutation duplicateTaskGroup($taskGroupID: UUID!, $name: String!, $position: Float!, $projectID: UUID!) {
|
||||||
|
duplicateTaskGroup(
|
||||||
|
input: {
|
||||||
|
projectID: $projectID
|
||||||
|
taskGroupID: $taskGroupID
|
||||||
|
name: $name
|
||||||
|
position: $position
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
taskGroup {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
position
|
||||||
|
tasks {
|
||||||
|
...TaskFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${TASK_FRAGMENT}
|
||||||
|
}
|
||||||
|
`;
|
13
frontend/src/shared/graphql/taskGroup/sortTaskGroup.ts
Normal file
13
frontend/src/shared/graphql/taskGroup/sortTaskGroup.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
const SORT_TASK_GROUP_MUTATION = gql`
|
||||||
|
mutation sortTaskGroup($tasks: [TaskPositionUpdate!]!, $taskGroupID: UUID!) {
|
||||||
|
sortTaskGroup(input: { taskGroupID: $taskGroupID, tasks: $tasks }) {
|
||||||
|
taskGroupID
|
||||||
|
tasks {
|
||||||
|
id
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -24,6 +24,7 @@ export const CREATE_USER_MUTATION = gql`
|
|||||||
fullName
|
fullName
|
||||||
initials
|
initials
|
||||||
username
|
username
|
||||||
|
bio
|
||||||
profileIcon {
|
profileIcon {
|
||||||
url
|
url
|
||||||
initials
|
initials
|
||||||
|
19
frontend/src/shared/graphql/user/updateUserInfo.ts
Normal file
19
frontend/src/shared/graphql/user/updateUserInfo.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
export const UPDATE_USER_INFO_MUTATION = gql`
|
||||||
|
mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) {
|
||||||
|
updateUserInfo(input: { name: $name, initials: $initials, email: $email, bio: $bio }) {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
bio
|
||||||
|
profileIcon {
|
||||||
|
initials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default UPDATE_USER_INFO_MUTATION;
|
13
frontend/src/shared/utils/error.ts
Normal file
13
frontend/src/shared/utils/error.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ApolloError } from '@apollo/client';
|
||||||
|
|
||||||
|
export default function hasNotFoundError(...errors: Array<ApolloError | undefined>) {
|
||||||
|
for (const error of errors) {
|
||||||
|
if (error && error.graphQLErrors.length !== 0) {
|
||||||
|
const notFound = error.graphQLErrors.find(e => e.extensions && e.extensions.code === '404');
|
||||||
|
if (notFound) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
132
frontend/src/shared/utils/sorting.ts
Normal file
132
frontend/src/shared/utils/sorting.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export enum TaskSortingType {
|
||||||
|
NONE,
|
||||||
|
COMPLETE,
|
||||||
|
DUE_DATE,
|
||||||
|
MEMBERS,
|
||||||
|
LABELS,
|
||||||
|
TASK_TITLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaskSortingDirection {
|
||||||
|
ASC,
|
||||||
|
DESC,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskSorting = {
|
||||||
|
type: TaskSortingType;
|
||||||
|
direction: TaskSortingDirection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sortString(a: string, b: string) {
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a > b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
|
||||||
|
if (taskSorting.type === TaskSortingType.TASK_TITLE) {
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (taskSorting.type === TaskSortingType.DUE_DATE) {
|
||||||
|
if (a.dueDate && !b.dueDate) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.dueDate && !a.dueDate) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return moment(a.dueDate).diff(moment(b.dueDate));
|
||||||
|
}
|
||||||
|
if (taskSorting.type === TaskSortingType.COMPLETE) {
|
||||||
|
if (a.complete && !b.complete) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.complete && !a.complete) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (taskSorting.type === TaskSortingType.LABELS) {
|
||||||
|
// sorts non-empty labels by name, then by empty label color name
|
||||||
|
let aLabels = [];
|
||||||
|
let bLabels = [];
|
||||||
|
let aLabelsEmpty = [];
|
||||||
|
let bLabelsEmpty = [];
|
||||||
|
if (a.labels) {
|
||||||
|
for (const aLabel of a.labels) {
|
||||||
|
if (aLabel.projectLabel.name && aLabel.projectLabel.name !== '') {
|
||||||
|
aLabels.push(aLabel.projectLabel.name);
|
||||||
|
} else {
|
||||||
|
aLabelsEmpty.push(aLabel.projectLabel.labelColor.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (b.labels) {
|
||||||
|
for (const bLabel of b.labels) {
|
||||||
|
if (bLabel.projectLabel.name && bLabel.projectLabel.name !== '') {
|
||||||
|
bLabels.push(bLabel.projectLabel.name);
|
||||||
|
} else {
|
||||||
|
bLabelsEmpty.push(bLabel.projectLabel.labelColor.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aLabels = aLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||||
|
bLabels = bLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||||
|
aLabelsEmpty = aLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||||
|
bLabelsEmpty = bLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||||
|
if (aLabelsEmpty.length !== 0 || bLabelsEmpty.length !== 0) {
|
||||||
|
if (aLabelsEmpty.length > bLabelsEmpty.length) {
|
||||||
|
if (bLabels.length !== 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aLabels.length < bLabels.length) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (aLabels.length > bLabels.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (taskSorting.type === TaskSortingType.MEMBERS) {
|
||||||
|
let aMembers = [];
|
||||||
|
let bMembers = [];
|
||||||
|
if (a.assigned) {
|
||||||
|
for (const aMember of a.assigned) {
|
||||||
|
if (aMember.fullName) {
|
||||||
|
aMembers.push(aMember.fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (b.assigned) {
|
||||||
|
for (const bMember of b.assigned) {
|
||||||
|
if (bMember.fullName) {
|
||||||
|
bMembers.push(bMember.fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aMembers = aMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
||||||
|
bMembers = bMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
||||||
|
if (aMembers.length < bMembers.length) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (aMembers.length > bMembers.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
2
frontend/src/taskcafe.d.ts
vendored
2
frontend/src/taskcafe.d.ts
vendored
@ -46,6 +46,8 @@ type OwnedList = {
|
|||||||
type TaskUser = {
|
type TaskUser = {
|
||||||
id: string;
|
id: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
email?: string;
|
||||||
|
bio?: string;
|
||||||
profileIcon: ProfileIcon;
|
profileIcon: ProfileIcon;
|
||||||
username?: string;
|
username?: string;
|
||||||
role?: Role;
|
role?: Role;
|
||||||
|
@ -157,4 +157,5 @@ type UserAccount struct {
|
|||||||
Initials string `json:"initials"`
|
Initials string `json:"initials"`
|
||||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||||
RoleCode string `json:"role_code"`
|
RoleCode string `json:"role_code"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ type Querier interface {
|
|||||||
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
|
||||||
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
|
CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error)
|
||||||
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
|
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
|
||||||
|
CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error)
|
||||||
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
|
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
|
||||||
CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, error)
|
CreateTaskChecklist(ctx context.Context, arg CreateTaskChecklistParams) (TaskChecklist, error)
|
||||||
CreateTaskChecklistItem(ctx context.Context, arg CreateTaskChecklistItemParams) (TaskChecklistItem, error)
|
CreateTaskChecklistItem(ctx context.Context, arg CreateTaskChecklistItemParams) (TaskChecklistItem, error)
|
||||||
@ -111,7 +112,9 @@ type Querier interface {
|
|||||||
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
|
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
|
||||||
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
|
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
|
||||||
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
|
||||||
|
UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error)
|
||||||
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
|
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
|
||||||
|
UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccountInfoParams) (UserAccount, error)
|
||||||
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
|
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
|
||||||
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
|
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
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 *;
|
||||||
|
|
||||||
|
-- name: CreateTaskAll :one
|
||||||
|
INSERT INTO task (task_group_id, created_at, name, position, description, complete, due_date)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateTaskDescription :one
|
-- name: UpdateTaskDescription :one
|
||||||
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING *;
|
UPDATE task SET description = $2 WHERE task_id = $1 RETURNING *;
|
||||||
|
|
||||||
@ -17,6 +21,9 @@ SELECT * FROM task;
|
|||||||
-- name: UpdateTaskLocation :one
|
-- name: UpdateTaskLocation :one
|
||||||
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING *;
|
UPDATE task SET task_group_id = $2, position = $3 WHERE task_id = $1 RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateTaskPosition :one
|
||||||
|
UPDATE task SET position = $2 WHERE task_id = $1 RETURNING *;
|
||||||
|
|
||||||
-- name: DeleteTaskByID :exec
|
-- name: DeleteTaskByID :exec
|
||||||
DELETE FROM task WHERE task_id = $1;
|
DELETE FROM task WHERE task_id = $1;
|
||||||
|
|
||||||
|
@ -15,6 +15,10 @@ INSERT INTO user_account(full_name, initials, email, username, created_at, passw
|
|||||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateUserAccountInfo :one
|
||||||
|
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
|
||||||
|
WHERE user_id = $1 RETURNING *;
|
||||||
|
|
||||||
-- name: DeleteUserAccountByID :exec
|
-- name: DeleteUserAccountByID :exec
|
||||||
DELETE FROM user_account WHERE user_id = $1;
|
DELETE FROM user_account WHERE user_id = $1;
|
||||||
|
|
||||||
|
@ -45,6 +45,46 @@ func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, e
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createTaskAll = `-- name: CreateTaskAll :one
|
||||||
|
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
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTaskAllParams struct {
|
||||||
|
TaskGroupID uuid.UUID `json:"task_group_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Position float64 `json:"position"`
|
||||||
|
Description sql.NullString `json:"description"`
|
||||||
|
Complete bool `json:"complete"`
|
||||||
|
DueDate sql.NullTime `json:"due_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTaskAll(ctx context.Context, arg CreateTaskAllParams) (Task, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createTaskAll,
|
||||||
|
arg.TaskGroupID,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.Name,
|
||||||
|
arg.Position,
|
||||||
|
arg.Description,
|
||||||
|
arg.Complete,
|
||||||
|
arg.DueDate,
|
||||||
|
)
|
||||||
|
var i Task
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TaskID,
|
||||||
|
&i.TaskGroupID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Position,
|
||||||
|
&i.Description,
|
||||||
|
&i.DueDate,
|
||||||
|
&i.Complete,
|
||||||
|
&i.CompletedAt,
|
||||||
|
)
|
||||||
|
return i, 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
|
||||||
`
|
`
|
||||||
@ -305,3 +345,29 @@ func (q *Queries) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams)
|
|||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateTaskPositionParams struct {
|
||||||
|
TaskID uuid.UUID `json:"task_id"`
|
||||||
|
Position float64 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateTaskPosition, arg.TaskID, arg.Position)
|
||||||
|
var i Task
|
||||||
|
err := row.Scan(
|
||||||
|
&i.TaskID,
|
||||||
|
&i.TaskGroupID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Name,
|
||||||
|
&i.Position,
|
||||||
|
&i.Description,
|
||||||
|
&i.DueDate,
|
||||||
|
&i.Complete,
|
||||||
|
&i.CompletedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
const createUserAccount = `-- name: CreateUserAccount :one
|
const createUserAccount = `-- name: CreateUserAccount :one
|
||||||
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
|
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserAccountParams struct {
|
type CreateUserAccountParams struct {
|
||||||
@ -48,6 +48,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -62,7 +63,7 @@ func (q *Queries) DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 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 FROM user_account WHERE username != 'system'
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
|
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
|
||||||
@ -85,6 +86,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -119,7 +121,7 @@ func (q *Queries) GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUserAccountByID = `-- name: GetUserAccountByID :one
|
const getUserAccountByID = `-- name: GetUserAccountByID :one
|
||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE user_id = $1
|
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE user_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
|
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
|
||||||
@ -136,12 +138,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
|
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
|
||||||
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE username = $1
|
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
|
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
|
||||||
@ -158,12 +161,13 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const setUserPassword = `-- name: SetUserPassword :one
|
const setUserPassword = `-- name: SetUserPassword :one
|
||||||
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
|
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||||
`
|
`
|
||||||
|
|
||||||
type SetUserPasswordParams struct {
|
type SetUserPasswordParams struct {
|
||||||
@ -185,13 +189,52 @@ func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserAccountInfo = `-- name: UpdateUserAccountInfo :one
|
||||||
|
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
|
||||||
|
WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserAccountInfoParams struct {
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Initials string `json:"initials"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccountInfoParams) (UserAccount, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateUserAccountInfo,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Bio,
|
||||||
|
arg.FullName,
|
||||||
|
arg.Initials,
|
||||||
|
arg.Email,
|
||||||
|
)
|
||||||
|
var i UserAccount
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Email,
|
||||||
|
&i.Username,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.ProfileBgColor,
|
||||||
|
&i.FullName,
|
||||||
|
&i.Initials,
|
||||||
|
&i.ProfileAvatarUrl,
|
||||||
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
|
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
|
||||||
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
|
||||||
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
|
RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserAccountProfileAvatarURLParams struct {
|
type UpdateUserAccountProfileAvatarURLParams struct {
|
||||||
@ -213,12 +256,13 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUserRole = `-- name: UpdateUserRole :one
|
const updateUserRole = `-- name: UpdateUserRole :one
|
||||||
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code
|
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserRoleParams struct {
|
type UpdateUserRoleParams struct {
|
||||||
@ -240,6 +284,7 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams)
|
|||||||
&i.Initials,
|
&i.Initials,
|
||||||
&i.ProfileAvatarUrl,
|
&i.ProfileAvatarUrl,
|
||||||
&i.RoleCode,
|
&i.RoleCode,
|
||||||
|
&i.Bio,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler returns a new graphql endpoint handler.
|
// NewHandler returns a new graphql endpoint handler.
|
||||||
@ -51,6 +52,8 @@ func NewHandler(repo db.Repository) http.Handler {
|
|||||||
fieldName = "TeamID"
|
fieldName = "TeamID"
|
||||||
case ObjectTypeTask:
|
case ObjectTypeTask:
|
||||||
fieldName = "TaskID"
|
fieldName = "TaskID"
|
||||||
|
case ObjectTypeTaskGroup:
|
||||||
|
fieldName = "TaskGroupID"
|
||||||
default:
|
default:
|
||||||
fieldName = "ProjectID"
|
fieldName = "ProjectID"
|
||||||
}
|
}
|
||||||
@ -68,6 +71,13 @@ func NewHandler(repo db.Repository) http.Handler {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else if typeArg == ObjectTypeTaskGroup {
|
||||||
|
log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching project ID using task group ID")
|
||||||
|
taskGroup, err := repo.GetTaskGroupByID(ctx, subjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subjectID = taskGroup.ProjectID
|
||||||
}
|
}
|
||||||
roles, err := GetProjectRoles(ctx, repo, subjectID)
|
roles, err := GetProjectRoles(ctx, repo, subjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -186,3 +196,13 @@ func GetActionType(actionType int32) ActionType {
|
|||||||
panic("Not a valid entity type!")
|
panic("Not a valid entity type!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotFoundError creates a 404 gqlerror
|
||||||
|
func NotFoundError(message string) error {
|
||||||
|
return &gqlerror.Error{
|
||||||
|
Message: message,
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "404",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -111,6 +111,15 @@ type DeleteTaskGroupPayload struct {
|
|||||||
TaskGroup *db.TaskGroup `json:"taskGroup"`
|
TaskGroup *db.TaskGroup `json:"taskGroup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteTaskGroupTasks struct {
|
||||||
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteTaskGroupTasksPayload struct {
|
||||||
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
|
Tasks []uuid.UUID `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteTaskInput struct {
|
type DeleteTaskInput struct {
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
@ -151,6 +160,17 @@ type DeleteUserAccountPayload struct {
|
|||||||
UserAccount *db.UserAccount `json:"userAccount"`
|
UserAccount *db.UserAccount `json:"userAccount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DuplicateTaskGroup struct {
|
||||||
|
ProjectID uuid.UUID `json:"projectID"`
|
||||||
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Position float64 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DuplicateTaskGroupPayload struct {
|
||||||
|
TaskGroup *db.TaskGroup `json:"taskGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
type FindProject struct {
|
type FindProject struct {
|
||||||
ProjectID uuid.UUID `json:"projectID"`
|
ProjectID uuid.UUID `json:"projectID"`
|
||||||
}
|
}
|
||||||
@ -209,9 +229,9 @@ type NewRefreshToken struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NewTask struct {
|
type NewTask struct {
|
||||||
TaskGroupID string `json:"taskGroupID"`
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Position float64 `json:"position"`
|
Position float64 `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NewTaskGroup struct {
|
type NewTaskGroup struct {
|
||||||
@ -296,10 +316,25 @@ type SetTaskComplete struct {
|
|||||||
Complete bool `json:"complete"`
|
Complete bool `json:"complete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SortTaskGroup struct {
|
||||||
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
|
Tasks []TaskPositionUpdate `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortTaskGroupPayload struct {
|
||||||
|
TaskGroupID uuid.UUID `json:"taskGroupID"`
|
||||||
|
Tasks []db.Task `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
type TaskBadges struct {
|
type TaskBadges struct {
|
||||||
Checklist *ChecklistBadge `json:"checklist"`
|
Checklist *ChecklistBadge `json:"checklist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskPositionUpdate struct {
|
||||||
|
TaskID uuid.UUID `json:"taskID"`
|
||||||
|
Position float64 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
type TeamRole struct {
|
type TeamRole struct {
|
||||||
TeamID uuid.UUID `json:"teamID"`
|
TeamID uuid.UUID `json:"teamID"`
|
||||||
RoleCode RoleCode `json:"roleCode"`
|
RoleCode RoleCode `json:"roleCode"`
|
||||||
@ -420,6 +455,17 @@ type UpdateTeamMemberRolePayload struct {
|
|||||||
Member *Member `json:"member"`
|
Member *Member `json:"member"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateUserInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Initials string `json:"initials"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserInfoPayload struct {
|
||||||
|
User *db.UserAccount `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateUserPassword struct {
|
type UpdateUserPassword struct {
|
||||||
UserID uuid.UUID `json:"userID"`
|
UserID uuid.UUID `json:"userID"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
@ -602,10 +648,11 @@ func (e EntityType) MarshalGQL(w io.Writer) {
|
|||||||
type ObjectType string
|
type ObjectType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ObjectTypeOrg ObjectType = "ORG"
|
ObjectTypeOrg ObjectType = "ORG"
|
||||||
ObjectTypeTeam ObjectType = "TEAM"
|
ObjectTypeTeam ObjectType = "TEAM"
|
||||||
ObjectTypeProject ObjectType = "PROJECT"
|
ObjectTypeProject ObjectType = "PROJECT"
|
||||||
ObjectTypeTask ObjectType = "TASK"
|
ObjectTypeTask ObjectType = "TASK"
|
||||||
|
ObjectTypeTaskGroup ObjectType = "TASK_GROUP"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllObjectType = []ObjectType{
|
var AllObjectType = []ObjectType{
|
||||||
@ -613,11 +660,12 @@ var AllObjectType = []ObjectType{
|
|||||||
ObjectTypeTeam,
|
ObjectTypeTeam,
|
||||||
ObjectTypeProject,
|
ObjectTypeProject,
|
||||||
ObjectTypeTask,
|
ObjectTypeTask,
|
||||||
|
ObjectTypeTaskGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ObjectType) IsValid() bool {
|
func (e ObjectType) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask:
|
case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask, ObjectTypeTaskGroup:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -78,6 +78,7 @@ type UserAccount {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
initials: String!
|
initials: String!
|
||||||
|
bio: String!
|
||||||
role: Role!
|
role: Role!
|
||||||
username: String!
|
username: String!
|
||||||
profileIcon: ProfileIcon!
|
profileIcon: ProfileIcon!
|
||||||
@ -173,6 +174,7 @@ enum ObjectType {
|
|||||||
TEAM
|
TEAM
|
||||||
PROJECT
|
PROJECT
|
||||||
TASK
|
TASK
|
||||||
|
TASK_GROUP
|
||||||
}
|
}
|
||||||
|
|
||||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
|
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
|
||||||
@ -376,20 +378,20 @@ type UpdateProjectMemberRolePayload {
|
|||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTask(input: NewTask!):
|
createTask(input: NewTask!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
|
||||||
deleteTask(input: DeleteTaskInput!):
|
deleteTask(input: DeleteTaskInput!):
|
||||||
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
|
|
||||||
updateTaskDescription(input: UpdateTaskDescriptionInput!):
|
updateTaskDescription(input: UpdateTaskDescriptionInput!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
updateTaskLocation(input: NewTaskLocation!):
|
updateTaskLocation(input: NewTaskLocation!):
|
||||||
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
updateTaskName(input: UpdateTaskName!):
|
updateTaskName(input: UpdateTaskName!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
setTaskComplete(input: SetTaskComplete!):
|
setTaskComplete(input: SetTaskComplete!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
updateTaskDueDate(input: UpdateTaskDueDate!):
|
updateTaskDueDate(input: UpdateTaskDueDate!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
|
|
||||||
assignTask(input: AssignTaskInput):
|
assignTask(input: AssignTaskInput):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
@ -398,7 +400,7 @@ extend type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input NewTask {
|
input NewTask {
|
||||||
taskGroupID: String!
|
taskGroupID: UUID!
|
||||||
name: String!
|
name: String!
|
||||||
position: Float!
|
position: Float!
|
||||||
}
|
}
|
||||||
@ -547,6 +549,47 @@ extend type Mutation {
|
|||||||
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
deleteTaskGroup(input: DeleteTaskGroupInput!):
|
deleteTaskGroup(input: DeleteTaskGroupInput!):
|
||||||
DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
duplicateTaskGroup(input: DuplicateTaskGroup!):
|
||||||
|
DuplicateTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
sortTaskGroup(input: SortTaskGroup!):
|
||||||
|
SortTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
deleteTaskGroupTasks(input: DeleteTaskGroupTasks!):
|
||||||
|
DeleteTaskGroupTasksPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
input NewTaskGroupLocation {
|
||||||
@ -681,6 +724,19 @@ extend type Mutation {
|
|||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!):
|
updateUserRole(input: UpdateUserRole!):
|
||||||
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
updateUserInfo(input: UpdateUserInfo!):
|
||||||
|
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserInfoPayload {
|
||||||
|
user: UserAccount!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInfo {
|
||||||
|
name: String!
|
||||||
|
initials: String!
|
||||||
|
email: String!
|
||||||
|
bio: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateUserPassword {
|
input UpdateUserPassword {
|
||||||
|
@ -179,14 +179,9 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
|
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
|
||||||
taskGroupID, err := uuid.Parse(input.TaskGroupID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("issue while parsing task group ID")
|
|
||||||
return &db.Task{}, err
|
|
||||||
}
|
|
||||||
createdAt := time.Now().UTC()
|
createdAt := time.Now().UTC()
|
||||||
log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": taskGroupID}).Info("creating task")
|
log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task")
|
||||||
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{taskGroupID, createdAt, input.Name, input.Position})
|
task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("issue while creating task")
|
log.WithError(err).Error("issue while creating task")
|
||||||
return &db.Task{}, err
|
return &db.Task{}, err
|
||||||
@ -238,6 +233,9 @@ func (r *mutationResolver) SetTaskComplete(ctx context.Context, input SetTaskCom
|
|||||||
completedAt := time.Now().UTC()
|
completedAt := time.Now().UTC()
|
||||||
task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete, CompletedAt: sql.NullTime{Time: completedAt, Valid: true}})
|
task, err := r.Repository.SetTaskComplete(ctx, db.SetTaskCompleteParams{TaskID: input.TaskID, Complete: input.Complete, CompletedAt: sql.NullTime{Time: completedAt, Valid: true}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return &db.Task{}, NotFoundError("task does not exist")
|
||||||
|
}
|
||||||
return &db.Task{}, err
|
return &db.Task{}, err
|
||||||
}
|
}
|
||||||
return &task, nil
|
return &task, nil
|
||||||
@ -438,6 +436,111 @@ func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTask
|
|||||||
return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil
|
return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) DuplicateTaskGroup(ctx context.Context, input DuplicateTaskGroup) (*DuplicateTaskGroupPayload, error) {
|
||||||
|
createdAt := time.Now().UTC()
|
||||||
|
taskGroup, err := r.Repository.CreateTaskGroup(ctx, db.CreateTaskGroupParams{ProjectID: input.ProjectID, Position: input.Position, Name: input.Name, CreatedAt: createdAt})
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
originalTasks, err := r.Repository.GetTasksForTaskGroupID(ctx, input.TaskGroupID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
for _, originalTask := range originalTasks {
|
||||||
|
task, err := r.Repository.CreateTaskAll(ctx, db.CreateTaskAllParams{
|
||||||
|
TaskGroupID: taskGroup.TaskGroupID, CreatedAt: createdAt, Name: originalTask.Name, Position: originalTask.Position,
|
||||||
|
Complete: originalTask.Complete, DueDate: originalTask.DueDate, Description: originalTask.Description})
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
members, err := r.Repository.GetAssignedMembersForTask(ctx, originalTask.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
for _, member := range members {
|
||||||
|
_, err := r.Repository.CreateTaskAssigned(ctx, db.CreateTaskAssignedParams{
|
||||||
|
TaskID: task.TaskID, UserID: member.UserID, AssignedDate: member.AssignedDate})
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels, err := r.Repository.GetTaskLabelsForTaskID(ctx, originalTask.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
for _, label := range labels {
|
||||||
|
_, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{
|
||||||
|
TaskID: task.TaskID, ProjectLabelID: label.ProjectLabelID, AssignedDate: label.AssignedDate})
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checklists, err := r.Repository.GetTaskChecklistsForTask(ctx, originalTask.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
for _, checklist := range checklists {
|
||||||
|
newChecklist, err := r.Repository.CreateTaskChecklist(ctx, db.CreateTaskChecklistParams{
|
||||||
|
TaskID: task.TaskID, Name: checklist.Name, CreatedAt: createdAt, Position: checklist.Position})
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
checklistItems, err := r.Repository.GetTaskChecklistItemsForTaskChecklist(ctx, checklist.TaskChecklistID)
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
for _, checklistItem := range checklistItems {
|
||||||
|
item, err := r.Repository.CreateTaskChecklistItem(ctx, db.CreateTaskChecklistItemParams{
|
||||||
|
TaskChecklistID: newChecklist.TaskChecklistID,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Name: checklistItem.Name,
|
||||||
|
Position: checklist.Position,
|
||||||
|
})
|
||||||
|
if checklistItem.Complete {
|
||||||
|
r.Repository.SetTaskChecklistItemComplete(ctx, db.SetTaskChecklistItemCompleteParams{TaskChecklistItemID: item.TaskChecklistItemID, Complete: true})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return &DuplicateTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
return &DuplicateTaskGroupPayload{TaskGroup: &taskGroup}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SortTaskGroup(ctx context.Context, input SortTaskGroup) (*SortTaskGroupPayload, error) {
|
||||||
|
tasks := []db.Task{}
|
||||||
|
for _, task := range input.Tasks {
|
||||||
|
t, err := r.Repository.UpdateTaskPosition(ctx, db.UpdateTaskPositionParams{TaskID: task.TaskID, Position: task.Position})
|
||||||
|
if err != nil {
|
||||||
|
return &SortTaskGroupPayload{}, err
|
||||||
|
}
|
||||||
|
tasks = append(tasks, t)
|
||||||
|
}
|
||||||
|
return &SortTaskGroupPayload{Tasks: tasks, TaskGroupID: input.TaskGroupID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) DeleteTaskGroupTasks(ctx context.Context, input DeleteTaskGroupTasks) (*DeleteTaskGroupTasksPayload, error) {
|
||||||
|
tasks, err := r.Repository.GetTasksForTaskGroupID(ctx, input.TaskGroupID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return &DeleteTaskGroupTasksPayload{}, err
|
||||||
|
}
|
||||||
|
removedTasks := []uuid.UUID{}
|
||||||
|
for _, task := range tasks {
|
||||||
|
err = r.Repository.DeleteTaskByID(ctx, task.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return &DeleteTaskGroupTasksPayload{}, err
|
||||||
|
}
|
||||||
|
removedTasks = append(removedTasks, task.TaskID)
|
||||||
|
}
|
||||||
|
return &DeleteTaskGroupTasksPayload{TaskGroupID: input.TaskGroupID, Tasks: removedTasks}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*db.Task, error) {
|
func (r *mutationResolver) AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*db.Task, error) {
|
||||||
assignedDate := time.Now().UTC()
|
assignedDate := time.Now().UTC()
|
||||||
_, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{input.TaskID, input.ProjectLabelID, assignedDate})
|
_, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{input.TaskID, input.ProjectLabelID, assignedDate})
|
||||||
@ -721,6 +824,17 @@ func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserR
|
|||||||
return &UpdateUserRolePayload{User: &user}, nil
|
return &UpdateUserRolePayload{User: &user}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error) {
|
||||||
|
userID, ok := GetUserID(ctx)
|
||||||
|
if !ok {
|
||||||
|
return &UpdateUserInfoPayload{}, errors.New("invalid user ID")
|
||||||
|
}
|
||||||
|
user, err := r.Repository.UpdateUserAccountInfo(ctx, db.UpdateUserAccountInfoParams{
|
||||||
|
Bio: input.Bio, FullName: input.Name, Initials: input.Initials, Email: input.Email, UserID: userID,
|
||||||
|
})
|
||||||
|
return &UpdateUserInfoPayload{User: &user}, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
|
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
|
||||||
return obj.NotificationID, nil
|
return obj.NotificationID, nil
|
||||||
}
|
}
|
||||||
@ -917,6 +1031,14 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
|
|||||||
|
|
||||||
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
|
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
|
||||||
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return &db.Task{}, &gqlerror.Error{
|
||||||
|
Message: "Task does not exist",
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "404",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return &task, err
|
return &task, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1124,6 +1246,9 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
|
|||||||
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
|
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
|
||||||
taskMembers := []Member{}
|
taskMembers := []Member{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return taskMembers, nil
|
||||||
|
}
|
||||||
return taskMembers, err
|
return taskMembers, err
|
||||||
}
|
}
|
||||||
for _, taskMemberLink := range taskMemberLinks {
|
for _, taskMemberLink := range taskMemberLinks {
|
||||||
@ -1158,11 +1283,19 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *taskResolver) Labels(ctx context.Context, obj *db.Task) ([]db.TaskLabel, error) {
|
func (r *taskResolver) Labels(ctx context.Context, obj *db.Task) ([]db.TaskLabel, error) {
|
||||||
return r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
|
labels, err := r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return []db.TaskLabel{}, err
|
||||||
|
}
|
||||||
|
return labels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *taskResolver) Checklists(ctx context.Context, obj *db.Task) ([]db.TaskChecklist, error) {
|
func (r *taskResolver) Checklists(ctx context.Context, obj *db.Task) ([]db.TaskChecklist, error) {
|
||||||
return r.Repository.GetTaskChecklistsForTask(ctx, obj.TaskID)
|
checklists, err := r.Repository.GetTaskChecklistsForTask(ctx, obj.TaskID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return []db.TaskChecklist{}, err
|
||||||
|
}
|
||||||
|
return checklists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, error) {
|
func (r *taskResolver) Badges(ctx context.Context, obj *db.Task) (*TaskBadges, error) {
|
||||||
|
@ -78,6 +78,7 @@ type UserAccount {
|
|||||||
createdAt: Time!
|
createdAt: Time!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
initials: String!
|
initials: String!
|
||||||
|
bio: String!
|
||||||
role: Role!
|
role: Role!
|
||||||
username: String!
|
username: String!
|
||||||
profileIcon: ProfileIcon!
|
profileIcon: ProfileIcon!
|
||||||
|
@ -14,6 +14,7 @@ enum ObjectType {
|
|||||||
TEAM
|
TEAM
|
||||||
PROJECT
|
PROJECT
|
||||||
TASK
|
TASK
|
||||||
|
TASK_GROUP
|
||||||
}
|
}
|
||||||
|
|
||||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
|
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createTask(input: NewTask!):
|
createTask(input: NewTask!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK_GROUP)
|
||||||
deleteTask(input: DeleteTaskInput!):
|
deleteTask(input: DeleteTaskInput!):
|
||||||
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
|
|
||||||
updateTaskDescription(input: UpdateTaskDescriptionInput!):
|
updateTaskDescription(input: UpdateTaskDescriptionInput!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
updateTaskLocation(input: NewTaskLocation!):
|
updateTaskLocation(input: NewTaskLocation!):
|
||||||
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
updateTaskName(input: UpdateTaskName!):
|
updateTaskName(input: UpdateTaskName!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
setTaskComplete(input: SetTaskComplete!):
|
setTaskComplete(input: SetTaskComplete!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
updateTaskDueDate(input: UpdateTaskDueDate!):
|
updateTaskDueDate(input: UpdateTaskDueDate!):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
|
|
||||||
assignTask(input: AssignTaskInput):
|
assignTask(input: AssignTaskInput):
|
||||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||||
@ -22,7 +22,7 @@ extend type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input NewTask {
|
input NewTask {
|
||||||
taskGroupID: String!
|
taskGroupID: UUID!
|
||||||
name: String!
|
name: String!
|
||||||
position: Float!
|
position: Float!
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,47 @@ extend type Mutation {
|
|||||||
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
deleteTaskGroup(input: DeleteTaskGroupInput!):
|
deleteTaskGroup(input: DeleteTaskGroupInput!):
|
||||||
DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
duplicateTaskGroup(input: DuplicateTaskGroup!):
|
||||||
|
DuplicateTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
sortTaskGroup(input: SortTaskGroup!):
|
||||||
|
SortTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
deleteTaskGroupTasks(input: DeleteTaskGroupTasks!):
|
||||||
|
DeleteTaskGroupTasksPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
input NewTaskGroupLocation {
|
||||||
|
@ -11,6 +11,19 @@ extend type Mutation {
|
|||||||
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
|
||||||
updateUserRole(input: UpdateUserRole!):
|
updateUserRole(input: UpdateUserRole!):
|
||||||
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
updateUserInfo(input: UpdateUserInfo!):
|
||||||
|
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserInfoPayload {
|
||||||
|
user: UserAccount!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInfo {
|
||||||
|
name: String!
|
||||||
|
initials: String!
|
||||||
|
email: String!
|
||||||
|
bio: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateUserPassword {
|
input UpdateUserPassword {
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/frontend"
|
"github.com/jordanknott/taskcafe/internal/frontend"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frontend serves the index.html file
|
// Frontend serves the index.html file
|
||||||
@ -30,7 +31,7 @@ func (h *TaskcafeHandler) Frontend(w http.ResponseWriter, r *http.Request) {
|
|||||||
// ProfileImageUpload handles a user uploading a new avatar profile image
|
// ProfileImageUpload handles a user uploading a new avatar profile image
|
||||||
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
|
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Info("preparing to upload file")
|
log.Info("preparing to upload file")
|
||||||
userID, ok := r.Context().Value("userID").(uuid.UUID)
|
userID, ok := r.Context().Value(utils.UserIDKey).(uuid.UUID)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error("not a valid uuid")
|
log.Error("not a valid uuid")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
1
migrations/0052_add-bio-col-to-user_account.up.sql
Normal file
1
migrations/0052_add-bio-col-to-user_account.up.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE user_account ADD COLUMN bio text NOT NULL DEFAULT '';
|
2
scripts/lint.sh
Executable file
2
scripts/lint.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
yarn --cwd frontend eslint $(echo $1 | sed 's/frontend\///g')
|
Reference in New Issue
Block a user