Compare commits
No commits in common. "e3274cdecf54dcc2db0da1373845e167d7532fd8" and "7fc1a7f3af4931100bea1697f65133df24f47a28" have entirely different histories.
e3274cdecf
...
7fc1a7f3af
|
@ -4,31 +4,19 @@ import { HomePage } from './HomePage/HomePage';
|
||||||
import { ProfilePage } from './ProfilePage/ProfilePage';
|
import { ProfilePage } from './ProfilePage/ProfilePage';
|
||||||
import { DashboardPage } from './DashboardPage/DashboardPage';
|
import { DashboardPage } from './DashboardPage/DashboardPage';
|
||||||
import { PrivateRoute } from './PrivateRoute';
|
import { PrivateRoute } from './PrivateRoute';
|
||||||
import { useLoggedIn, LoggedInContext, getSavedLoggedIn, saveLoggedIn } from '../hooks/useLoggedIn';
|
import { useLoggedIn, LoggedInContext } from '../hooks/useLoggedIn';
|
||||||
import { useUserProfile } from '../gql/queries/user';
|
import { useUserProfile } from '../gql/queries/user';
|
||||||
import { ProjectPage } from './ProjectPage/ProjectPage';
|
import { ProjectPage } from './ProjectPage/ProjectPage';
|
||||||
import { createClient } from '../util/apollo';
|
|
||||||
import { ApolloProvider } from '@apollo/client';
|
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App: FunctionComponent<AppProps> = () => {
|
export const App: FunctionComponent<AppProps> = () => {
|
||||||
const [ loggedIn, setLoggedIn ] = useState(getSavedLoggedIn());
|
const { user } = useUserProfile();
|
||||||
|
|
||||||
const client = createClient((loggedIn) => {
|
|
||||||
setLoggedIn(loggedIn);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
saveLoggedIn(loggedIn);
|
|
||||||
}, [loggedIn]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<LoggedInContext.Provider value={user.id !== ''}>
|
||||||
<LoggedInContext.Provider value={loggedIn}>
|
|
||||||
<UserSessionCheck setLoggedIn={setLoggedIn} />
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" exact component={HomePage} />
|
<Route path="/" exact component={HomePage} />
|
||||||
|
@ -39,22 +27,5 @@ export const App: FunctionComponent<AppProps> = () => {
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</LoggedInContext.Provider>
|
</LoggedInContext.Provider>
|
||||||
</ApolloProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface UserSessionCheckProps {
|
|
||||||
setLoggedIn: (boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSessionCheck: FunctionComponent<UserSessionCheckProps> = ({ setLoggedIn }) => {
|
|
||||||
const { user, loading } = useUserProfile();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loading) return;
|
|
||||||
setLoggedIn(user.id !== '');
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
|
@ -17,6 +17,11 @@ const EditableText: FunctionComponent<EditableTextProps> = ({ onChange, value, r
|
||||||
const [ internalValue, setInternalValue ] = useState(value);
|
const [ internalValue, setInternalValue ] = useState(value);
|
||||||
const [ editMode, setEditMode ] = useState(false);
|
const [ editMode, setEditMode ] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (internalValue === value) return;
|
||||||
|
if (onChange) onChange(internalValue);
|
||||||
|
}, [internalValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalValue(value);
|
setInternalValue(value);
|
||||||
}, [value])
|
}, [value])
|
||||||
|
@ -27,8 +32,6 @@ const EditableText: FunctionComponent<EditableTextProps> = ({ onChange, value, r
|
||||||
|
|
||||||
const onValidateButtonClick = () => {
|
const onValidateButtonClick = () => {
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
if (internalValue === value) return;
|
|
||||||
if (onChange) onChange(internalValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onValueChange = (evt: ChangeEvent) => {
|
const onValueChange = (evt: ChangeEvent) => {
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const FRAGMENT_FULL_TASK = gql`
|
export const FRAGMENT_FULL_PROJECT = gql`
|
||||||
fragment FullTask on Task {
|
fragment FullProject on Project {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
taskCategories {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
costPerTimeUnit
|
||||||
|
}
|
||||||
|
tasks {
|
||||||
id
|
id
|
||||||
label
|
label
|
||||||
category {
|
category {
|
||||||
|
@ -14,10 +22,7 @@ fragment FullTask on Task {
|
||||||
pessimistic
|
pessimistic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
params {
|
||||||
|
|
||||||
export const FRAGMENT_FULL_PROJECT_PARAMS = gql`
|
|
||||||
fragment FullProjectParams on ProjectParams {
|
|
||||||
timeUnit {
|
timeUnit {
|
||||||
label
|
label
|
||||||
acronym
|
acronym
|
||||||
|
@ -25,31 +30,5 @@ fragment FullProjectParams on ProjectParams {
|
||||||
currency
|
currency
|
||||||
hideFinancialPreviewOnPrint
|
hideFinancialPreviewOnPrint
|
||||||
}
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
export const FRAGMENT_FULL_TASK_CATEGORY = gql`
|
|
||||||
fragment FullTaskCategory on TaskCategory {
|
|
||||||
id
|
|
||||||
label
|
|
||||||
costPerTimeUnit
|
|
||||||
}
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
export const FRAGMENT_FULL_PROJECT = gql`
|
|
||||||
fragment FullProject on Project {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
taskCategories {
|
|
||||||
...FullTaskCategory
|
|
||||||
}
|
|
||||||
tasks {
|
|
||||||
...FullTask
|
|
||||||
}
|
|
||||||
params {
|
|
||||||
...FullProjectParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${FRAGMENT_FULL_TASK}
|
|
||||||
${FRAGMENT_FULL_PROJECT_PARAMS}
|
|
||||||
${FRAGMENT_FULL_TASK_CATEGORY}
|
|
||||||
`
|
`
|
|
@ -1,5 +1,5 @@
|
||||||
import { gql, useMutation, PureQueryOptions } from '@apollo/client';
|
import { gql, useMutation, PureQueryOptions } from '@apollo/client';
|
||||||
import { FRAGMENT_FULL_PROJECT, FRAGMENT_FULL_TASK, FRAGMENT_FULL_PROJECT_PARAMS, FRAGMENT_FULL_TASK_CATEGORY } from '../fragments/project';
|
import { FRAGMENT_FULL_PROJECT } from '../fragments/project';
|
||||||
import { QUERY_PROJECTS } from '../queries/project';
|
import { QUERY_PROJECTS } from '../queries/project';
|
||||||
|
|
||||||
export const MUTATION_CREATE_PROJECT = gql`
|
export const MUTATION_CREATE_PROJECT = gql`
|
||||||
|
@ -37,10 +37,9 @@ export function useUpdateProjectTitleMutation() {
|
||||||
export const MUTATION_ADD_PROJECT_TASK = gql`
|
export const MUTATION_ADD_PROJECT_TASK = gql`
|
||||||
mutation addProjectTask($projectId: ID!, $changes: ProjectTaskChanges!) {
|
mutation addProjectTask($projectId: ID!, $changes: ProjectTaskChanges!) {
|
||||||
addProjectTask(projectId: $projectId, changes: $changes) {
|
addProjectTask(projectId: $projectId, changes: $changes) {
|
||||||
...FullTask
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${FRAGMENT_FULL_TASK}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function useAddProjectTaskMutation() {
|
export function useAddProjectTaskMutation() {
|
||||||
|
@ -50,10 +49,9 @@ export function useAddProjectTaskMutation() {
|
||||||
export const MUTATION_UPDATE_PROJECT_TASK = gql`
|
export const MUTATION_UPDATE_PROJECT_TASK = gql`
|
||||||
mutation updateProjectTask($projectId: ID!, $taskId: ID!, $changes: ProjectTaskChanges!) {
|
mutation updateProjectTask($projectId: ID!, $taskId: ID!, $changes: ProjectTaskChanges!) {
|
||||||
updateProjectTask(projectId: $projectId, taskId: $taskId, changes: $changes) {
|
updateProjectTask(projectId: $projectId, taskId: $taskId, changes: $changes) {
|
||||||
...FullTask
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${FRAGMENT_FULL_TASK}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function useUpdateProjectTaskMutation() {
|
export function useUpdateProjectTaskMutation() {
|
||||||
|
@ -69,52 +67,3 @@ mutation removeProjectTask($projectId: ID!, $taskId: ID!) {
|
||||||
export function useRemoveProjectTaskMutation() {
|
export function useRemoveProjectTaskMutation() {
|
||||||
return useMutation(MUTATION_REMOVE_PROJECT_TASK);
|
return useMutation(MUTATION_REMOVE_PROJECT_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MUTATION_UPDATE_PROJECT_PARAMS = gql`
|
|
||||||
mutation updateProjectParams($projectId: ID!, $changes: ProjectParamsChanges!) {
|
|
||||||
updateProjectParams(projectId: $projectId, changes: $changes) {
|
|
||||||
...FullProjectParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${FRAGMENT_FULL_PROJECT_PARAMS}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function useUpdateProjectParamsMutation() {
|
|
||||||
return useMutation(MUTATION_UPDATE_PROJECT_PARAMS);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MUTATION_ADD_PROJECT_TASK_CATEGORY = gql`
|
|
||||||
mutation addProjectTaskCategory($projectId: ID!, $changes: ProjectTaskCategoryChanges!) {
|
|
||||||
addProjectTaskCategory(projectId: $projectId, changes: $changes) {
|
|
||||||
...FullTaskCategory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${FRAGMENT_FULL_TASK_CATEGORY}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function useAddProjectTaskCategoryMutation() {
|
|
||||||
return useMutation(MUTATION_ADD_PROJECT_TASK_CATEGORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MUTATION_UPDATE_PROJECT_TASK_CATEGORY = gql`
|
|
||||||
mutation updateProjectTaskCategory($projectId: ID!, $taskCategoryId: ID!, $changes: ProjectTaskCategoryChanges!) {
|
|
||||||
updateProjectTaskCategory(projectId: $projectId, taskCategoryId: $taskCategoryId, changes: $changes) {
|
|
||||||
...FullTaskCategory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${FRAGMENT_FULL_TASK_CATEGORY}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function useUpdateProjectTaskCategoryMutation() {
|
|
||||||
return useMutation(MUTATION_UPDATE_PROJECT_TASK_CATEGORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MUTATION_REMOVE_PROJECT_TASK_CATEGORY = gql`
|
|
||||||
mutation removeProjectTaskCategory($projectId: ID!, $taskCategoryId: ID!) {
|
|
||||||
removeProjectTaskCategory(projectId: $projectId, taskCategoryId: $taskCategoryId)
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function useRemoveProjectTaskCategoryMutation() {
|
|
||||||
return useMutation(MUTATION_REMOVE_PROJECT_TASK_CATEGORY);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,41 @@
|
||||||
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { User } from '../../types/user';
|
||||||
import { useGraphQLData } from './helper';
|
import { useGraphQLData } from './helper';
|
||||||
import { Project } from '../../types/project';
|
import { Project } from '../../types/project';
|
||||||
import { FRAGMENT_FULL_PROJECT } from '../fragments/project';
|
|
||||||
|
|
||||||
export const QUERY_PROJECTS = gql`
|
export const QUERY_PROJECTS = gql`
|
||||||
query projects($filter: ProjectsFilter) {
|
query projects($filter: ProjectsFilter) {
|
||||||
projects(filter: $filter) {
|
projects(filter: $filter) {
|
||||||
...FullProject
|
id
|
||||||
|
title
|
||||||
|
taskCategories {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
costPerTimeUnit
|
||||||
|
}
|
||||||
|
tasks {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
category {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
}
|
||||||
|
estimations {
|
||||||
|
optimistic
|
||||||
|
likely
|
||||||
|
pessimistic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${FRAGMENT_FULL_PROJECT}`;
|
params {
|
||||||
|
timeUnit {
|
||||||
|
label
|
||||||
|
acronym
|
||||||
|
}
|
||||||
|
currency
|
||||||
|
hideFinancialPreviewOnPrint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
export function useProjectsQuery() {
|
export function useProjectsQuery() {
|
||||||
return useQuery(QUERY_PROJECTS);
|
return useQuery(QUERY_PROJECTS);
|
||||||
|
|
|
@ -1,22 +1,7 @@
|
||||||
import React, { useContext, useEffect } from "react";
|
import React, { useState, useContext } from "react";
|
||||||
|
|
||||||
const LOGGED_IN_KEY = 'loggedIn';
|
export const LoggedInContext = React.createContext(false);
|
||||||
|
|
||||||
export const LoggedInContext = React.createContext(getSavedLoggedIn());
|
|
||||||
|
|
||||||
export const useLoggedIn = () => {
|
export const useLoggedIn = () => {
|
||||||
return useContext(LoggedInContext);
|
return useContext(LoggedInContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function saveLoggedIn(loggedIn: boolean) {
|
|
||||||
window.sessionStorage.setItem(LOGGED_IN_KEY, JSON.stringify(loggedIn));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSavedLoggedIn(): boolean {
|
|
||||||
try {
|
|
||||||
const loggedIn = JSON.parse(window.sessionStorage.getItem(LOGGED_IN_KEY));
|
|
||||||
return !!loggedIn;
|
|
||||||
} catch(err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +1,21 @@
|
||||||
import { all, select, takeLatest, put, delay } from "redux-saga/effects";
|
import { all, select, takeLatest, put, delay } from "redux-saga/effects";
|
||||||
import { client } from '../gql/client';
|
import { client } from '../gql/client';
|
||||||
import {
|
import { MUTATION_CREATE_PROJECT, MUTATION_UPDATE_PROJECT_TITLE, MUTATION_ADD_PROJECT_TASK, MUTATION_REMOVE_PROJECT_TASK } from "../gql/mutations/project";
|
||||||
MUTATION_CREATE_PROJECT, MUTATION_UPDATE_PROJECT_TITLE,
|
import { UPDATE_PROJECT_TITLE, resetProject, ADD_TASK, taskSaved, AddTask, taskAdded, taskRemoved, RemoveTask, REMOVE_TASK } from "./useProjectReducer";
|
||||||
MUTATION_ADD_PROJECT_TASK, MUTATION_REMOVE_PROJECT_TASK,
|
|
||||||
MUTATION_UPDATE_PROJECT_TASK, MUTATION_UPDATE_PROJECT_PARAMS,
|
|
||||||
MUTATION_ADD_PROJECT_TASK_CATEGORY,
|
|
||||||
MUTATION_UPDATE_PROJECT_TASK_CATEGORY,
|
|
||||||
MUTATION_REMOVE_PROJECT_TASK_CATEGORY
|
|
||||||
} from "../gql/mutations/project";
|
|
||||||
import {
|
|
||||||
UPDATE_PROJECT_TITLE, resetProject,
|
|
||||||
ADD_TASK, taskSaved, AddTask,
|
|
||||||
taskRemoved, RemoveTask, REMOVE_TASK,
|
|
||||||
UPDATE_TASK_ESTIMATION, UpdateTaskEstimation,
|
|
||||||
UpdateTaskLabel, UPDATE_TASK_LABEL, UpdateParam,
|
|
||||||
paramsSaved, UPDATE_PARAM, AddTaskCategory,
|
|
||||||
ADD_TASK_CATEGORY, taskCategorySaved, UpdateTaskCategoryLabel, UPDATE_TASK_CATEGORY_LABEL, UPDATE_TASK_CATEGORY_COST, updateTaskCategoryCost, UpdateTaskCategoryCost, RemoveTaskCategory, taskCategoryRemoved, REMOVE_TASK_CATEGORY, backendError
|
|
||||||
} from "./useProjectReducer";
|
|
||||||
import { Project } from "../types/project";
|
import { Project } from "../types/project";
|
||||||
|
|
||||||
export function* rootSaga() {
|
export function* rootSaga() {
|
||||||
yield all([
|
yield all([
|
||||||
createProjectSaga(),
|
createProjectSaga(),
|
||||||
takeLatest(UPDATE_PROJECT_TITLE, updateProjectTitleSaga),
|
takeLatest(UPDATE_PROJECT_TITLE, updateProjectTitleSaga),
|
||||||
takeLatest(UPDATE_TASK_ESTIMATION, updateTaskEstimationSaga),
|
|
||||||
takeLatest(UPDATE_TASK_LABEL, updateTaskLabelSaga),
|
|
||||||
takeLatest(UPDATE_PARAM, updateProjectParamsSaga),
|
|
||||||
takeLatest(ADD_TASK, addTaskSaga),
|
takeLatest(ADD_TASK, addTaskSaga),
|
||||||
takeLatest(REMOVE_TASK, removeTaskSaga),
|
takeLatest(REMOVE_TASK, removeTaskSaga),
|
||||||
takeLatest(ADD_TASK_CATEGORY, addProjectTaskCategorySaga),
|
|
||||||
takeLatest(UPDATE_TASK_CATEGORY_LABEL, updateProjectTaskCategoryLabelSaga),
|
|
||||||
takeLatest(UPDATE_TASK_CATEGORY_COST, updateProjectTaskCategoryCostSaga),
|
|
||||||
takeLatest(REMOVE_TASK_CATEGORY, removeProjectTaskCategorySaga),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* updateProjectTitleSaga() {
|
export function* updateProjectTitleSaga() {
|
||||||
|
yield delay(500);
|
||||||
|
|
||||||
let project = yield select();
|
let project = yield select();
|
||||||
|
|
||||||
if (project.id === undefined) {
|
if (project.id === undefined) {
|
||||||
|
@ -93,7 +73,7 @@ export function* addTaskSaga({ task }: AddTask) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
yield put(taskSaved({ ...task, ...data.addProjectTask }));
|
yield put(taskAdded({ ...task, ...data.addProjectTask }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* removeTaskSaga({ id }: RemoveTask) {
|
export function* removeTaskSaga({ id }: RemoveTask) {
|
||||||
|
@ -113,160 +93,3 @@ export function* removeTaskSaga({ id }: RemoveTask) {
|
||||||
|
|
||||||
yield put(taskRemoved(id));
|
yield put(taskRemoved(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* updateTaskEstimationSaga({ id, confidence, value }: UpdateTaskEstimation) {
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = yield client.mutate({
|
|
||||||
mutation: MUTATION_UPDATE_PROJECT_TASK,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
taskId: id,
|
|
||||||
changes: {
|
|
||||||
estimations: {
|
|
||||||
[confidence]: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(taskSaved({ ...data.updateProjectTask }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* updateTaskLabelSaga({ id, label }: UpdateTaskLabel) {
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = yield client.mutate({
|
|
||||||
mutation: MUTATION_UPDATE_PROJECT_TASK,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
taskId: id,
|
|
||||||
changes: {
|
|
||||||
label,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(taskSaved({ ...data.updateProjectTask }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* updateProjectParamsSaga({ name, value }: UpdateParam) {
|
|
||||||
yield delay(500);
|
|
||||||
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
delete value.__typename;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = yield client.mutate({
|
|
||||||
mutation: MUTATION_UPDATE_PROJECT_PARAMS,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
changes: {
|
|
||||||
[name]: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(paramsSaved({ ...data.updateProjectParams }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* addProjectTaskCategorySaga({ taskCategory }: AddTaskCategory) {
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = yield client.mutate({
|
|
||||||
mutation: MUTATION_ADD_PROJECT_TASK_CATEGORY,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
changes: {
|
|
||||||
label: taskCategory.label,
|
|
||||||
costPerTimeUnit: taskCategory.costPerTimeUnit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(taskCategorySaved({ ...data.addProjectTaskCategory }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* updateProjectTaskCategoryLabelSaga({ categoryId, label }: UpdateTaskCategoryLabel) {
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = yield client.mutate({
|
|
||||||
mutation: MUTATION_UPDATE_PROJECT_TASK_CATEGORY,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
taskCategoryId: categoryId,
|
|
||||||
changes: {
|
|
||||||
label,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(taskCategorySaved({ ...data.updateProjectTaskCategory }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* updateProjectTaskCategoryCostSaga({ categoryId, costPerTimeUnit }: UpdateTaskCategoryCost) {
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = yield client.mutate({
|
|
||||||
mutation: MUTATION_UPDATE_PROJECT_TASK_CATEGORY,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
taskCategoryId: categoryId,
|
|
||||||
changes: {
|
|
||||||
costPerTimeUnit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield put(taskCategorySaved({ ...data.updateProjectTaskCategory }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* removeProjectTaskCategorySaga({ taskCategoryId }: RemoveTaskCategory) {
|
|
||||||
let project: Project = yield select();
|
|
||||||
|
|
||||||
if (project.id === undefined) {
|
|
||||||
project = yield createProjectSaga();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
yield client.mutate({
|
|
||||||
mutation: MUTATION_REMOVE_PROJECT_TASK_CATEGORY,
|
|
||||||
variables: {
|
|
||||||
projectId: project.id,
|
|
||||||
taskCategoryId,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch(err) {
|
|
||||||
yield put(backendError(err));
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
yield put(taskCategoryRemoved(taskCategoryId));
|
|
||||||
}
|
|
|
@ -3,11 +3,6 @@ import { Task, TaskID, EstimationConfidence, TaskCategoryID, TaskCategory } from
|
||||||
import { useReducerAndSaga } from "./useReducerAndSaga";
|
import { useReducerAndSaga } from "./useReducerAndSaga";
|
||||||
import { rootSaga } from "./useProjectReducer.sagas";
|
import { rootSaga } from "./useProjectReducer.sagas";
|
||||||
import { uuid } from "../util/uuid";
|
import { uuid } from "../util/uuid";
|
||||||
import { Params } from "../types/params";
|
|
||||||
|
|
||||||
export interface ProjectState extends Project {
|
|
||||||
lastBackendError: Error
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
type: string
|
type: string
|
||||||
|
@ -22,24 +17,23 @@ UpdateTaskEstimation |
|
||||||
UpdateProjectTitle |
|
UpdateProjectTitle |
|
||||||
UpdateTaskLabel |
|
UpdateTaskLabel |
|
||||||
UpdateParam |
|
UpdateParam |
|
||||||
ParamsSaved |
|
|
||||||
UpdateTaskCategoryLabel |
|
UpdateTaskCategoryLabel |
|
||||||
UpdateTaskCategoryCost |
|
UpdateTaskCategoryCost |
|
||||||
AddTaskCategory |
|
AddTaskCategory |
|
||||||
RemoveTaskCategory |
|
RemoveTaskCategory |
|
||||||
TaskCategoryRemoved |
|
ResetProject
|
||||||
TaskCategorySaved |
|
|
||||||
ResetProject |
|
|
||||||
BackendError
|
|
||||||
|
|
||||||
export function useProjectReducer(project: Project) {
|
export function useProjectReducer(project: Project) {
|
||||||
return useReducerAndSaga<ProjectState>(projectReducer, project, rootSaga);
|
return useReducerAndSaga<Project>(projectReducer, project, rootSaga);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectReducer(project: ProjectState, action: ProjectReducerActions): ProjectState {
|
export function projectReducer(project: Project, action: ProjectReducerActions): Project {
|
||||||
console.log(action.type, action);
|
console.log(action);
|
||||||
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case TASK_ADDED:
|
||||||
|
return handleTaskAdded(project, action as TaskAdded);
|
||||||
|
|
||||||
case TASK_SAVED:
|
case TASK_SAVED:
|
||||||
return handleTaskSaved(project, action as TaskSaved);
|
return handleTaskSaved(project, action as TaskSaved);
|
||||||
|
|
||||||
|
@ -58,21 +52,21 @@ export function projectReducer(project: ProjectState, action: ProjectReducerActi
|
||||||
case UPDATE_PARAM:
|
case UPDATE_PARAM:
|
||||||
return handleUpdateParam(project, action as UpdateParam);
|
return handleUpdateParam(project, action as UpdateParam);
|
||||||
|
|
||||||
case PARAMS_SAVED:
|
case ADD_TASK_CATEGORY:
|
||||||
return handleParamsSaved(project, action as ParamsSaved);
|
return handleAddTaskCategory(project, action as AddTaskCategory);
|
||||||
|
|
||||||
case TASK_CATEGORY_SAVED:
|
case REMOVE_TASK_CATEGORY:
|
||||||
return handleTaskCategorySaved(project, action as TaskCategorySaved);
|
return handleRemoveTaskCategory(project, action as RemoveTaskCategory);
|
||||||
|
|
||||||
case TASK_CATEGORY_REMOVED:
|
case UPDATE_TASK_CATEGORY_LABEL:
|
||||||
return handleTaskCategoryRemoved(project, action as TaskCategoryRemoved);
|
return handleUpdateTaskCategoryLabel(project, action as UpdateTaskCategoryLabel);
|
||||||
|
|
||||||
|
case UPDATE_TASK_CATEGORY_COST:
|
||||||
|
return handleUpdateTaskCategoryCost(project, action as UpdateTaskCategoryCost);
|
||||||
|
|
||||||
case RESET_PROJECT:
|
case RESET_PROJECT:
|
||||||
return handleResetProject(project, action as ResetProject);
|
return handleResetProject(project, action as ResetProject);
|
||||||
|
|
||||||
case BACKEND_ERROR:
|
|
||||||
return handleBackendError(project, action as BackendError);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
|
@ -92,6 +86,23 @@ export interface TaskAdded extends Action {
|
||||||
task: Task
|
task: Task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TASK_ADDED = "TASK_ADDED";
|
||||||
|
|
||||||
|
export function taskAdded(task: Task): TaskAdded {
|
||||||
|
return { type: TASK_ADDED, task };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleTaskAdded(project: Project, action: TaskAdded): Project {
|
||||||
|
const task = { ...action.task };
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
tasks: [
|
||||||
|
...project.tasks,
|
||||||
|
task,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const TASK_SAVED = "TASK_SAVED";
|
export const TASK_SAVED = "TASK_SAVED";
|
||||||
|
|
||||||
export interface TaskSaved extends Action {
|
export interface TaskSaved extends Action {
|
||||||
|
@ -102,14 +113,11 @@ export function taskSaved(task: Task): TaskSaved {
|
||||||
return { type: TASK_SAVED, task };
|
return { type: TASK_SAVED, task };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleTaskSaved(project: ProjectState, action: TaskSaved): ProjectState {
|
export function handleTaskSaved(project: Project, action: TaskSaved): Project {
|
||||||
const taskIndex = project.tasks.findIndex(t => t.id === action.task.id);
|
const taskIndex = project.tasks.findIndex(t => t.id === action.task.id);
|
||||||
|
if (taskIndex === -1) return project;
|
||||||
const tasks = [ ...project.tasks ];
|
const tasks = [ ...project.tasks ];
|
||||||
if (taskIndex === -1) {
|
|
||||||
tasks.push({ ...action.task });
|
|
||||||
} else {
|
|
||||||
tasks[taskIndex] = { ...tasks[taskIndex], ...action.task };
|
tasks[taskIndex] = { ...tasks[taskIndex], ...action.task };
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
tasks
|
tasks
|
||||||
|
@ -137,7 +145,7 @@ export function taskRemoved(id: number): TaskRemoved {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function handleTaskRemoved(project: ProjectState, action: TaskRemoved): ProjectState {
|
export function handleTaskRemoved(project: Project, action: TaskRemoved): Project {
|
||||||
const tasks = [...project.tasks];
|
const tasks = [...project.tasks];
|
||||||
const taskIndex = project.tasks.findIndex(t => t.id === action.id);
|
const taskIndex = project.tasks.findIndex(t => t.id === action.id);
|
||||||
if (taskIndex === -1) return project;
|
if (taskIndex === -1) return project;
|
||||||
|
@ -160,7 +168,7 @@ export function updateTaskEstimation(id: number, confidence: EstimationConfidenc
|
||||||
return { type: UPDATE_TASK_ESTIMATION, id, confidence, value };
|
return { type: UPDATE_TASK_ESTIMATION, id, confidence, value };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleUpdateTaskEstimation(project: ProjectState, action: UpdateTaskEstimation): ProjectState {
|
export function handleUpdateTaskEstimation(project: Project, action: UpdateTaskEstimation): Project {
|
||||||
const tasks = [...project.tasks];
|
const tasks = [...project.tasks];
|
||||||
const taskIndex = project.tasks.findIndex(t => t.id === action.id);
|
const taskIndex = project.tasks.findIndex(t => t.id === action.id);
|
||||||
if (taskIndex === -1) return project;
|
if (taskIndex === -1) return project;
|
||||||
|
@ -170,6 +178,14 @@ export function handleUpdateTaskEstimation(project: ProjectState, action: Update
|
||||||
[action.confidence]: action.value
|
[action.confidence]: action.value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (estimations.likely < estimations.optimistic) {
|
||||||
|
estimations.likely = estimations.optimistic;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estimations.pessimistic < estimations.likely) {
|
||||||
|
estimations.pessimistic = estimations.likely;
|
||||||
|
}
|
||||||
|
|
||||||
project.tasks[taskIndex] = { ...project.tasks[taskIndex], estimations };
|
project.tasks[taskIndex] = { ...project.tasks[taskIndex], estimations };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -189,7 +205,7 @@ export function updateProjectTitle(title: string): UpdateProjectTitle {
|
||||||
return { type: UPDATE_PROJECT_TITLE, title };
|
return { type: UPDATE_PROJECT_TITLE, title };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleUpdateProjectTitle(project: ProjectState, action: UpdateProjectTitle): ProjectState {
|
export function handleUpdateProjectTitle(project: Project, action: UpdateProjectTitle): Project {
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
title: action.title
|
title: action.title
|
||||||
|
@ -207,7 +223,7 @@ export function updateTaskLabel(id: number, label: string): UpdateTaskLabel {
|
||||||
return { type: UPDATE_TASK_LABEL, id, label };
|
return { type: UPDATE_TASK_LABEL, id, label };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleUpdateTaskLabel(project: ProjectState, action: UpdateTaskLabel): ProjectState {
|
export function handleUpdateTaskLabel(project: Project, action: UpdateTaskLabel): Project {
|
||||||
const tasks = [...project.tasks];
|
const tasks = [...project.tasks];
|
||||||
const taskIndex = project.tasks.findIndex(t => t.id === action.id);
|
const taskIndex = project.tasks.findIndex(t => t.id === action.id);
|
||||||
if (taskIndex === -1) return project;
|
if (taskIndex === -1) return project;
|
||||||
|
@ -231,7 +247,7 @@ export function updateParam(name: string, value: any): UpdateParam {
|
||||||
return { type: UPDATE_PARAM, name, value };
|
return { type: UPDATE_PARAM, name, value };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleUpdateParam(project: ProjectState, action: UpdateParam): ProjectState {
|
export function handleUpdateParam(project: Project, action: UpdateParam): Project {
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
params: {
|
params: {
|
||||||
|
@ -241,51 +257,6 @@ export function handleUpdateParam(project: ProjectState, action: UpdateParam): P
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParamsSaved extends Action {
|
|
||||||
params: Params
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PARAMS_SAVED = "PARAMS_SAVED";
|
|
||||||
|
|
||||||
export function paramsSaved(params: Params): ParamsSaved {
|
|
||||||
return { type: PARAMS_SAVED, params };
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleParamsSaved(project: ProjectState, action: ParamsSaved): ProjectState {
|
|
||||||
return {
|
|
||||||
...project,
|
|
||||||
params: {
|
|
||||||
...action.params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TaskCategorySaved extends Action {
|
|
||||||
taskCategory: TaskCategory
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TASK_CATEGORY_SAVED = "TASK_CATEGORY_SAVED";
|
|
||||||
|
|
||||||
export function taskCategorySaved(taskCategory: TaskCategory): TaskCategorySaved {
|
|
||||||
return { type: TASK_CATEGORY_SAVED, taskCategory };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleTaskCategorySaved(project: ProjectState, action: TaskCategorySaved): ProjectState {
|
|
||||||
const taskCategories = [...project.taskCategories];
|
|
||||||
const taskCategoryIndex = taskCategories.findIndex(tc => tc.id === action.taskCategory.id);
|
|
||||||
|
|
||||||
if (taskCategoryIndex === -1) {
|
|
||||||
taskCategories.push({ ...action.taskCategory });
|
|
||||||
} else {
|
|
||||||
taskCategories[taskCategoryIndex] = { ...action.taskCategory };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...project,
|
|
||||||
taskCategories
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateTaskCategoryLabel extends Action {
|
export interface UpdateTaskCategoryLabel extends Action {
|
||||||
categoryId: TaskCategoryID
|
categoryId: TaskCategoryID
|
||||||
label: string
|
label: string
|
||||||
|
@ -297,6 +268,19 @@ export function updateTaskCategoryLabel(categoryId: TaskCategoryID, label: strin
|
||||||
return { type: UPDATE_TASK_CATEGORY_LABEL, categoryId, label };
|
return { type: UPDATE_TASK_CATEGORY_LABEL, categoryId, label };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleUpdateTaskCategoryLabel(project: Project, action: UpdateTaskCategoryLabel): Project {
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
taskCategories: {
|
||||||
|
...project.taskCategories,
|
||||||
|
[action.categoryId]: {
|
||||||
|
...project.taskCategories[action.categoryId],
|
||||||
|
label: action.label
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateTaskCategoryCost extends Action {
|
export interface UpdateTaskCategoryCost extends Action {
|
||||||
categoryId: TaskCategoryID
|
categoryId: TaskCategoryID
|
||||||
costPerTimeUnit: number
|
costPerTimeUnit: number
|
||||||
|
@ -308,6 +292,19 @@ export function updateTaskCategoryCost(categoryId: TaskCategoryID, costPerTimeUn
|
||||||
return { type: UPDATE_TASK_CATEGORY_COST, categoryId, costPerTimeUnit };
|
return { type: UPDATE_TASK_CATEGORY_COST, categoryId, costPerTimeUnit };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleUpdateTaskCategoryCost(project: Project, action: UpdateTaskCategoryCost): Project {
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
taskCategories: {
|
||||||
|
...project.taskCategories,
|
||||||
|
[action.categoryId]: {
|
||||||
|
...project.taskCategories[action.categoryId],
|
||||||
|
costPerTimeUnit: action.costPerTimeUnit
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const ADD_TASK_CATEGORY = "ADD_TASK_CATEGORY";
|
export const ADD_TASK_CATEGORY = "ADD_TASK_CATEGORY";
|
||||||
|
|
||||||
export interface AddTaskCategory extends Action {
|
export interface AddTaskCategory extends Action {
|
||||||
|
@ -318,6 +315,17 @@ export function addTaskCategory(taskCategory: TaskCategory): AddTaskCategory {
|
||||||
return { type: ADD_TASK_CATEGORY, taskCategory };
|
return { type: ADD_TASK_CATEGORY, taskCategory };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleAddTaskCategory(project: Project, action: AddTaskCategory): Project {
|
||||||
|
const taskCategory = { ...action.taskCategory };
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
taskCategories: {
|
||||||
|
...project.taskCategories,
|
||||||
|
[taskCategory.id]: taskCategory,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface RemoveTaskCategory extends Action {
|
export interface RemoveTaskCategory extends Action {
|
||||||
taskCategoryId: TaskCategoryID
|
taskCategoryId: TaskCategoryID
|
||||||
}
|
}
|
||||||
|
@ -328,28 +336,18 @@ export function removeTaskCategory(taskCategoryId: TaskCategoryID): RemoveTaskCa
|
||||||
return { type: REMOVE_TASK_CATEGORY, taskCategoryId };
|
return { type: REMOVE_TASK_CATEGORY, taskCategoryId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskCategoryRemoved extends Action {
|
export function handleRemoveTaskCategory(project: Project, action: RemoveTaskCategory): Project {
|
||||||
id: number
|
const taskCategories = { ...project.taskCategories };
|
||||||
}
|
delete taskCategories[action.taskCategoryId];
|
||||||
|
|
||||||
export const TASK_CATEGORY_REMOVED = "TASK_CATEGORY_REMOVED";
|
|
||||||
|
|
||||||
export function taskCategoryRemoved(id: number): TaskCategoryRemoved {
|
|
||||||
return { type: TASK_CATEGORY_REMOVED, id };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleTaskCategoryRemoved(project: ProjectState, action: TaskCategoryRemoved): ProjectState {
|
|
||||||
const taskCategories = [...project.taskCategories];
|
|
||||||
const taskCategoryIndex = project.taskCategories.findIndex(tc => tc.id === action.id);
|
|
||||||
if (taskCategoryIndex === -1) return project;
|
|
||||||
taskCategories.splice(taskCategoryIndex, 1);
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
taskCategories
|
taskCategories: {
|
||||||
|
...project.taskCategories,
|
||||||
|
...taskCategories
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ResetProject extends Action {
|
export interface ResetProject extends Action {
|
||||||
project: Project
|
project: Project
|
||||||
}
|
}
|
||||||
|
@ -370,26 +368,9 @@ export function resetProject(project: Project): ResetProject {
|
||||||
return { type: RESET_PROJECT, project: newProject };
|
return { type: RESET_PROJECT, project: newProject };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleResetProject(project: ProjectState, action: ResetProject): ProjectState {
|
export function handleResetProject(project: Project, action: ResetProject): Project {
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
...action.project,
|
...action.project,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackendError extends Action {
|
|
||||||
err: Error
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BACKEND_ERROR = "BACKEND_ERROR";
|
|
||||||
|
|
||||||
export function backendError(err: Error): BackendError {
|
|
||||||
return { type: BACKEND_ERROR, err };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleBackendError(project: ProjectState, action: BackendError): ProjectState {
|
|
||||||
return {
|
|
||||||
...project,
|
|
||||||
lastBackendError: action.err,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { App } from './components/App';
|
import { App } from './components/App';
|
||||||
|
import { client } from './gql/client';
|
||||||
|
|
||||||
import "./style/index.css";
|
import "./style/index.css";
|
||||||
import "bulma/css/bulma.css";
|
import "bulma/css/bulma.css";
|
||||||
|
@ -12,7 +13,11 @@ import '@fortawesome/fontawesome-free/js/regular'
|
||||||
import '@fortawesome/fontawesome-free/js/brands'
|
import '@fortawesome/fontawesome-free/js/brands'
|
||||||
import './resources/favicon.png';
|
import './resources/favicon.png';
|
||||||
|
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<App />,
|
<ApolloProvider client={client}>
|
||||||
|
<App />
|
||||||
|
</ApolloProvider>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
|
|
||||||
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
|
|
||||||
import { Config } from '../config';
|
|
||||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
|
||||||
import { RetryLink } from "@apollo/client/link/retry";
|
|
||||||
import { onError } from "@apollo/client/link/error";
|
|
||||||
import { SubscriptionClient } from "subscriptions-transport-ws";
|
|
||||||
import { User } from '../types/user';
|
|
||||||
|
|
||||||
export function createClient(setLoggedIn: (boolean) => void) {
|
|
||||||
const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, {
|
|
||||||
reconnect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorLink = onError(({ operation }) => {
|
|
||||||
const { response } = operation.getContext();
|
|
||||||
if (response.status === 401) setLoggedIn(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const retryLink = new RetryLink({attempts: {max: 2}}).split(
|
|
||||||
(operation) => operation.operationName === 'subscription',
|
|
||||||
new WebSocketLink(subscriptionClient),
|
|
||||||
new HttpLink({
|
|
||||||
uri: Config.graphQLEndpoint,
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const cache = new InMemoryCache({});
|
|
||||||
|
|
||||||
return new ApolloClient<any>({
|
|
||||||
cache: cache,
|
|
||||||
link: from([
|
|
||||||
errorLink,
|
|
||||||
retryLink
|
|
||||||
]),
|
|
||||||
defaultOptions: {
|
|
||||||
watchQuery: {
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
errorPolicy: 'ignore',
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
errorPolicy: 'all',
|
|
||||||
},
|
|
||||||
mutate: {
|
|
||||||
errorPolicy: 'all',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeArrayByField<T>(fieldName: string) {
|
|
||||||
return (existing: T[] = [], incoming: T[], { readField, mergeObjects }) => {
|
|
||||||
const merged: any[] = existing ? existing.slice(0) : [];
|
|
||||||
|
|
||||||
const objectFieldToIndex: Record<string, number> = Object.create(null);
|
|
||||||
if (existing) {
|
|
||||||
existing.forEach((obj, index) => {
|
|
||||||
objectFieldToIndex[readField(fieldName, obj)] = index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
incoming.forEach(obj => {
|
|
||||||
const field = readField(fieldName, obj);
|
|
||||||
const index = objectFieldToIndex[field];
|
|
||||||
if (typeof index === "number") {
|
|
||||||
merged[index] = mergeObjects(merged[index], obj);
|
|
||||||
} else {
|
|
||||||
objectFieldToIndex[name] = merged.length;
|
|
||||||
merged.push(obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package graph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAssociatedTaskExist = &gqlerror.Error{
|
|
||||||
Message: "",
|
|
||||||
Extensions: map[string]interface{}{
|
|
||||||
"code": "associated-task-exist",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -2,10 +2,48 @@ package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/guesstimate/internal/model"
|
"forge.cadoles.com/Cadoles/guesstimate/internal/model"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleEstimations(ctx context.Context, task *model.Task) (*model.Estimations, error) {
|
func handleEstimations(ctx context.Context, task *model.Task) (*model.Estimations, error) {
|
||||||
return task.Estimations, nil
|
estimations := &model.Estimations{}
|
||||||
|
|
||||||
|
if task.Estimations == nil {
|
||||||
|
return estimations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawOptimistic, exists := task.Estimations[model.EstimationOptimistic]
|
||||||
|
if exists && rawOptimistic != nil {
|
||||||
|
optimistic, err := strconv.ParseFloat(*rawOptimistic, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
estimations.Optimistic = optimistic
|
||||||
|
}
|
||||||
|
|
||||||
|
rawLikely, exists := task.Estimations[model.EstimationLikely]
|
||||||
|
if exists && rawLikely != nil {
|
||||||
|
likely, err := strconv.ParseFloat(*rawLikely, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
estimations.Likely = likely
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPessimistic, exists := task.Estimations[model.EstimationPessimistic]
|
||||||
|
if exists && rawPessimistic != nil {
|
||||||
|
pessimistic, err := strconv.ParseFloat(*rawPessimistic, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
estimations.Pessimistic = pessimistic
|
||||||
|
}
|
||||||
|
|
||||||
|
return estimations, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,32 +18,10 @@ input ProjectTaskEstimationsChanges {
|
||||||
pessimistic: Float
|
pessimistic: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
input ProjectParamsChanges {
|
|
||||||
timeUnit: TimeUnitChanges
|
|
||||||
currency: String
|
|
||||||
roundUpEstimations: Boolean
|
|
||||||
hideFinancialPreviewOnPrint: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
input TimeUnitChanges {
|
|
||||||
label: String
|
|
||||||
acronym: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ProjectTaskCategoryChanges {
|
|
||||||
label: String
|
|
||||||
costPerTimeUnit: Float
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
updateUser(id: ID!, changes: UserChanges!): User!
|
updateUser(id: ID!, changes: UserChanges!): User!
|
||||||
createProject(changes: CreateProjectChanges!): Project!
|
createProject(changes: CreateProjectChanges!): Project!
|
||||||
updateProjectTitle(projectId: ID!, title: String!): Project!
|
updateProjectTitle(projectId: ID!, title: String!): Project!
|
||||||
addProjectTask(projectId: ID!, changes: ProjectTaskChanges!): Task!
|
addProjectTask(projectId: ID!, changes: ProjectTaskChanges): Task!
|
||||||
removeProjectTask(projectId: ID!, taskId: ID!): Boolean!
|
removeProjectTask(projectId: ID!, taskId: ID!): Boolean!
|
||||||
updateProjectTask(projectId: ID!, taskId: ID!, changes: ProjectTaskChanges!): Task!
|
|
||||||
addProjectTaskCategory(projectId: ID!, changes: ProjectTaskCategoryChanges!): TaskCategory!
|
|
||||||
updateProjectTaskCategory(projectId: ID!, taskCategoryId: ID!, changes: ProjectTaskCategoryChanges!): TaskCategory!
|
|
||||||
removeProjectTaskCategory(projectId: ID!, taskCategoryId: ID!): Boolean!
|
|
||||||
updateProjectParams(projectId: ID!, changes: ProjectParamsChanges!): ProjectParams!
|
|
||||||
}
|
}
|
|
@ -22,7 +22,7 @@ func (r *mutationResolver) UpdateProjectTitle(ctx context.Context, projectID int
|
||||||
return handleUpdateProjectTitle(ctx, projectID, title)
|
return handleUpdateProjectTitle(ctx, projectID, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) AddProjectTask(ctx context.Context, projectID int64, changes model.ProjectTaskChanges) (*model.Task, error) {
|
func (r *mutationResolver) AddProjectTask(ctx context.Context, projectID int64, changes *model.ProjectTaskChanges) (*model.Task, error) {
|
||||||
return handleAddProjectTask(ctx, projectID, changes)
|
return handleAddProjectTask(ctx, projectID, changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,26 +30,6 @@ func (r *mutationResolver) RemoveProjectTask(ctx context.Context, projectID int6
|
||||||
return handleRemoveProjectTask(ctx, projectID, taskID)
|
return handleRemoveProjectTask(ctx, projectID, taskID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) UpdateProjectTask(ctx context.Context, projectID int64, taskID int64, changes model.ProjectTaskChanges) (*model.Task, error) {
|
|
||||||
return handleUpdateProjectTask(ctx, projectID, taskID, changes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) AddProjectTaskCategory(ctx context.Context, projectID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) {
|
|
||||||
return handleAddProjectTaskCategory(ctx, projectID, changes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) UpdateProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) {
|
|
||||||
return handleUpdateProjectTaskCategory(ctx, projectID, taskCategoryID, changes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) RemoveProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64) (bool, error) {
|
|
||||||
return handleRemoveProjectTaskCategory(ctx, projectID, taskCategoryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) UpdateProjectParams(ctx context.Context, projectID int64, changes model.ProjectParamsChanges) (*model.ProjectParams, error) {
|
|
||||||
return handleUpdateProjectParams(ctx, projectID, changes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutation returns generated.MutationResolver implementation.
|
// Mutation returns generated.MutationResolver implementation.
|
||||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@ package graph
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/guesstimate/internal/model"
|
"forge.cadoles.com/Cadoles/guesstimate/internal/model"
|
||||||
model1 "forge.cadoles.com/Cadoles/guesstimate/internal/model"
|
model1 "forge.cadoles.com/Cadoles/guesstimate/internal/model"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -66,7 +64,7 @@ func handleUpdateProjectTitle(ctx context.Context, projectID int64, title string
|
||||||
return project, nil
|
return project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAddProjectTask(ctx context.Context, projectID int64, changes model.ProjectTaskChanges) (*model.Task, error) {
|
func handleAddProjectTask(ctx context.Context, projectID int64, changes *model.ProjectTaskChanges) (*model.Task, error) {
|
||||||
db, err := getDB(ctx)
|
db, err := getDB(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
|
@ -96,88 +94,3 @@ func handleRemoveProjectTask(ctx context.Context, projectID int64, taskID int64)
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateProjectTask(ctx context.Context, projectID, taskID int64, changes model.ProjectTaskChanges) (*model.Task, error) {
|
|
||||||
db, err := getDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := model.NewProjectRepository(db)
|
|
||||||
|
|
||||||
task, err := repo.UpdateTask(ctx, projectID, taskID, changes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleUpdateProjectParams(ctx context.Context, projectID int64, changes model.ProjectParamsChanges) (*model.ProjectParams, error) {
|
|
||||||
db, err := getDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := model.NewProjectRepository(db)
|
|
||||||
|
|
||||||
project, err := repo.UpdateParams(ctx, projectID, changes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return project.Params, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAddProjectTaskCategory(ctx context.Context, projectID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) {
|
|
||||||
db, err := getDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := model.NewProjectRepository(db)
|
|
||||||
|
|
||||||
taskCategory, err := repo.AddTaskCategory(ctx, projectID, changes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskCategory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleUpdateProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64, changes model.ProjectTaskCategoryChanges) (*model.TaskCategory, error) {
|
|
||||||
db, err := getDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := model.NewProjectRepository(db)
|
|
||||||
|
|
||||||
taskCategory, err := repo.UpdateTaskCategory(ctx, projectID, taskCategoryID, changes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskCategory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveProjectTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64) (bool, error) {
|
|
||||||
db, err := getDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := model.NewProjectRepository(db)
|
|
||||||
|
|
||||||
if err := repo.RemoveTaskCategory(ctx, projectID, taskCategoryID); err != nil {
|
|
||||||
if errors.Is(err, model.ErrAssociatedTaskExist) {
|
|
||||||
graphql.AddError(ctx, ErrAssociatedTaskExist)
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,7 +18,15 @@ func (r *queryResolver) Projects(ctx context.Context, filter *model1.ProjectsFil
|
||||||
return handleProjects(ctx, filter)
|
return handleProjects(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *taskResolver) Estimations(ctx context.Context, obj *model1.Task) (*model1.Estimations, error) {
|
||||||
|
return handleEstimations(ctx, obj)
|
||||||
|
}
|
||||||
|
|
||||||
// Query returns generated.QueryResolver implementation.
|
// Query returns generated.QueryResolver implementation.
|
||||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
// Task returns generated.TaskResolver implementation.
|
||||||
|
func (r *Resolver) Task() generated.TaskResolver { return &taskResolver{r} }
|
||||||
|
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
|
type taskResolver struct{ *Resolver }
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAssociatedTaskExist = errors.New("associated task exist")
|
|
||||||
)
|
|
|
@ -90,6 +90,12 @@ type Access struct {
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
EstimationPessimistic = "pessimistic"
|
||||||
|
EstimationLikely = "likely"
|
||||||
|
EstimationOptimistic = "optimistic"
|
||||||
|
)
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Base
|
Base
|
||||||
ProjectID int64 `json:"-"`
|
ProjectID int64 `json:"-"`
|
||||||
|
@ -97,13 +103,7 @@ type Task struct {
|
||||||
Label *string `json:"label"`
|
Label *string `json:"label"`
|
||||||
CategoryID int64 `json:"-"`
|
CategoryID int64 `json:"-"`
|
||||||
Category *TaskCategory `json:"category"`
|
Category *TaskCategory `json:"category"`
|
||||||
Estimations *Estimations `gorm:"EMBEDDED;EMBEDDED_PREFIX:estimation_" json:"estimations"`
|
Estimations postgres.Hstore `json:"estimations"`
|
||||||
}
|
|
||||||
|
|
||||||
type Estimations struct {
|
|
||||||
Optimistic float64 `json:"optimistic"`
|
|
||||||
Likely float64 `json:"likely"`
|
|
||||||
Pessimistic float64 `json:"pessimistic"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskCategory struct {
|
type TaskCategory struct {
|
||||||
|
|
|
@ -3,6 +3,9 @@ package model
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm/dialects/postgres"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/guesstimate/internal/orm"
|
"forge.cadoles.com/Cadoles/guesstimate/internal/orm"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
@ -134,14 +137,58 @@ func (r *ProjectRepository) Search(ctx context.Context, filter *ProjectsFilter)
|
||||||
return projects, nil
|
return projects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRepository) AddTask(ctx context.Context, projectID int64, changes ProjectTaskChanges) (*Task, error) {
|
func (r *ProjectRepository) AddTask(ctx context.Context, projectID int64, changes *ProjectTaskChanges) (*Task, error) {
|
||||||
project := &Project{}
|
project := &Project{}
|
||||||
project.ID = projectID
|
project.ID = projectID
|
||||||
task := &Task{}
|
task := &Task{}
|
||||||
|
|
||||||
|
if changes == nil {
|
||||||
|
return nil, errors.Errorf("changes should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := updateTaskWithChanges(tx, task, changes); err != nil {
|
if changes.Label != nil {
|
||||||
return errors.WithStack(err)
|
task.Label = changes.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.CategoryID != nil {
|
||||||
|
taskCategory := &TaskCategory{}
|
||||||
|
taskCategory.ID = *changes.CategoryID
|
||||||
|
task.Category = taskCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Estimations != nil {
|
||||||
|
if task.Estimations == nil {
|
||||||
|
task.Estimations = postgres.Hstore{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Estimations.Pessimistic != nil {
|
||||||
|
pessimistic := strconv.FormatFloat(*changes.Estimations.Pessimistic, 'f', 12, 64)
|
||||||
|
task.Estimations[EstimationPessimistic] = &pessimistic
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Estimations.Likely != nil {
|
||||||
|
likely := strconv.FormatFloat(*changes.Estimations.Likely, 'f', 12, 64)
|
||||||
|
task.Estimations[EstimationLikely] = &likely
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Estimations.Optimistic != nil {
|
||||||
|
optimistic := strconv.FormatFloat(*changes.Estimations.Optimistic, 'f', 12, 64)
|
||||||
|
task.Estimations[EstimationOptimistic] = &optimistic
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.CategoryID != nil {
|
||||||
|
taskCategory := &TaskCategory{}
|
||||||
|
if err := tx.Find(taskCategory, "id = ?", *changes.CategoryID).Error; err != nil {
|
||||||
|
return errors.Wrap(err, "could not find task category")
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Category = taskCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Save(task).Error; err != nil {
|
||||||
|
return errors.Wrap(err, "could not create task")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tx.Model(project).Association("Tasks").Append(task).Error
|
err := tx.Model(project).Association("Tasks").Append(task).Error
|
||||||
|
@ -172,7 +219,7 @@ func (r *ProjectRepository) RemoveTask(ctx context.Context, projectID int64, tas
|
||||||
return errors.Wrap(err, "could not remove task relationship")
|
return errors.Wrap(err, "could not remove task relationship")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Delete(task, "id = ? AND project_id = ?", taskID, projectID).Error
|
err = tx.Delete(task, "id = ?", taskID).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not delete task")
|
return errors.Wrap(err, "could not delete task")
|
||||||
}
|
}
|
||||||
|
@ -186,21 +233,15 @@ func (r *ProjectRepository) RemoveTask(ctx context.Context, projectID int64, tas
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRepository) UpdateTask(ctx context.Context, projectID, taskID int64, changes ProjectTaskChanges) (*Task, error) {
|
func (r *ProjectRepository) UpdateTaskEstimation(ctx context.Context, projectID, taskID int64, estimation string, value float64) (*Task, error) {
|
||||||
task := &Task{}
|
|
||||||
|
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
err := tx.Model(task).
|
task := &Task{}
|
||||||
Preload("Category").
|
if err := tx.First(task, "id = ?", taskID).Error; err != nil {
|
||||||
First(task, "id = ? AND project_id = ?", taskID, projectID).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updateTaskWithChanges(tx, task, changes); err != nil {
|
strValue := strconv.FormatFloat(value, 'f', 12, 64)
|
||||||
return errors.WithStack(err)
|
task.Estimations[estimation] = &strValue
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Save(task).Error; err != nil {
|
if err := tx.Save(task).Error; err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
|
@ -212,218 +253,7 @@ func (r *ProjectRepository) UpdateTask(ctx context.Context, projectID, taskID in
|
||||||
return nil, errors.Wrap(err, "could not update task")
|
return nil, errors.Wrap(err, "could not update task")
|
||||||
}
|
}
|
||||||
|
|
||||||
return task, nil
|
return nil, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectRepository) UpdateParams(ctx context.Context, projectID int64, changes ProjectParamsChanges) (*Project, error) {
|
|
||||||
project := &Project{}
|
|
||||||
project.ID = projectID
|
|
||||||
|
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
err := tx.Model(project).
|
|
||||||
Preload("ACL").
|
|
||||||
Preload("ACL.User").
|
|
||||||
Preload("Tasks").
|
|
||||||
Preload("Tasks.Category").
|
|
||||||
Preload("TaskCategories").
|
|
||||||
First(project, "id = ?", projectID).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if project.Params == nil {
|
|
||||||
project.Params = &ProjectParams{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.Currency != nil {
|
|
||||||
project.Params.Currency = *changes.Currency
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.HideFinancialPreviewOnPrint != nil {
|
|
||||||
project.Params.HideFinancialPreviewOnPrint = *changes.HideFinancialPreviewOnPrint
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.RoundUpEstimations != nil {
|
|
||||||
project.Params.RoundUpEstimations = *changes.RoundUpEstimations
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.TimeUnit != nil {
|
|
||||||
if project.Params.TimeUnit == nil {
|
|
||||||
project.Params.TimeUnit = &TimeUnit{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.TimeUnit.Acronym != nil {
|
|
||||||
project.Params.TimeUnit.Acronym = *changes.TimeUnit.Acronym
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.TimeUnit.Label != nil {
|
|
||||||
project.Params.TimeUnit.Label = *changes.TimeUnit.Label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Save(project).Error; err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "could not update project params")
|
|
||||||
}
|
|
||||||
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectRepository) AddTaskCategory(ctx context.Context, projectID int64, changes ProjectTaskCategoryChanges) (*TaskCategory, error) {
|
|
||||||
project := &Project{}
|
|
||||||
project.ID = projectID
|
|
||||||
taskCategory := &TaskCategory{}
|
|
||||||
|
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := updateTaskCategoryWithChanges(tx, taskCategory, changes); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tx.Model(project).Association("TaskCategories").Append(taskCategory).Error
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not add task category")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "could not add task category")
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskCategory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectRepository) RemoveTaskCategory(ctx context.Context, projectID int64, taskCategoryID int64) error {
|
|
||||||
project := &Project{}
|
|
||||||
project.ID = projectID
|
|
||||||
|
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
taskCategory := &TaskCategory{}
|
|
||||||
taskCategory.ID = taskCategoryID
|
|
||||||
|
|
||||||
var totalAssociatedTasks int
|
|
||||||
if err := tx.Model(&Task{}).Where("category_id = ?", taskCategoryID).Count(&totalAssociatedTasks).Error; err != nil {
|
|
||||||
return errors.Wrap(err, "could not count associated tasks")
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalAssociatedTasks != 0 {
|
|
||||||
return errors.WithStack(ErrAssociatedTaskExist)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tx.Model(project).Association("TaskCategories").Delete(taskCategory).Error
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not remove task category relationship")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Delete(taskCategory, "id = ? AND project_id = ?", taskCategoryID, projectID).Error
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not delete task category")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not remove task category")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectRepository) UpdateTaskCategory(ctx context.Context, projectID, taskCategoryID int64, changes ProjectTaskCategoryChanges) (*TaskCategory, error) {
|
|
||||||
taskCategory := &TaskCategory{}
|
|
||||||
|
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
err := tx.Model(taskCategory).
|
|
||||||
First(taskCategory, "id = ? AND project_id = ?", taskCategoryID, projectID).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := updateTaskCategoryWithChanges(tx, taskCategory, changes); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Save(taskCategory).Error; err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "could not update task category")
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskCategory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateTaskWithChanges(db *gorm.DB, task *Task, changes ProjectTaskChanges) error {
|
|
||||||
if changes.Label != nil {
|
|
||||||
task.Label = changes.Label
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.CategoryID != nil {
|
|
||||||
taskCategory := &TaskCategory{}
|
|
||||||
taskCategory.ID = *changes.CategoryID
|
|
||||||
task.Category = taskCategory
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.Estimations == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.Estimations == nil {
|
|
||||||
task.Estimations = &Estimations{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.Estimations.Pessimistic != nil {
|
|
||||||
task.Estimations.Pessimistic = *changes.Estimations.Pessimistic
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.Estimations.Likely != nil {
|
|
||||||
task.Estimations.Likely = *changes.Estimations.Likely
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.Estimations.Optimistic != nil {
|
|
||||||
task.Estimations.Optimistic = *changes.Estimations.Optimistic
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.Estimations.Likely < task.Estimations.Optimistic {
|
|
||||||
task.Estimations.Likely = task.Estimations.Optimistic
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.Estimations.Pessimistic < task.Estimations.Likely {
|
|
||||||
task.Estimations.Pessimistic = task.Estimations.Likely
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.CategoryID != nil {
|
|
||||||
taskCategory := &TaskCategory{}
|
|
||||||
if err := db.Find(taskCategory, "id = ?", *changes.CategoryID).Error; err != nil {
|
|
||||||
return errors.Wrap(err, "could not find task category")
|
|
||||||
}
|
|
||||||
|
|
||||||
task.Category = taskCategory
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateTaskCategoryWithChanges(db *gorm.DB, taskCategory *TaskCategory, changes ProjectTaskCategoryChanges) error {
|
|
||||||
if changes.Label != nil {
|
|
||||||
taskCategory.Label = *changes.Label
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes.CostPerTimeUnit != nil {
|
|
||||||
taskCategory.CostPerTimeUnit = *changes.CostPerTimeUnit
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProjectRepository(db *gorm.DB) *ProjectRepository {
|
func NewProjectRepository(db *gorm.DB) *ProjectRepository {
|
||||||
|
|
Loading…
Reference in New Issue