From 059d8f206ea1d49d81a96c8a184cf94c0d131d4f Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 16 May 2024 20:28:11 +0900 Subject: [PATCH 01/15] chore: add react-query package --- package.json | 2 ++ yarn.lock | 33 +++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 2efa826..33157f7 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "prepare": "husky install" }, "dependencies": { + "@tanstack/react-query": "^5.36.2", + "@tanstack/react-query-devtools": "^5.36.2", "axios": "^0.21.2", "classnames": "^2.2.6", "framer-motion": "^10.15.0", diff --git a/yarn.lock b/yarn.lock index 8358ef9..c4f38e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -383,14 +383,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.7.2": - version "7.24.1" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz" - integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - -"@babel/plugin-syntax-jsx@^7.24.1": +"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.24.1", "@babel/plugin-syntax-jsx@^7.7.2": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== @@ -1715,6 +1708,30 @@ "@swc/core-win32-ia32-msvc" "1.3.71" "@swc/core-win32-x64-msvc" "1.3.71" +"@tanstack/query-core@5.36.1": + version "5.36.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.36.1.tgz#ae46f935c4752812a56c6815305061a3da82e7b8" + integrity sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg== + +"@tanstack/query-devtools@5.32.1": + version "5.32.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.32.1.tgz#2c03f2fbe9162b650e697c469c8618c7a05d593f" + integrity sha512-7Xq57Ctopiy/4atpb0uNY5VRuCqRS/1fi/WBCKKX6jHMa6cCgDuV/AQuiwRXcKARbq2OkVAOrW2v4xK9nTbcCA== + +"@tanstack/react-query-devtools@^5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.36.2.tgz#d020f6e44cf730af35d349426c96d85257d1a2ca" + integrity sha512-bkPQrKmKJOa2dNs6rBB9aef8jCG8XAg8QKIhwN8NI+QaXky86IofnO8YjiF6P1mYquLXbQvK0VZ9DnGV0wH/eA== + dependencies: + "@tanstack/query-devtools" "5.32.1" + +"@tanstack/react-query@^5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.36.2.tgz#1b7dc4c2fa0e48912335f0a157dd942cfa269326" + integrity sha512-bHNa+5dead+j6SA8WVlEOPxcGfteVFgdyFTCFcxBgjnPf0fFpHUc7aNZBCnvmPXqy/BeQa9zTuU9ectb7i8ZXA== + dependencies: + "@tanstack/query-core" "5.36.1" + "@testing-library/dom@^9.0.0": version "9.3.4" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz" From 06fad26ff25eb52f0b97e1f95e17b9b0be1504b6 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 16 May 2024 20:29:10 +0900 Subject: [PATCH 02/15] feat: add queries for account page --- src/queries/account.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/queries/account.ts diff --git a/src/queries/account.ts b/src/queries/account.ts new file mode 100644 index 0000000..9968001 --- /dev/null +++ b/src/queries/account.ts @@ -0,0 +1,44 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import axios from 'axios'; + +export const useSessionInfo = () => { + return useQuery({ + queryKey: ['sessionInfo'], + staleTime: Infinity, + queryFn: async () => { + return ( + await axios.get('/session/info', { + metadata: { + gaCategory: 'User', + gaVariable: 'GET / Instance', + }, + }) + ).data; + }, + }); +}; + +export const useDepartmentOptions = () => { + return useQuery({ + queryKey: ['departmentOptions'], + staleTime: Infinity, + queryFn: async () => { + return (await axios.get('/session/department-options')).data; + }, + }); +}; + +export const useUpdateFavoriteDepartments = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ selectedDepartments }: { selectedDepartments: string[] }) => { + return axios.post('/session/favorite-departments', { + fav_department: Array.from(selectedDepartments).filter((d) => d !== 'ALL'), + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['departmentOptions'] }); + queryClient.invalidateQueries({ queryKey: ['sessionInfo'] }); + }, + }); +}; From fa3c199b18bbb46258b12877f5a36b087656554c Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 16 May 2024 20:37:41 +0900 Subject: [PATCH 03/15] feat: set up react-query in App component --- src/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 5e6c628..c441f39 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,8 @@ import { initReactI18next } from 'react-i18next'; import { Provider as ReduxProvider } from 'react-redux'; import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import { compose, legacy_createStore as createStore } from 'redux'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import App from '@/App'; import { API_URL, TRACKING_ID } from '@/const'; @@ -126,9 +128,10 @@ ReactGA.initialize([ ]); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const reduxStore = createStore(rootReducer, composeEnhancers()); +const queryClient = new QueryClient(); + const router = createBrowserRouter([ { path: '/', @@ -157,7 +160,10 @@ const router = createBrowserRouter([ createRoot(document.getElementById('root')!).render( - + + + + , ); From 4d30e639e5f5c9b158761858a3b685e406ff4118 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 16 May 2024 21:47:07 +0900 Subject: [PATCH 04/15] migrate: AcademicInfoSubSection component Refactor to function component and apply react-query --- .../account/AcademicInfoSubSection.jsx | 56 ------------------- .../account/AcademicInfoSubSection.tsx | 41 ++++++++++++++ 2 files changed, 41 insertions(+), 56 deletions(-) delete mode 100644 src/components/sections/account/AcademicInfoSubSection.jsx create mode 100644 src/components/sections/account/AcademicInfoSubSection.tsx diff --git a/src/components/sections/account/AcademicInfoSubSection.jsx b/src/components/sections/account/AcademicInfoSubSection.jsx deleted file mode 100644 index b15d433..0000000 --- a/src/components/sections/account/AcademicInfoSubSection.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; - -import { appBoundClassNames as classNames } from '../../../common/boundClassNames'; - -import userShape from '../../../shapes/model/session/UserShape'; -import { CONTACT } from '../../../common/constants'; -import Attributes from '../../Attributes'; - -class AcademicInfoSubSection extends Component { - render() { - const { t } = this.props; - const { user } = this.props; - - if (user == null) { - return null; - } - - return ( -
-
{t('ui.title.academicInformation')}
- d[t('js.property.name')]).join(', '), - }, - ]} - /> -
- {t('ui.message.academicInfoCaptionHead')} - - {CONTACT} - - {t('ui.message.academicInfoCaptionTail')} -
-
- ); - } -} - -const mapStateToProps = (state) => ({ - user: state.common.user.user, -}); - -const mapDispatchToProps = (dispatch) => ({}); - -AcademicInfoSubSection.propTypes = { - user: userShape, -}; - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(AcademicInfoSubSection), -); diff --git a/src/components/sections/account/AcademicInfoSubSection.tsx b/src/components/sections/account/AcademicInfoSubSection.tsx new file mode 100644 index 0000000..f4d8072 --- /dev/null +++ b/src/components/sections/account/AcademicInfoSubSection.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from 'react-i18next'; + +import { appBoundClassNames as classNames } from '@/common/boundClassNames'; +import { CONTACT } from '@/common/constants'; +import Attributes from '@/components/Attributes'; +import { useSessionInfo } from '@/queries/account'; +import { useTranslatedString } from '@/hooks/useTranslatedString'; + +const AcademicInfoSubSection = () => { + const { t } = useTranslation(); + const translate = useTranslatedString(); + const { data: user } = useSessionInfo(); + + if (!user) { + return null; + } + + return ( +
+
{t('ui.title.academicInformation')}
+ translate(d, 'name')).join(', '), + }, + ]} + /> +
+ {t('ui.message.academicInfoCaptionHead')} + + {CONTACT} + + {t('ui.message.academicInfoCaptionTail')} +
+
+ ); +}; + +export default AcademicInfoSubSection; From e6a145da63903096b51c5af507b2a1b632ead971 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 16 May 2024 21:48:35 +0900 Subject: [PATCH 05/15] migrate: MyInfoSubSection component Refactor to function component and apply react-query --- .../sections/account/MyInfoSubSection.jsx | 56 ------------------- .../sections/account/MyInfoSubSection.tsx | 40 +++++++++++++ 2 files changed, 40 insertions(+), 56 deletions(-) delete mode 100644 src/components/sections/account/MyInfoSubSection.jsx create mode 100644 src/components/sections/account/MyInfoSubSection.tsx diff --git a/src/components/sections/account/MyInfoSubSection.jsx b/src/components/sections/account/MyInfoSubSection.jsx deleted file mode 100644 index c9ee4d0..0000000 --- a/src/components/sections/account/MyInfoSubSection.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; - -import { appBoundClassNames as classNames } from '../../../common/boundClassNames'; - -import userShape from '../../../shapes/model/session/UserShape'; - -import { getFullName } from '../../../common/guideline/components/Header'; -import Attributes from '../../Attributes'; - -class MyInfoSubSection extends Component { - render() { - const { t } = this.props; - const { user } = this.props; - - if (user == null) { - return null; - } - - return ( -
-
{t('ui.title.myInformation')}
- -
- {t('ui.message.myInfoCaptionHead')} - - SPARCS SSO - - {t('ui.message.myInfoCaptionTail')} -
-
- ); - } -} - -const mapStateToProps = (state) => ({ - user: state.common.user.user, -}); - -const mapDispatchToProps = (dispatch) => ({}); - -MyInfoSubSection.propTypes = { - user: userShape, -}; - -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(MyInfoSubSection)); diff --git a/src/components/sections/account/MyInfoSubSection.tsx b/src/components/sections/account/MyInfoSubSection.tsx new file mode 100644 index 0000000..b1ae2bb --- /dev/null +++ b/src/components/sections/account/MyInfoSubSection.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from 'react-i18next'; + +import { appBoundClassNames as classNames } from '@/common/boundClassNames'; +import { getFullName } from '@/common/guideline/components/Header'; +import Attributes from '@/components/Attributes'; +import { useSessionInfo } from '@/queries/account'; + +const MyInfoSubSection = () => { + const { t } = useTranslation(); + const { data: user } = useSessionInfo(); + + if (!user) { + return null; + } + + return ( +
+
{t('ui.title.myInformation')}
+ +
+ {t('ui.message.myInfoCaptionHead')} + + SPARCS SSO + + {t('ui.message.myInfoCaptionTail')} +
+
+ ); +}; + +export default MyInfoSubSection; From 1331b5545acbbb7f16794a0782074851105866db Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 16 May 2024 21:49:16 +0900 Subject: [PATCH 06/15] migrate: FavoriteDepartmentSubSection component Refactor to function component and apply react-query --- .../account/FavoriteDepartmentsSubSection.jsx | 174 ------------------ .../account/FavoriteDepartmentsSubSection.tsx | 81 ++++++++ src/queries/account.ts | 6 +- src/shapes/model/session/User.ts | 2 +- 4 files changed, 86 insertions(+), 177 deletions(-) delete mode 100644 src/components/sections/account/FavoriteDepartmentsSubSection.jsx create mode 100644 src/components/sections/account/FavoriteDepartmentsSubSection.tsx diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.jsx b/src/components/sections/account/FavoriteDepartmentsSubSection.jsx deleted file mode 100644 index b82090e..0000000 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.jsx +++ /dev/null @@ -1,174 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { withTranslation } from 'react-i18next'; -import axios from 'axios'; - -import { appBoundClassNames as classNames } from '../../../common/boundClassNames'; - -import SearchFilter from '../../SearchFilter'; - -import { setUser } from '../../../redux/actions/common/user'; - -import userShape from '../../../shapes/model/session/UserShape'; - -class FavoriteDepartmentsSubSection extends Component { - constructor(props) { - super(props); - - this.state = { - savedSelectedDepartments: new Set([]), - selectedDepartments: new Set([]), - allDepartments: [], - }; - } - - componentDidMount() { - const { user } = this.props; - - if (user) { - this._setUserDepartment(); - } - - axios - .get('/session/department-options', {}) - .then((response) => { - this.setState({ - allDepartments: response.data.flat(1), - }); - }) - .catch((error) => {}); - } - - componentDidUpdate(prevProps) { - const { user } = this.props; - - if (!prevProps.user && user) { - this._setUserDepartment(); - } - } - - _setUserDepartment = () => { - const { user } = this.props; - - this.setState({ - savedSelectedDepartments: new Set(user.favorite_departments.map((d) => String(d.id))), - selectedDepartments: new Set(user.favorite_departments.map((d) => String(d.id))), - }); - }; - - updateCheckedValues = (filterName) => (checkedValues) => { - this.setState({ - [filterName]: checkedValues, - }); - }; - - handleSubmit = (e) => { - const { selectedDepartments } = this.state; - - e.preventDefault(); - e.stopPropagation(); - - axios - .post( - '/session/favorite-departments', - { - fav_department: Array.from(selectedDepartments).filter((d) => d !== 'ALL'), - }, - {}, - ) - .then((response) => { - this.setState({ - savedSelectedDepartments: selectedDepartments, - }); - this._refetchUser(); - }) - .catch((error) => {}); - }; - - _refetchUser = () => { - const { setUserDispatch } = this.props; - - axios - .get('/session/info', { - metadata: { - gaCategory: 'User', - gaVariable: 'GET / Instance', - }, - }) - .then((response) => { - setUserDispatch(response.data); - }) - .catch((error) => {}); - }; - - render() { - const { t } = this.props; - const { user } = this.props; - const { allDepartments, savedSelectedDepartments, selectedDepartments } = this.state; - - if (user == null) { - return null; - } - - const departmentOptions = allDepartments.map((d) => [ - String(d.id), - `${d[t('js.property.name')]} (${d.code})`, - ]); - - const hasChange = - selectedDepartments.size !== savedSelectedDepartments.size || - Array.from(selectedDepartments).some((d) => !savedSelectedDepartments.has(d)); - - const favoriteDepartmentForm = - allDepartments.length === 0 ? null : ( -
- -
- {hasChange ? ( - - ) : ( - - )} -
- - ); - - return ( -
-
{t('ui.title.settings')}
- {favoriteDepartmentForm} -
- ); - } -} - -const mapStateToProps = (state) => ({ - user: state.common.user.user, -}); - -const mapDispatchToProps = (dispatch) => ({ - setUserDispatch: (user) => { - dispatch(setUser(user)); - }, -}); - -FavoriteDepartmentsSubSection.propTypes = { - user: userShape, - - setUserDispatch: PropTypes.func.isRequired, -}; - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(FavoriteDepartmentsSubSection), -); diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx new file mode 100644 index 0000000..0ad2fd2 --- /dev/null +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -0,0 +1,81 @@ +import { FormEventHandler, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { appBoundClassNames as classNames } from '@/common/boundClassNames'; +import SearchFilter from '@/components/SearchFilter'; +import { + useDepartmentOptions, + useSessionInfo, + useUpdateFavoriteDepartments, +} from '@/queries/account'; +import { useTranslatedString } from '@/hooks/useTranslatedString'; + +const FavoriteDepartmentsSubSection = () => { + const [selectedDepartments, setSelectedDepartments] = useState>(new Set([])); + + const { t } = useTranslation(); + const translate = useTranslatedString(); + const { data: user } = useSessionInfo(); + const { data: allDepartmentOptions } = useDepartmentOptions(); + const { mutate: updateFavoriteDepartments } = useUpdateFavoriteDepartments(); + + useEffect(() => { + if (user) { + setSelectedDepartments(new Set(user.favorite_departments.map((d) => String(d.id)))); + } + }, [user]); + + const handleSubmit: FormEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + updateFavoriteDepartments({ + selectedDepartments: Array.from(selectedDepartments).filter((d) => d !== 'ALL'), + }); + setSelectedDepartments(selectedDepartments); + }; + + if (!user || !allDepartmentOptions) { + return null; + } + + const departmentOptions = allDepartmentOptions + .flat(1) + .map((d) => [String(d.id), `${translate(d, 'name')} (${d.code})`]); + + const hasChange = + new Set(user.favorite_departments.map((d) => d.id.toString())).size !== + selectedDepartments.size || + [...new Set(user.favorite_departments.map((d) => d.id.toString()))].some( + (d) => !selectedDepartments.has(d), + ); + + return ( +
+
{t('ui.title.settings')}
+ {departmentOptions.length !== 0 && ( +
+ ) => + setSelectedDepartments(checkedValues) + } + inputName="department" + titleName={t('ui.search.favoriteDepartment')} + options={departmentOptions} + checkedValues={selectedDepartments} + /> +
+ +
+ + )} +
+ ); +}; + +export default FavoriteDepartmentsSubSection; diff --git a/src/queries/account.ts b/src/queries/account.ts index 9968001..50486aa 100644 --- a/src/queries/account.ts +++ b/src/queries/account.ts @@ -1,3 +1,5 @@ +import User from '@/shapes/model/session/User'; +import Department from '@/shapes/model/subject/Department'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import axios from 'axios'; @@ -7,7 +9,7 @@ export const useSessionInfo = () => { staleTime: Infinity, queryFn: async () => { return ( - await axios.get('/session/info', { + await axios.get('/session/info', { metadata: { gaCategory: 'User', gaVariable: 'GET / Instance', @@ -23,7 +25,7 @@ export const useDepartmentOptions = () => { queryKey: ['departmentOptions'], staleTime: Infinity, queryFn: async () => { - return (await axios.get('/session/department-options')).data; + return (await axios.get('/session/department-options')).data; }, }); }; diff --git a/src/shapes/model/session/User.ts b/src/shapes/model/session/User.ts index 1105d0c..eded726 100644 --- a/src/shapes/model/session/User.ts +++ b/src/shapes/model/session/User.ts @@ -11,7 +11,7 @@ export default interface User { majors: Department[]; department?: Department; departments: Department[]; - favorite_departments?: Department[]; + favorite_departments: Department[]; review_writable_lectures: Lecture[]; my_timetable_lectures: Lecture[]; reviews: Review[]; From 9fcdfe9094bcf544d99170da2111f9f4b3f6811d Mon Sep 17 00:00:00 2001 From: Seungbin Oh Date: Mon, 20 May 2024 01:14:38 +0900 Subject: [PATCH 07/15] test: add unit test for src/components/sections/account --- jest.setup.js | 3 ++ package.json | 1 + .../account/AcademicInfoSubSection.tsx | 5 ++- .../__tests__/AcademicInfoSubSection.test.tsx | 16 ++++++++ .../FavoriteDepartmentsSubSection.test.tsx | 6 +++ .../__tests__/MyInfoSubSection.test.tsx | 6 +++ src/test-utils.tsx | 37 ++++++++++++++++--- yarn.lock | 19 ++++++++++ 8 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 src/components/sections/account/__tests__/AcademicInfoSubSection.test.tsx create mode 100644 src/components/sections/account/__tests__/FavoriteDepartmentsSubSection.test.tsx create mode 100644 src/components/sections/account/__tests__/MyInfoSubSection.test.tsx diff --git a/jest.setup.js b/jest.setup.js index 1b8dd7e..32e70ba 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import axios from 'axios'; // eslint-disable-next-line no-undef jest.mock('react-i18next', () => ({ @@ -17,3 +18,5 @@ jest.mock('react-i18next', () => ({ return Component; }, })); + +axios.defaults.baseURL = 'http://localhost'; diff --git a/package.json b/package.json index 33157f7..b55218e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "i18next-xhr-backend": "^3.2.2", "lodash": "^4.17.21", "moment": "^2.29.2", + "nock": "^13.5.4", "prop-types": "^15.7.2", "qs": "^6.9.4", "react": "^18.2.0", diff --git a/src/components/sections/account/AcademicInfoSubSection.tsx b/src/components/sections/account/AcademicInfoSubSection.tsx index f4d8072..e6a52ff 100644 --- a/src/components/sections/account/AcademicInfoSubSection.tsx +++ b/src/components/sections/account/AcademicInfoSubSection.tsx @@ -29,7 +29,10 @@ const AcademicInfoSubSection = () => { />
{t('ui.message.academicInfoCaptionHead')} - + {CONTACT} {t('ui.message.academicInfoCaptionTail')} diff --git a/src/components/sections/account/__tests__/AcademicInfoSubSection.test.tsx b/src/components/sections/account/__tests__/AcademicInfoSubSection.test.tsx new file mode 100644 index 0000000..0c66651 --- /dev/null +++ b/src/components/sections/account/__tests__/AcademicInfoSubSection.test.tsx @@ -0,0 +1,16 @@ +import { renderWithQueryClient, sampleUser } from '@/test-utils'; +import AcademicInfoSubSection from '../AcademicInfoSubSection'; +import nock from 'nock'; + +test('renders AcademicInfoSubSection', async () => { + renderWithQueryClient(); +}); + +test('renders contact mail, student id', async () => { + nock('http://localhost').get('/session/info').reply(200, sampleUser); + const { findByTestId, findByText } = renderWithQueryClient(); + const contact = await findByTestId('contact-mail'); + expect(contact.textContent).toBe('otlplus@sparcs.org'); + const studentId = await findByText('20210378'); + expect(studentId).toBeTruthy(); +}); diff --git a/src/components/sections/account/__tests__/FavoriteDepartmentsSubSection.test.tsx b/src/components/sections/account/__tests__/FavoriteDepartmentsSubSection.test.tsx new file mode 100644 index 0000000..1f06d25 --- /dev/null +++ b/src/components/sections/account/__tests__/FavoriteDepartmentsSubSection.test.tsx @@ -0,0 +1,6 @@ +import { renderWithQueryClient } from '@/test-utils'; +import FavoriteDepartmentsSubSection from '../FavoriteDepartmentsSubSection'; + +test('renders FavoriteDepartmentsSubSection', async () => { + renderWithQueryClient(); +}); diff --git a/src/components/sections/account/__tests__/MyInfoSubSection.test.tsx b/src/components/sections/account/__tests__/MyInfoSubSection.test.tsx new file mode 100644 index 0000000..008e1eb --- /dev/null +++ b/src/components/sections/account/__tests__/MyInfoSubSection.test.tsx @@ -0,0 +1,6 @@ +import { renderWithQueryClient } from '@/test-utils'; +import MyInfoSubSection from '../MyInfoSubSection'; + +test('renders MyInfoSubSection', async () => { + renderWithQueryClient(); +}); diff --git a/src/test-utils.tsx b/src/test-utils.tsx index 59f1265..a23cd73 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -1,16 +1,27 @@ -import React, { ReactElement } from 'react'; -import { render, RenderOptions } from '@testing-library/react'; +import { ReactElement, ReactNode } from 'react'; +import { render } from '@testing-library/react'; import Course from '@/shapes/model/subject/Course'; import { SemesterType } from '@/shapes/enum'; import Lecture from '@/shapes/model/subject/Lecture'; import Classtime from '@/shapes/model/subject/Classtime'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import User from '@/shapes/model/session/User'; -const AllTheProviders = ({ children }: { children: React.ReactNode }) => { - return children; +const queryClientWrapper = ({ children }: { children: ReactNode }) => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + return {children}; }; -const customRender = (ui: ReactElement, options?: Omit) => - render(ui, { wrapper: AllTheProviders, ...options }); +const customRender = (ui: ReactElement) => render(ui); + +export const renderWithQueryClient = (ui: ReactElement) => + render(ui, { wrapper: queryClientWrapper }); export * from '@testing-library/react'; export { customRender as render }; @@ -84,3 +95,17 @@ export const sampleClasstime: Classtime = { begin: 0, end: 0, }; + +export const sampleUser: User = { + id: 0, + email: '', + student_id: '20210378', + firstName: '', + lastName: '', + majors: [], + departments: [], + favorite_departments: [], + review_writable_lectures: [], + my_timetable_lectures: [], + reviews: [], +}; diff --git a/yarn.lock b/yarn.lock index c4f38e9..7ef244d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4921,6 +4921,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + json2mq@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz" @@ -5465,6 +5470,15 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +nock@^13.5.4: + version "13.5.4" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" + integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -5871,6 +5885,11 @@ prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + property-information@^6.0.0: version "6.2.0" resolved "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz" From 4fafacef2849f9634aa6d123f2c929f6670a98cd Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 23 May 2024 17:42:25 +0900 Subject: [PATCH 08/15] chore: remove unnecessary line --- .../sections/account/FavoriteDepartmentsSubSection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx index 0ad2fd2..7e85373 100644 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -32,7 +32,6 @@ const FavoriteDepartmentsSubSection = () => { updateFavoriteDepartments({ selectedDepartments: Array.from(selectedDepartments).filter((d) => d !== 'ALL'), }); - setSelectedDepartments(selectedDepartments); }; if (!user || !allDepartmentOptions) { From 6d99fddc82fc17a8e4e567e7aa0efc57099a9148 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 23 May 2024 17:44:16 +0900 Subject: [PATCH 09/15] feat: process departmentOption data in RQ --- .../sections/account/FavoriteDepartmentsSubSection.tsx | 10 ++-------- src/queries/account.ts | 6 +++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx index 7e85373..4226dd7 100644 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -8,15 +8,13 @@ import { useSessionInfo, useUpdateFavoriteDepartments, } from '@/queries/account'; -import { useTranslatedString } from '@/hooks/useTranslatedString'; const FavoriteDepartmentsSubSection = () => { const [selectedDepartments, setSelectedDepartments] = useState>(new Set([])); const { t } = useTranslation(); - const translate = useTranslatedString(); const { data: user } = useSessionInfo(); - const { data: allDepartmentOptions } = useDepartmentOptions(); + const { data: departmentOptions } = useDepartmentOptions(); const { mutate: updateFavoriteDepartments } = useUpdateFavoriteDepartments(); useEffect(() => { @@ -34,14 +32,10 @@ const FavoriteDepartmentsSubSection = () => { }); }; - if (!user || !allDepartmentOptions) { + if (!user || !departmentOptions) { return null; } - const departmentOptions = allDepartmentOptions - .flat(1) - .map((d) => [String(d.id), `${translate(d, 'name')} (${d.code})`]); - const hasChange = new Set(user.favorite_departments.map((d) => d.id.toString())).size !== selectedDepartments.size || diff --git a/src/queries/account.ts b/src/queries/account.ts index 50486aa..0f7d42f 100644 --- a/src/queries/account.ts +++ b/src/queries/account.ts @@ -1,3 +1,4 @@ +import { useTranslatedString } from '@/hooks/useTranslatedString'; import User from '@/shapes/model/session/User'; import Department from '@/shapes/model/subject/Department'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -21,11 +22,14 @@ export const useSessionInfo = () => { }; export const useDepartmentOptions = () => { + const translate = useTranslatedString(); return useQuery({ queryKey: ['departmentOptions'], staleTime: Infinity, queryFn: async () => { - return (await axios.get('/session/department-options')).data; + return (await axios.get('/session/department-options')).data + .flat(1) + .map((d) => [String(d.id), `${translate(d, 'name')} (${d.code})`]); }, }); }; From 24ca9ab38646b498f04e957687aa3afad2fad00b Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 23 May 2024 17:45:37 +0900 Subject: [PATCH 10/15] chore: simplify selected departments comparison logic --- .../sections/account/FavoriteDepartmentsSubSection.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx index 4226dd7..0c938b1 100644 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -36,12 +36,10 @@ const FavoriteDepartmentsSubSection = () => { return null; } + // TODO: Change comparison logic const hasChange = - new Set(user.favorite_departments.map((d) => d.id.toString())).size !== - selectedDepartments.size || - [...new Set(user.favorite_departments.map((d) => d.id.toString()))].some( - (d) => !selectedDepartments.has(d), - ); + user.favorite_departments.length !== selectedDepartments.size || + user.favorite_departments.some(({ id }) => !selectedDepartments.has(id.toString())); return (
From c799fd57d7ed8323d26b3247f351e50c6213942d Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 23 May 2024 17:49:51 +0900 Subject: [PATCH 11/15] chore: remove duplicated logic --- .../sections/account/FavoriteDepartmentsSubSection.tsx | 4 +--- src/queries/account.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx index 0c938b1..99d887a 100644 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -27,9 +27,7 @@ const FavoriteDepartmentsSubSection = () => { e.preventDefault(); e.stopPropagation(); - updateFavoriteDepartments({ - selectedDepartments: Array.from(selectedDepartments).filter((d) => d !== 'ALL'), - }); + updateFavoriteDepartments({ selectedDepartments }); }; if (!user || !departmentOptions) { diff --git a/src/queries/account.ts b/src/queries/account.ts index 0f7d42f..7a284eb 100644 --- a/src/queries/account.ts +++ b/src/queries/account.ts @@ -37,7 +37,7 @@ export const useDepartmentOptions = () => { export const useUpdateFavoriteDepartments = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ selectedDepartments }: { selectedDepartments: string[] }) => { + mutationFn: ({ selectedDepartments }: { selectedDepartments: Set }) => { return axios.post('/session/favorite-departments', { fav_department: Array.from(selectedDepartments).filter((d) => d !== 'ALL'), }); From ce3d2d21809de11da25363d6cc1d17102f815af0 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 23 May 2024 19:52:12 +0900 Subject: [PATCH 12/15] refactor: use QUERY_KEYS constants in account queries --- src/queries/account.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/queries/account.ts b/src/queries/account.ts index 7a284eb..226f1f3 100644 --- a/src/queries/account.ts +++ b/src/queries/account.ts @@ -4,9 +4,14 @@ import Department from '@/shapes/model/subject/Department'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import axios from 'axios'; +const QUERY_KEYS = { + SESSION_INFO: 'sessionInfo', + DEPARTMENT_OPTIONS: 'departmentOptions', +} as const; + export const useSessionInfo = () => { return useQuery({ - queryKey: ['sessionInfo'], + queryKey: [QUERY_KEYS.SESSION_INFO], staleTime: Infinity, queryFn: async () => { return ( @@ -24,7 +29,7 @@ export const useSessionInfo = () => { export const useDepartmentOptions = () => { const translate = useTranslatedString(); return useQuery({ - queryKey: ['departmentOptions'], + queryKey: [QUERY_KEYS.DEPARTMENT_OPTIONS], staleTime: Infinity, queryFn: async () => { return (await axios.get('/session/department-options')).data @@ -43,8 +48,8 @@ export const useUpdateFavoriteDepartments = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['departmentOptions'] }); - queryClient.invalidateQueries({ queryKey: ['sessionInfo'] }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.DEPARTMENT_OPTIONS] }); + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.SESSION_INFO] }); }, }); }; From 947ad17bf9cd7a00ed36a5c326e4472df47314b5 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 30 May 2024 14:30:17 +0900 Subject: [PATCH 13/15] refactor: improve loading behavior in FavoriteDepartmentsSubSection --- .../sections/account/FavoriteDepartmentsSubSection.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx index 99d887a..c22fb44 100644 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -30,7 +30,7 @@ const FavoriteDepartmentsSubSection = () => { updateFavoriteDepartments({ selectedDepartments }); }; - if (!user || !departmentOptions) { + if (!user) { return null; } @@ -42,7 +42,7 @@ const FavoriteDepartmentsSubSection = () => { return (
{t('ui.title.settings')}
- {departmentOptions.length !== 0 && ( + {departmentOptions ? (
) => @@ -62,6 +62,8 @@ const FavoriteDepartmentsSubSection = () => {
+ ) : ( + {t('ui.placeholder.loading')} )}
); From 463ea43710b84ef67a5e673b3276595217869e67 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 30 May 2024 16:52:42 +0900 Subject: [PATCH 14/15] fix: add language switch to departmentOptions --- .../sections/account/FavoriteDepartmentsSubSection.tsx | 7 ++++++- src/queries/account.ts | 7 ++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx index c22fb44..e54d033 100644 --- a/src/components/sections/account/FavoriteDepartmentsSubSection.tsx +++ b/src/components/sections/account/FavoriteDepartmentsSubSection.tsx @@ -8,11 +8,13 @@ import { useSessionInfo, useUpdateFavoriteDepartments, } from '@/queries/account'; +import { useTranslatedString } from '@/hooks/useTranslatedString'; const FavoriteDepartmentsSubSection = () => { const [selectedDepartments, setSelectedDepartments] = useState>(new Set([])); const { t } = useTranslation(); + const translate = useTranslatedString(); const { data: user } = useSessionInfo(); const { data: departmentOptions } = useDepartmentOptions(); const { mutate: updateFavoriteDepartments } = useUpdateFavoriteDepartments(); @@ -50,7 +52,10 @@ const FavoriteDepartmentsSubSection = () => { } inputName="department" titleName={t('ui.search.favoriteDepartment')} - options={departmentOptions} + options={departmentOptions.map((d) => [ + String(d.id), + `${translate(d, 'name')} (${d.code})`, + ])} checkedValues={selectedDepartments} />
diff --git a/src/queries/account.ts b/src/queries/account.ts index 226f1f3..823e055 100644 --- a/src/queries/account.ts +++ b/src/queries/account.ts @@ -1,4 +1,3 @@ -import { useTranslatedString } from '@/hooks/useTranslatedString'; import User from '@/shapes/model/session/User'; import Department from '@/shapes/model/subject/Department'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -27,14 +26,12 @@ export const useSessionInfo = () => { }; export const useDepartmentOptions = () => { - const translate = useTranslatedString(); + // const translate = useTranslatedString(); return useQuery({ queryKey: [QUERY_KEYS.DEPARTMENT_OPTIONS], staleTime: Infinity, queryFn: async () => { - return (await axios.get('/session/department-options')).data - .flat(1) - .map((d) => [String(d.id), `${translate(d, 'name')} (${d.code})`]); + return (await axios.get('/session/department-options')).data.flat(1); }, }); }; From aea8afd432ee15f2361903ba4e03f402fdab64c8 Mon Sep 17 00:00:00 2001 From: Yumin Cho Date: Thu, 30 May 2024 16:53:37 +0900 Subject: [PATCH 15/15] migrate: convert AccountPage to TS --- src/pages/AccountPage.jsx | 44 --------------------------------------- src/pages/AccountPage.tsx | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 44 deletions(-) delete mode 100644 src/pages/AccountPage.jsx create mode 100644 src/pages/AccountPage.tsx diff --git a/src/pages/AccountPage.jsx b/src/pages/AccountPage.jsx deleted file mode 100644 index f008bb9..0000000 --- a/src/pages/AccountPage.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { Component } from 'react'; -import { withTranslation } from 'react-i18next'; - -import { appBoundClassNames as classNames } from '../common/boundClassNames'; - -import Divider from '../components/Divider'; -import Scroller from '../components/Scroller'; -import MyInfoSubSection from '../components/sections/account/MyInfoSubSection'; -import AcademicInfoSubSection from '../components/sections/account/AcademicInfoSubSection'; -import FavoriteDepartmentsSubSection from '../components/sections/account/FavoriteDepartmentsSubSection'; -import { API_URL } from '../const'; - -class AccountPage extends Component { - render() { - const { t } = this.props; - return ( -
-
-
- - - - - - - - - -
-
-
- ); - } -} - -AccountPage.propTypes = {}; - -export default withTranslation()(AccountPage); diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx new file mode 100644 index 0000000..e1e43cf --- /dev/null +++ b/src/pages/AccountPage.tsx @@ -0,0 +1,38 @@ +import { useTranslation } from 'react-i18next'; + +import { appBoundClassNames as classNames } from '@/common/boundClassNames'; +import Divider from '@/components/Divider'; +import Scroller from '@/components/Scroller'; +import MyInfoSubSection from '@/components/sections/account/MyInfoSubSection'; +import AcademicInfoSubSection from '@/components/sections/account/AcademicInfoSubSection'; +import FavoriteDepartmentsSubSection from '@/components/sections/account/FavoriteDepartmentsSubSection'; +import { API_URL } from '@/const'; + +const AccountPage = () => { + const { t } = useTranslation(); + return ( +
+
+
+ + + + + + + + + +
+
+
+ ); +}; + +export default AccountPage;