feat(ui+backend): task update ok
This commit is contained in:
@ -4,28 +4,57 @@ import { HomePage } from './HomePage/HomePage';
|
||||
import { ProfilePage } from './ProfilePage/ProfilePage';
|
||||
import { DashboardPage } from './DashboardPage/DashboardPage';
|
||||
import { PrivateRoute } from './PrivateRoute';
|
||||
import { useLoggedIn, LoggedInContext } from '../hooks/useLoggedIn';
|
||||
import { useLoggedIn, LoggedInContext, getSavedLoggedIn, saveLoggedIn } from '../hooks/useLoggedIn';
|
||||
import { useUserProfile } from '../gql/queries/user';
|
||||
import { ProjectPage } from './ProjectPage/ProjectPage';
|
||||
import { createClient } from '../util/apollo';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
|
||||
export interface AppProps {
|
||||
|
||||
}
|
||||
|
||||
export const App: FunctionComponent<AppProps> = () => {
|
||||
const { user } = useUserProfile();
|
||||
const [ loggedIn, setLoggedIn ] = useState(getSavedLoggedIn());
|
||||
|
||||
const client = createClient((loggedIn) => {
|
||||
setLoggedIn(loggedIn);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
saveLoggedIn(loggedIn);
|
||||
}, [loggedIn]);
|
||||
|
||||
return (
|
||||
<LoggedInContext.Provider value={user.id !== ''}>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<PrivateRoute path="/profile" exact component={ProfilePage} />
|
||||
<PrivateRoute path="/dashboard" exact component={DashboardPage} />
|
||||
<PrivateRoute path="/projects/:id" exact component={ProjectPage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</LoggedInContext.Provider>
|
||||
<ApolloProvider client={client}>
|
||||
<LoggedInContext.Provider value={loggedIn}>
|
||||
<UserSessionCheck setLoggedIn={setLoggedIn} />
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<PrivateRoute path="/profile" exact component={ProfilePage} />
|
||||
<PrivateRoute path="/dashboard" exact component={DashboardPage} />
|
||||
<PrivateRoute path="/projects/:id" exact component={ProjectPage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</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,26 +17,23 @@ const EditableText: FunctionComponent<EditableTextProps> = ({ onChange, value, r
|
||||
const [ internalValue, setInternalValue ] = useState(value);
|
||||
const [ editMode, setEditMode ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (internalValue === value) return;
|
||||
if (onChange) onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value])
|
||||
|
||||
const onEditIconClick = () => {
|
||||
setEditMode(true);
|
||||
setEditMode(true);
|
||||
};
|
||||
|
||||
const onValidateButtonClick = () => {
|
||||
setEditMode(false);
|
||||
setEditMode(false);
|
||||
if (internalValue === value) return;
|
||||
if (onChange) onChange(internalValue);
|
||||
}
|
||||
|
||||
const onValueChange = (evt: ChangeEvent) => {
|
||||
const currentTarget = evt.currentTarget as HTMLInputElement;
|
||||
setInternalValue(currentTarget.value);
|
||||
const currentTarget = evt.currentTarget as HTMLInputElement;
|
||||
setInternalValue(currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,21 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FRAGMENT_FULL_TASK = gql`
|
||||
fragment FullTask on Task {
|
||||
id
|
||||
label
|
||||
category {
|
||||
id
|
||||
label
|
||||
}
|
||||
estimations {
|
||||
optimistic
|
||||
likely
|
||||
pessimistic
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FRAGMENT_FULL_PROJECT = gql`
|
||||
fragment FullProject on Project {
|
||||
id
|
||||
@ -10,17 +26,7 @@ fragment FullProject on Project {
|
||||
costPerTimeUnit
|
||||
}
|
||||
tasks {
|
||||
id
|
||||
label
|
||||
category {
|
||||
id
|
||||
label
|
||||
}
|
||||
estimations {
|
||||
optimistic
|
||||
likely
|
||||
pessimistic
|
||||
}
|
||||
...FullTask
|
||||
}
|
||||
params {
|
||||
timeUnit {
|
||||
@ -31,4 +37,5 @@ fragment FullProject on Project {
|
||||
hideFinancialPreviewOnPrint
|
||||
}
|
||||
}
|
||||
${FRAGMENT_FULL_TASK}
|
||||
`
|
@ -1,5 +1,5 @@
|
||||
import { gql, useMutation, PureQueryOptions } from '@apollo/client';
|
||||
import { FRAGMENT_FULL_PROJECT } from '../fragments/project';
|
||||
import { FRAGMENT_FULL_PROJECT, FRAGMENT_FULL_TASK } from '../fragments/project';
|
||||
import { QUERY_PROJECTS } from '../queries/project';
|
||||
|
||||
export const MUTATION_CREATE_PROJECT = gql`
|
||||
@ -37,9 +37,10 @@ export function useUpdateProjectTitleMutation() {
|
||||
export const MUTATION_ADD_PROJECT_TASK = gql`
|
||||
mutation addProjectTask($projectId: ID!, $changes: ProjectTaskChanges!) {
|
||||
addProjectTask(projectId: $projectId, changes: $changes) {
|
||||
id
|
||||
...FullTask
|
||||
}
|
||||
}
|
||||
${FRAGMENT_FULL_TASK}
|
||||
`;
|
||||
|
||||
export function useAddProjectTaskMutation() {
|
||||
@ -49,9 +50,10 @@ export function useAddProjectTaskMutation() {
|
||||
export const MUTATION_UPDATE_PROJECT_TASK = gql`
|
||||
mutation updateProjectTask($projectId: ID!, $taskId: ID!, $changes: ProjectTaskChanges!) {
|
||||
updateProjectTask(projectId: $projectId, taskId: $taskId, changes: $changes) {
|
||||
id
|
||||
...FullTask
|
||||
}
|
||||
}
|
||||
${FRAGMENT_FULL_TASK}
|
||||
`;
|
||||
|
||||
export function useUpdateProjectTaskMutation() {
|
||||
|
@ -1,41 +1,15 @@
|
||||
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||
import { User } from '../../types/user';
|
||||
import { useGraphQLData } from './helper';
|
||||
import { Project } from '../../types/project';
|
||||
import { FRAGMENT_FULL_PROJECT } from '../fragments/project';
|
||||
|
||||
export const QUERY_PROJECTS = gql`
|
||||
query projects($filter: ProjectsFilter) {
|
||||
projects(filter: $filter) {
|
||||
id
|
||||
title
|
||||
taskCategories {
|
||||
id
|
||||
label
|
||||
costPerTimeUnit
|
||||
}
|
||||
tasks {
|
||||
id
|
||||
label
|
||||
category {
|
||||
id
|
||||
label
|
||||
}
|
||||
estimations {
|
||||
optimistic
|
||||
likely
|
||||
pessimistic
|
||||
}
|
||||
}
|
||||
params {
|
||||
timeUnit {
|
||||
label
|
||||
acronym
|
||||
}
|
||||
currency
|
||||
hideFinancialPreviewOnPrint
|
||||
}
|
||||
...FullProject
|
||||
}
|
||||
}`;
|
||||
}
|
||||
${FRAGMENT_FULL_PROJECT}`;
|
||||
|
||||
export function useProjectsQuery() {
|
||||
return useQuery(QUERY_PROJECTS);
|
||||
|
@ -1,7 +1,22 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
|
||||
export const LoggedInContext = React.createContext(false);
|
||||
const LOGGED_IN_KEY = 'loggedIn';
|
||||
|
||||
export const LoggedInContext = React.createContext(getSavedLoggedIn());
|
||||
|
||||
export const useLoggedIn = () => {
|
||||
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,21 +1,21 @@
|
||||
import { all, select, takeLatest, put, delay } from "redux-saga/effects";
|
||||
import { client } from '../gql/client';
|
||||
import { MUTATION_CREATE_PROJECT, MUTATION_UPDATE_PROJECT_TITLE, MUTATION_ADD_PROJECT_TASK, MUTATION_REMOVE_PROJECT_TASK } from "../gql/mutations/project";
|
||||
import { UPDATE_PROJECT_TITLE, resetProject, ADD_TASK, taskSaved, AddTask, taskAdded, taskRemoved, RemoveTask, REMOVE_TASK } from "./useProjectReducer";
|
||||
import { MUTATION_CREATE_PROJECT, MUTATION_UPDATE_PROJECT_TITLE, MUTATION_ADD_PROJECT_TASK, MUTATION_REMOVE_PROJECT_TASK, MUTATION_UPDATE_PROJECT_TASK } from "../gql/mutations/project";
|
||||
import { UPDATE_PROJECT_TITLE, resetProject, ADD_TASK, taskSaved, AddTask, taskRemoved, RemoveTask, REMOVE_TASK, UPDATE_TASK_ESTIMATION, updateTaskEstimation, UpdateTaskEstimation, UpdateTaskLabel, UPDATE_TASK_LABEL } from "./useProjectReducer";
|
||||
import { Project } from "../types/project";
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
createProjectSaga(),
|
||||
takeLatest(UPDATE_PROJECT_TITLE, updateProjectTitleSaga),
|
||||
takeLatest(UPDATE_TASK_ESTIMATION, updateTaskEstimationSaga),
|
||||
takeLatest(UPDATE_TASK_LABEL, updateTaskLabelSaga),
|
||||
takeLatest(ADD_TASK, addTaskSaga),
|
||||
takeLatest(REMOVE_TASK, removeTaskSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* updateProjectTitleSaga() {
|
||||
yield delay(500);
|
||||
|
||||
let project = yield select();
|
||||
|
||||
if (project.id === undefined) {
|
||||
@ -73,7 +73,7 @@ export function* addTaskSaga({ task }: AddTask) {
|
||||
}
|
||||
});
|
||||
|
||||
yield put(taskAdded({ ...task, ...data.addProjectTask }));
|
||||
yield put(taskSaved({ ...task, ...data.addProjectTask }));
|
||||
}
|
||||
|
||||
export function* removeTaskSaga({ id }: RemoveTask) {
|
||||
@ -92,4 +92,48 @@ export function* removeTaskSaga({ id }: RemoveTask) {
|
||||
});
|
||||
|
||||
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 }));
|
||||
}
|
@ -28,12 +28,9 @@ export function useProjectReducer(project: Project) {
|
||||
}
|
||||
|
||||
export function projectReducer(project: Project, action: ProjectReducerActions): Project {
|
||||
console.log(action);
|
||||
console.log(action.type, action);
|
||||
|
||||
switch(action.type) {
|
||||
case TASK_ADDED:
|
||||
return handleTaskAdded(project, action as TaskAdded);
|
||||
|
||||
case TASK_SAVED:
|
||||
return handleTaskSaved(project, action as TaskSaved);
|
||||
|
||||
@ -86,23 +83,6 @@ export interface TaskAdded extends Action {
|
||||
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 interface TaskSaved extends Action {
|
||||
@ -115,9 +95,12 @@ export function taskSaved(task: Task): TaskSaved {
|
||||
|
||||
export function handleTaskSaved(project: Project, action: TaskSaved): Project {
|
||||
const taskIndex = project.tasks.findIndex(t => t.id === action.task.id);
|
||||
if (taskIndex === -1) return project;
|
||||
const tasks = [ ...project.tasks ];
|
||||
tasks[taskIndex] = { ...tasks[taskIndex], ...action.task };
|
||||
if (taskIndex === -1) {
|
||||
tasks.push({ ...action.task });
|
||||
} else {
|
||||
tasks[taskIndex] = { ...tasks[taskIndex], ...action.task };
|
||||
}
|
||||
return {
|
||||
...project,
|
||||
tasks
|
||||
@ -177,14 +160,6 @@ export function handleUpdateTaskEstimation(project: Project, action: UpdateTaskE
|
||||
...project.tasks[taskIndex].estimations,
|
||||
[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 };
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
import { client } from './gql/client';
|
||||
|
||||
import "./style/index.css";
|
||||
import "bulma/css/bulma.css";
|
||||
@ -13,11 +12,7 @@ import '@fortawesome/fontawesome-free/js/regular'
|
||||
import '@fortawesome/fontawesome-free/js/brands'
|
||||
import './resources/favicon.png';
|
||||
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
|
||||
ReactDOM.render(
|
||||
<ApolloProvider client={client}>
|
||||
<App />
|
||||
</ApolloProvider>,
|
||||
<App />,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
77
client/src/util/apollo.ts
Normal file
77
client/src/util/apollo.ts
Normal file
@ -0,0 +1,77 @@
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user