From 557d097b9355652315dbf0853955f08f66dec00b Mon Sep 17 00:00:00 2001 From: JeanM65 Date: Tue, 19 Mar 2024 15:48:44 +0800 Subject: [PATCH 1/3] save to DB and update store --- src/redux/actions/estimate.action.ts | 9 ++++--- src/redux/reducers/estimate.reducer.ts | 36 ++++++++++++++++++++++++++ src/redux/store.ts | 2 ++ src/types/estimate.type.ts | 14 ++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/redux/reducers/estimate.reducer.ts diff --git a/src/redux/actions/estimate.action.ts b/src/redux/actions/estimate.action.ts index 8ace7e20..8e734922 100644 --- a/src/redux/actions/estimate.action.ts +++ b/src/redux/actions/estimate.action.ts @@ -1,10 +1,11 @@ -import Parse from "parse"; +import Parse, { Attributes } from "parse"; import { PATH_NAMES } from "@/utils/pathnames"; import { setValues } from "@/utils/parse.utils"; import { actionWithLoader } from "@/utils/app.utils"; import { AppDispatch } from "../store"; import { setMessageSlice } from "../reducers/app.reducer"; import i18n from "@/config/i18n"; +import { addEstimateToEstimateSlice } from "../reducers/estimate.reducer"; const Estimate = Parse.Object.extend("Estimate"); @@ -14,13 +15,15 @@ export const createEstimate = (values: any): any => { return actionWithLoader(async (dispatch: AppDispatch): Promise => { const estimate = new Estimate() - setValues(estimate, values, ESTIMATE_PROPERTIES); + // setValues(estimate, values, ESTIMATE_PROPERTIES); estimate.set("url", values.url) const savedEstimate = await estimate.save(); + console.log('saved Estimate:-----------:', savedEstimate); dispatch(setMessageSlice(i18n.t('common:estimateCreatedSuccessfully'))); - return savedEstimate; + dispatch(addEstimateToEstimateSlice((savedEstimate as Attributes).toJSON())); + // return savedEstimate; }); }; diff --git a/src/redux/reducers/estimate.reducer.ts b/src/redux/reducers/estimate.reducer.ts new file mode 100644 index 00000000..666d37e2 --- /dev/null +++ b/src/redux/reducers/estimate.reducer.ts @@ -0,0 +1,36 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IArticle, IArticleState } from '@/types/article.types'; +import { IEstimate, IEstimateState } from '@/types/estimate.type'; + +const initialState: IEstimateState = { + loading: false, + estimate: null, + estimates: [], +}; + +export const estimate = createSlice({ + name: 'estimate', + initialState, + reducers: { + addEstimateToEstimateSlice: (state: IEstimateState, action: PayloadAction) => { + state.estimates = [...state.estimates, action.payload]; + }, + }, +}); + +export const { + addEstimateToEstimateSlice +} = estimate.actions; + +// ---------------------------------------------- // +// ------------------ SELECTOR ------------------ // +// ---------------------------------------------- // +// NOTE: do not use RootState as state type to avoid import circular dependencies (from store.ts) +// we can not commit to git if there is circular dependencies +export const getArticleSelector = (state: Record): IArticleState => state.article; +export const getArticleArticleSelector = (state: Record): IArticle => state.article.article; +export const getArticleLoadingSelector = (state: Record): boolean => state.article.loading; +export const getArticleArticlesSelector = (state: Record): IArticle[] => state.article.articles; +export const getArticleCountSelector = (state: Record): number => state.article.count; + +export default estimate.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 5c6353ed..1dfbbb5e 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -7,12 +7,14 @@ import appReducer from './reducers/app.reducer'; import roleReducer from './reducers/role.reducer'; import settingsReducer from './reducers/settings.reducer'; import userReducer from './reducers/user.reducer'; +import estimateReducer from './reducers/estimate.reducer'; import articleReducer from './reducers/article.reducer'; const reducers = { app: appReducer, user: userReducer, article: articleReducer, + estimate: estimateReducer, settings: settingsReducer, role: roleReducer, }; diff --git a/src/types/estimate.type.ts b/src/types/estimate.type.ts index 5c227d29..d73bbb77 100644 --- a/src/types/estimate.type.ts +++ b/src/types/estimate.type.ts @@ -1,4 +1,18 @@ import { z } from "zod"; +import { Attributes } from "parse"; import { estimateSchema } from "@/validations/estimate.validation"; export type EstimateInput = z.infer; + +export interface IEstimate extends Attributes { + id: string; + url: string; + updatedAt?: string; + createdAt?: string; +} + +export interface IEstimateState { + loading: boolean; + estimate: IEstimate | null; + estimates: IEstimate[]; +} \ No newline at end of file From 419c2ed6004e6d37144d77ccb393c06c023b7a37 Mon Sep 17 00:00:00 2001 From: JeanM65 Date: Tue, 19 Mar 2024 21:23:35 +0800 Subject: [PATCH 2/3] add estimate container --- src/pages/estimates/Estimate.tsx | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/pages/estimates/Estimate.tsx diff --git a/src/pages/estimates/Estimate.tsx b/src/pages/estimates/Estimate.tsx new file mode 100644 index 00000000..c2b1b241 --- /dev/null +++ b/src/pages/estimates/Estimate.tsx @@ -0,0 +1,51 @@ +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import Head from '@/components/Head'; +import Dialog from '@/components/Dialog'; +import AddFab from '@/components/AddFab'; +import EstimateForm from './EstimateForm'; +import { EstimateInput } from '@/types/estimate.type'; +import { SubmitHandler } from 'react-hook-form'; +import { useDispatch } from 'react-redux'; +import { createEstimate } from '@/redux/actions/estimate.action'; + +const ESTIMATE_FORM_ID = 'estimate-form-id'; + +const Estimate = () => { + const { t } = useTranslation(); + + const dispatch = useDispatch(); + + + const [openFormDialog, setOpenFormDialog] = useState(false); + + const onSubmitHandler: SubmitHandler = values => { + dispatch(createEstimate(values)); + }; + + const toggleDialog = () => setOpenFormDialog(!openFormDialog); + + return ( +
+ +

Estimates

+ + + + +
+ ); +} + +export default Estimate; From 68cac1930daa7ada35f3962ceeea8ce5f93df223 Mon Sep 17 00:00:00 2001 From: JeanM65 Date: Wed, 20 Mar 2024 18:11:46 +0800 Subject: [PATCH 3/3] display estimate list --- src/pages/articles/Articles.tsx | 15 ++-- src/pages/estimates/Estimates.tsx | 109 ++++++++++++++++------- src/pages/users/Users.tsx | 2 +- src/redux/actions/estimate.action.ts | 27 +++++- src/redux/reducers/estimate.reducer.ts | 21 +++-- src/routes/protected/estimate.routes.tsx | 14 ++- src/types/util.type.ts | 6 ++ 7 files changed, 141 insertions(+), 53 deletions(-) diff --git a/src/pages/articles/Articles.tsx b/src/pages/articles/Articles.tsx index 1d41c349..1cb861e6 100644 --- a/src/pages/articles/Articles.tsx +++ b/src/pages/articles/Articles.tsx @@ -16,6 +16,7 @@ import { IArticle } from '@/types/article.types'; import { getArticleArticlesSelector } from '@/redux/reducers/article.reducer'; import { createArticle, deleteArticle, goToArticle } from '@/redux/actions/article.action'; import Head from '@/components/Head'; +import { getUserFullName } from '@/utils/user.utils'; const Articles = () => { const articles = useSelector(getArticleArticlesSelector); @@ -29,9 +30,9 @@ const Articles = () => { dispatch(createArticle(values)); } - const handleDelete = (id: string) => { - dispatch(deleteArticle(id)); - } + // const handleDelete = (id: string) => { + // dispatch(deleteArticle(id)); + // } const handlePreview = (id: string) => { navigate(goToArticle(id)); @@ -60,9 +61,9 @@ const Articles = () => { {article.objectId} {article.title} - {/* + {article.has("author") ? getUserFullName(article.get("author")) : "-"} - */} + handlePreview(article.objectId)}> @@ -70,9 +71,9 @@ const Articles = () => { {/* handleEdit(article.id)}> */} - handleDelete(article.objectId)}> + {/* handleDelete(article.objectId)}> - + */} ))} diff --git a/src/pages/estimates/Estimates.tsx b/src/pages/estimates/Estimates.tsx index 1d21ba90..185b0d22 100644 --- a/src/pages/estimates/Estimates.tsx +++ b/src/pages/estimates/Estimates.tsx @@ -1,49 +1,92 @@ +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { FiPlus, FiTrash2 , FiEye } from "react-icons/fi"; + +import { Fab, IconButton } from '@mui/material'; +import { useNavigate } from '@tanstack/react-router'; +import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; import Head from '@/components/Head'; -import Dialog from '@/components/Dialog'; -import AddFab from '@/components/AddFab'; -import EstimateForm from './EstimateForm'; -import { EstimateInput } from '@/types/estimate.type'; -import { SubmitHandler } from 'react-hook-form'; -import { useDispatch } from 'react-redux'; -import { createEstimate } from '@/redux/actions/estimate.action'; - -const ESTIMATE_FORM_ID = 'estimate-form-id'; +import { getEstimateEstimatesSelector } from '@/redux/reducers/estimate.reducer'; +import { IEstimate } from '@/types/estimate.type'; +import { loadEstimates, onEstimatesEnter } from '@/redux/actions/estimate.action'; const Estimates = () => { - const { t } = useTranslation(); + const estimates = useSelector(getEstimateEstimatesSelector); + // const navigate = useNavigate(); + const { t } = useTranslation(); const dispatch = useDispatch(); + + // const handleAddEstimate = async () => { + // const values = { title: `Article ${uid}` }; + // dispatch(createEstimate(values)); + // } + const getEstimate = async () => { + // const values = { url: `Estimate ${uid}` }; + dispatch(loadEstimates()); - const [openFormDialog, setOpenFormDialog] = useState(false); + } - const onSubmitHandler: SubmitHandler = values => { - dispatch(createEstimate(values)); - }; + // const handleDelete = (id: string) => { + // dispatch(deleteArticle(id)); + // } - const toggleDialog = () => setOpenFormDialog(!openFormDialog); + // const handlePreview = (id: string) => { + // navigate(goToArticle(id)); + // } return (
- -

Estimates

- - - - + + + + + + Id + Title + {/* Author */} + Actions + + + + {estimates.map((estimate: IEstimate, index: number) => ( + + + {estimate.objectId} + + + {estimate.url} + + + {/* handlePreview(article.objectId)}> + + */} + {/* handleEdit(article.id)}> + + */} + {/* handleDelete(article.objectId)}> + + */} + + + ))} + +
+
+ {/* */} + {/* + + */}
); } diff --git a/src/pages/users/Users.tsx b/src/pages/users/Users.tsx index 0710a82e..4ce6a795 100644 --- a/src/pages/users/Users.tsx +++ b/src/pages/users/Users.tsx @@ -131,7 +131,7 @@ const Users = () => { } const onSendEmailFormSubmit = async (values: SendEmailInput) => { - console.log('values: ', values); + // console.log('values: ', values); if (!selectedUser) return; await dispatch(sendEmailToUser(selectedUser, values)); handleCloseDialog(); diff --git a/src/redux/actions/estimate.action.ts b/src/redux/actions/estimate.action.ts index 8e734922..99cf9d22 100644 --- a/src/redux/actions/estimate.action.ts +++ b/src/redux/actions/estimate.action.ts @@ -5,28 +5,49 @@ import { actionWithLoader } from "@/utils/app.utils"; import { AppDispatch } from "../store"; import { setMessageSlice } from "../reducers/app.reducer"; import i18n from "@/config/i18n"; -import { addEstimateToEstimateSlice } from "../reducers/estimate.reducer"; +import { addEstimateToEstimateSlice, loadEstimatesSlice } from "../reducers/estimate.reducer"; +// import { ParseResult } from "@/types/util.type"; const Estimate = Parse.Object.extend("Estimate"); const ESTIMATE_PROPERTIES = new Set(['url']); +export const loadEstimates = (): any => { + return actionWithLoader(async (dispatch: AppDispatch): Promise => { + // user from BO + const result: any = await new Parse.Query(Estimate) + .withCount() + .notEqualTo('deleted', true) + .find(); + + const estimates = result.results.map((estimate: Attributes) => estimate.toJSON()); + + dispatch(loadEstimatesSlice(estimates)); + }); +}; + export const createEstimate = (values: any): any => { return actionWithLoader(async (dispatch: AppDispatch): Promise => { const estimate = new Estimate() - // setValues(estimate, values, ESTIMATE_PROPERTIES); + setValues(estimate, values, ESTIMATE_PROPERTIES); estimate.set("url", values.url) const savedEstimate = await estimate.save(); - console.log('saved Estimate:-----------:', savedEstimate); dispatch(setMessageSlice(i18n.t('common:estimateCreatedSuccessfully'))); dispatch(addEstimateToEstimateSlice((savedEstimate as Attributes).toJSON())); // return savedEstimate; }); }; +export const onEstimatesEnter = (): any => { + return actionWithLoader(async (dispatch: AppDispatch): Promise => { + + dispatch(loadEstimates()); + }); +}; + // --------------------------------------- // // ------------- redirection ------------- // diff --git a/src/redux/reducers/estimate.reducer.ts b/src/redux/reducers/estimate.reducer.ts index 666d37e2..289b3dc6 100644 --- a/src/redux/reducers/estimate.reducer.ts +++ b/src/redux/reducers/estimate.reducer.ts @@ -1,5 +1,4 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { IArticle, IArticleState } from '@/types/article.types'; import { IEstimate, IEstimateState } from '@/types/estimate.type'; const initialState: IEstimateState = { @@ -15,11 +14,15 @@ export const estimate = createSlice({ addEstimateToEstimateSlice: (state: IEstimateState, action: PayloadAction) => { state.estimates = [...state.estimates, action.payload]; }, + loadEstimatesSlice: (state: IEstimateState, action: PayloadAction) => { + state.estimates = action.payload; + } }, }); export const { - addEstimateToEstimateSlice + addEstimateToEstimateSlice, + loadEstimatesSlice } = estimate.actions; // ---------------------------------------------- // @@ -27,10 +30,14 @@ export const { // ---------------------------------------------- // // NOTE: do not use RootState as state type to avoid import circular dependencies (from store.ts) // we can not commit to git if there is circular dependencies -export const getArticleSelector = (state: Record): IArticleState => state.article; -export const getArticleArticleSelector = (state: Record): IArticle => state.article.article; -export const getArticleLoadingSelector = (state: Record): boolean => state.article.loading; -export const getArticleArticlesSelector = (state: Record): IArticle[] => state.article.articles; -export const getArticleCountSelector = (state: Record): number => state.article.count; +// export const getArticleSelector = (state: Record): IArticleState => state.article; +// export const getArticleArticleSelector = (state: Record): IArticle => state.article.article; +// export const getArticleLoadingSelector = (state: Record): boolean => state.article.loading; +export const getEstimateEstimatesSelector = (state: Record): IEstimate[] => state.estimate.estimates; +console.log('getEstimateEstimatesSelector------- : ', getEstimateEstimatesSelector ); + + +// export const getArticleCountSelector = (state: Record): number => state.article.count; + export default estimate.reducer; diff --git a/src/routes/protected/estimate.routes.tsx b/src/routes/protected/estimate.routes.tsx index 0f8b342a..418d2cea 100644 --- a/src/routes/protected/estimate.routes.tsx +++ b/src/routes/protected/estimate.routes.tsx @@ -2,7 +2,10 @@ import { Outlet, createRoute } from "@tanstack/react-router"; import { privateLayout } from "./private.routes"; import { PATH_NAMES } from "@/utils/pathnames"; +import { onEnter } from "@/redux/actions/app.action"; import Estimates from "@/pages/estimates/Estimates"; +import Estimate from "@/pages/estimates/Estimate"; +import { onEstimatesEnter } from "@/redux/actions/estimate.action"; export const estimatesLayout = createRoute({ getParentRoute: () => privateLayout, @@ -12,11 +15,18 @@ export const estimatesLayout = createRoute({ export const estimatesRoute = createRoute({ getParentRoute: () => estimatesLayout, - // beforeLoad: onEnter(onArticlesEnter), + beforeLoad: onEnter(onEstimatesEnter), component: Estimates, path: "/", }); -const estimateRoutes = [estimatesRoute]; +export const estimateRoute = createRoute({ + getParentRoute: () => estimatesLayout, + // beforeLoad: onEnter(onArticleEnter), + component: Estimate, + path: "$id", +}); + +const estimateRoutes = [estimatesRoute, estimateRoute]; export default estimateRoutes; diff --git a/src/types/util.type.ts b/src/types/util.type.ts index f5789251..376c1308 100644 --- a/src/types/util.type.ts +++ b/src/types/util.type.ts @@ -1,5 +1,6 @@ import { Dayjs } from "dayjs"; import { ISelectOption } from "./app.type"; +import { Attributes } from "parse"; export type DateType = string | number | Date | Dayjs | null | undefined; @@ -21,3 +22,8 @@ export interface ICreatableSelectOption extends Partial { inputValue?: any; disabled?: boolean; } + +export interface ParseResult { + results: Attributes[]; +} +