From 7fc1a7f3af4931100bea1697f65133df24f47a28 Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 11 Sep 2020 09:19:18 +0200 Subject: [PATCH] feat(ui+backend): base of data persistence --- Makefile | 2 +- .../components/DashboardPage/ItemPanel.tsx | 4 +- .../DashboardPage/ProjectModelPanel.tsx | 3 +- .../components/DashboardPage/ProjectPanel.tsx | 10 +- .../components/ProjectPage/EstimationTab.tsx | 14 +- ...ancielPreview.tsx => FinancialPreview.tsx} | 2 +- .../components/ProjectPage/ProjectPage.tsx | 31 ++- .../ProjectPage/RepartitionPreview.tsx | 2 +- .../ProjectPage/TaskCategorieTable.tsx | 2 +- .../src/components/ProjectPage/TasksTable.tsx | 41 +-- client/src/gql/fragments/project.ts | 34 +++ client/src/gql/mutations/project.tsx | 69 +++++ client/src/gql/queries/helper.ts | 4 +- client/src/gql/queries/project.ts | 49 ++++ client/src/hooks/useProjectReducer.sagas.ts | 95 +++++++ client/src/hooks/useProjectReducer.ts | 222 ++++++++++----- client/src/hooks/useReducerAndSaga.ts | 24 ++ client/src/types/params.ts | 38 +-- client/src/types/project.ts | 80 +++--- client/src/types/task.ts | 17 +- client/src/util/uuid.ts | 35 +++ cmd/server/migration.go | 43 ++- internal/config/config.go | 2 +- internal/gqlgen.yml | 3 - internal/graph/estimation_handler.go | 49 ++++ internal/graph/mutation.graphql | 20 ++ internal/graph/mutation.resolvers.go | 18 +- internal/graph/project_handler.go | 96 +++++++ internal/graph/query.graphql | 54 ++++ internal/graph/query.resolvers.go | 12 + internal/graph/user_handler.go | 2 +- internal/model/base.go | 21 ++ internal/model/project.go | 123 +++++++++ internal/model/project_repository.go | 261 ++++++++++++++++++ internal/model/user.go | 3 +- .../postgres/initdb.d/init-databases.sh | 2 + modd.conf | 6 +- 37 files changed, 1298 insertions(+), 195 deletions(-) rename client/src/components/ProjectPage/{FinancielPreview.tsx => FinancialPreview.tsx} (97%) create mode 100644 client/src/gql/fragments/project.ts create mode 100644 client/src/gql/mutations/project.tsx create mode 100644 client/src/gql/queries/project.ts create mode 100644 client/src/hooks/useProjectReducer.sagas.ts create mode 100644 client/src/hooks/useReducerAndSaga.ts create mode 100644 client/src/util/uuid.ts create mode 100644 internal/graph/estimation_handler.go create mode 100644 internal/graph/project_handler.go create mode 100644 internal/model/base.go create mode 100644 internal/model/project.go create mode 100644 internal/model/project_repository.go diff --git a/Makefile b/Makefile index 3dea2b9..99a9760 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ down: docker-compose down -v --remove-orphans db-shell: - docker-compose exec postgres psql -Udaddy + docker-compose exec postgres psql -Uguesstimate migrate: build-server ( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml -migrate $(MIGRATE) ) diff --git a/client/src/components/DashboardPage/ItemPanel.tsx b/client/src/components/DashboardPage/ItemPanel.tsx index 4335c68..f4c0598 100644 --- a/client/src/components/DashboardPage/ItemPanel.tsx +++ b/client/src/components/DashboardPage/ItemPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { WithLoader } from "../WithLoader"; export interface Item { - id: string + id: string|number [propName: string]: any; } @@ -21,7 +21,7 @@ export interface ItemPanelProps { isLoading?: boolean items: Item[] tabs?: TabDefinition[], - itemKey: (item: Item, index: number) => string + itemKey: (item: Item, index: number) => string|number itemLabel: (item: Item, index: number) => string itemUrl: (item: Item, index: number) => string } diff --git a/client/src/components/DashboardPage/ProjectModelPanel.tsx b/client/src/components/DashboardPage/ProjectModelPanel.tsx index 8bd9498..36d3048 100644 --- a/client/src/components/DashboardPage/ProjectModelPanel.tsx +++ b/client/src/components/DashboardPage/ProjectModelPanel.tsx @@ -1,5 +1,6 @@ import React, { FunctionComponent } from "react"; import { ItemPanel } from "./ItemPanel"; +import { useProjects } from "../../gql/queries/project"; export interface ProjectModelPanelProps { @@ -13,7 +14,7 @@ export const ProjectModelPanel: FunctionComponent = () = newItemUrl="/models/new" items={[]} itemKey={(item) => { return item.id }} - itemLabel={(item) => { return item.id }} + itemLabel={(item) => { return `${item.id}` }} itemUrl={(item) => { return `/models/${item.id}` }} /> ); diff --git a/client/src/components/DashboardPage/ProjectPanel.tsx b/client/src/components/DashboardPage/ProjectPanel.tsx index df5f69e..d1cd01c 100644 --- a/client/src/components/DashboardPage/ProjectPanel.tsx +++ b/client/src/components/DashboardPage/ProjectPanel.tsx @@ -1,19 +1,25 @@ import React, { FunctionComponent } from "react"; import { ItemPanel } from "./ItemPanel"; +import { useProjects } from "../../gql/queries/project"; export interface ProjectPanelProps { } export const ProjectPanel: FunctionComponent = () => { + const { projects } = useProjects({ + fetchPolicy: 'cache-and-network', + }); + return ( { return item.id }} - itemLabel={(item) => { return item.id }} + itemLabel={(item) => { return `#${item.id} - ${item.title ? item.title : 'Projet sans nom'}` }} itemUrl={(item) => { return `/projects/${item.id}` }} /> ); diff --git a/client/src/components/ProjectPage/EstimationTab.tsx b/client/src/components/ProjectPage/EstimationTab.tsx index d169067..2317410 100644 --- a/client/src/components/ProjectPage/EstimationTab.tsx +++ b/client/src/components/ProjectPage/EstimationTab.tsx @@ -2,7 +2,7 @@ import React, { FunctionComponent, Fragment } from "react"; import { Project } from "../../types/project"; import TaskTable from "./TasksTable"; import { TimePreview } from "./TimePreview"; -import FinancialPreview from "./FinancielPreview"; +import FinancialPreview from "./FinancialPreview"; import { addTask, updateTaskEstimation, removeTask, updateTaskLabel, ProjectReducerActions } from "../../hooks/useProjectReducer"; import { Task, TaskID, EstimationConfidence } from "../../types/task"; import RepartitionPreview from "./RepartitionPreview"; @@ -18,16 +18,16 @@ const EstimationTab: FunctionComponent = ({ project, dispatc dispatch(addTask(task)); }; - const onTaskRemove = (taskId: TaskID) => { - dispatch(removeTask(taskId)); + const onTaskRemove = (id: number) => { + dispatch(removeTask(id)); } - const onTaskLabelUpdate = (taskId: TaskID, label: string) => { - dispatch(updateTaskLabel(taskId, label)); + const onTaskLabelUpdate = (id: number, label: string) => { + dispatch(updateTaskLabel(id, label)); } - const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => { - dispatch(updateTaskEstimation(taskId, confidence, value)); + const onEstimationChange = (id: number, confidence: EstimationConfidence, value: number) => { + dispatch(updateTaskEstimation(id, confidence, value)); }; return ( diff --git a/client/src/components/ProjectPage/FinancielPreview.tsx b/client/src/components/ProjectPage/FinancialPreview.tsx similarity index 97% rename from client/src/components/ProjectPage/FinancielPreview.tsx rename to client/src/components/ProjectPage/FinancialPreview.tsx index 5300733..bf4e4c2 100644 --- a/client/src/components/ProjectPage/FinancielPreview.tsx +++ b/client/src/components/ProjectPage/FinancialPreview.tsx @@ -64,7 +64,7 @@ export const CostDetails:FunctionComponent = ({ project, cost, { Object.keys(cost.details).map(taskCategoryId => { - const taskCategory = project.params.taskCategories[taskCategoryId]; + const taskCategory = project.taskCategories[parseInt(taskCategoryId)]; const details = cost.details[taskCategoryId]; return ( diff --git a/client/src/components/ProjectPage/ProjectPage.tsx b/client/src/components/ProjectPage/ProjectPage.tsx index f3a5f8a..3379eb9 100644 --- a/client/src/components/ProjectPage/ProjectPage.tsx +++ b/client/src/components/ProjectPage/ProjectPage.tsx @@ -1,14 +1,15 @@ import React, { FunctionComponent, useEffect } from "react"; import style from "./style.module.css"; import { newProject, Project } from "../../types/project"; -import { useProjectReducer, updateProjectLabel } from "../../hooks/useProjectReducer"; +import { useProjectReducer, updateProjectTitle, resetProject } from "../../hooks/useProjectReducer"; import EditableText from "../EditableText/EditableText"; import Tabs from "../../components/Tabs/Tabs"; import EstimationTab from "./EstimationTab"; import ParamsTab from "./ParamsTab"; import ExportTab from "./ExportTab"; -import { useParams } from "react-router"; +import { useParams, useHistory } from "react-router"; import { Page } from "../Page"; +import { useProjects } from "../../gql/queries/project"; export interface ProjectProps { projectId: string @@ -16,10 +17,24 @@ export interface ProjectProps { export const ProjectPage: FunctionComponent = () => { const { id } = useParams(); - const [ project, dispatch ] = useProjectReducer(newProject()); - - const onProjectLabelChange = (projectLabel: string) => { - dispatch(updateProjectLabel(projectLabel)); + const isNew = id === 'new'; + const projectId = isNew ? undefined : parseInt(id); + const { projects } = useProjects({ variables: { filter: {ids: projectId !== undefined ? [projectId] : undefined} }}); + const [ project, dispatch ] = useProjectReducer(newProject(projectId)); + const history = useHistory(); + + useEffect(() => { + history.push(`/projects/${project.id}`); + }, [project.id]) + + useEffect(() => { + if (isNew || projects.length === 0) return; + dispatch(resetProject(projects[0])); + }, [projects.length]); + + const onProjectTitleChange = (projectTitle: string) => { + if (project.title === projectTitle) return; + dispatch(updateProjectTitle(projectTitle)); }; return ( @@ -30,8 +45,8 @@ export const ProjectPage: FunctionComponent = () => { (

{value}

)} - onChange={onProjectLabelChange} - value={project.label ? project.label : "Projet sans nom"} + onChange={onProjectTitleChange} + value={project.title ? project.title : "Projet sans nom"} />
= ({ projec { - Object.values(project.params.taskCategories).map(tc => { + Object.values(project.taskCategories).map(tc => { let percent = (repartition[tc.id] * 100).toFixed(0); return ( diff --git a/client/src/components/ProjectPage/TaskCategorieTable.tsx b/client/src/components/ProjectPage/TaskCategorieTable.tsx index 9f10b65..9eaf813 100644 --- a/client/src/components/ProjectPage/TaskCategorieTable.tsx +++ b/client/src/components/ProjectPage/TaskCategorieTable.tsx @@ -55,7 +55,7 @@ const TaskCategoriesTable: FunctionComponent = ({ proj { - Object.values(project.params.taskCategories).map(tc => { + Object.values(project.taskCategories).map(tc => { return ( diff --git a/client/src/components/ProjectPage/TasksTable.tsx b/client/src/components/ProjectPage/TasksTable.tsx index 708eb1b..005e942 100644 --- a/client/src/components/ProjectPage/TasksTable.tsx +++ b/client/src/components/ProjectPage/TasksTable.tsx @@ -9,22 +9,25 @@ import ProjectTimeUnit from "../ProjectTimeUnit"; export interface TaskTableProps { project: Project onTaskAdd: (task: Task) => void - onTaskRemove: (taskId: TaskID) => void - onEstimationChange: (taskId: TaskID, confidence: EstimationConfidence, value: number) => void - onTaskLabelUpdate: (taskId: TaskID, label: string) => void + onTaskRemove: (taskId: number) => void + onEstimationChange: (taskId: number, confidence: EstimationConfidence, value: number) => void + onTaskLabelUpdate: (taskId: number, label: string) => void } export type EstimationTotals = { [confidence in EstimationConfidence]: number } const TaskTable: FunctionComponent = ({ project, onTaskAdd, onEstimationChange, onTaskRemove, onTaskLabelUpdate }) => { - - const defaultTaskCategory = Object.keys(project.params.taskCategories)[0]; - const [ task, setTask ] = useState(newTask("", defaultTaskCategory)); + const [ task, setTask ] = useState(newTask("", null)); const [ totals, setTotals ] = useState({ [EstimationConfidence.Optimistic]: 0, [EstimationConfidence.Likely]: 0, [EstimationConfidence.Pessimistic]: 0, } as EstimationTotals); + + useEffect(() => { + if (project.taskCategories.length === 0) return; + setTask({...task, category: project.taskCategories[0]}); + }, [project.taskCategories]); const isPrint = usePrintMediaQuery(); @@ -33,7 +36,7 @@ const TaskTable: FunctionComponent = ({ project, onTaskAdd, onEs let likely = 0; let pessimistic = 0; - Object.values(project.tasks).forEach(t => { + project.tasks.forEach(t => { optimistic += t.estimations.optimistic; likely += t.estimations.likely; pessimistic += t.estimations.pessimistic; @@ -49,26 +52,28 @@ const TaskTable: FunctionComponent = ({ project, onTaskAdd, onEs const onNewTaskCategoryChange = (evt: ChangeEvent) => { const value = (evt.currentTarget as HTMLInputElement).value; - setTask({...task, category: value}); + const taskCategoryId = parseInt(value); + const category = project.taskCategories.find(tc => tc.id === taskCategoryId); + setTask({...task, category }); }; - const onTaskLabelChange = (taskId: TaskID, value: string) => { + const onTaskLabelChange = (taskId: number, value: string) => { onTaskLabelUpdate(taskId, value); }; const onAddTaskClick = (evt: MouseEvent) => { onTaskAdd(task); - setTask(newTask("", defaultTaskCategory)); + setTask(newTask("", project.taskCategories[0])); }; - const onTaskRemoveClick = (taskId: TaskID, evt: MouseEvent) => { + const onTaskRemoveClick = (taskId: number, evt: MouseEvent) => { onTaskRemove(taskId); }; - const withEstimationChange = (confidence: EstimationConfidence, taskID: TaskID, evt: ChangeEvent) => { + const withEstimationChange = (confidence: EstimationConfidence, taskId: number, evt: ChangeEvent) => { const textValue = (evt.currentTarget as HTMLInputElement).value; const value = parseFloat(textValue); - onEstimationChange(taskID, confidence, value); + onEstimationChange(taskId, confidence, value); }; const onOptimisticChange = withEstimationChange.bind(null, EstimationConfidence.Optimistic); @@ -93,11 +98,11 @@ const TaskTable: FunctionComponent = ({ project, onTaskAdd, onEs { - Object.values(project.tasks).map(t => { - const category = project.params.taskCategories[t.category]; + project.tasks.map((t,i) => { + const category = project.taskCategories.find(tc => tc.id === t.category.id); const categoryLabel = category ? category.label : '???'; return ( - +