From 38a245c5ccfa4354380a59af0338657ae4b45ac4 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Wed, 16 Oct 2024 21:16:01 +0200 Subject: [PATCH 01/26] initial implementation of the userMachine --- package-lock.json | 13 +++++- packages/xstate/package.json | 3 +- packages/xstate/src/machines/userMachine.js | 48 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/xstate/src/machines/userMachine.js diff --git a/package-lock.json b/package-lock.json index e75776d..b258f64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4624,6 +4624,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xstate": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz", + "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -4823,10 +4832,12 @@ } }, "packages/xstate": { + "name": "@timo/xstate", "version": "1.0.0", "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "xstate": "^5.18.2" }, "devDependencies": { "@types/react": "^18.2.64", diff --git a/packages/xstate/package.json b/packages/xstate/package.json index 210ad62..21b57ea 100644 --- a/packages/xstate/package.json +++ b/packages/xstate/package.json @@ -10,7 +10,8 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "xstate": "^5.18.2" }, "devDependencies": { "@types/react": "^18.2.64", diff --git a/packages/xstate/src/machines/userMachine.js b/packages/xstate/src/machines/userMachine.js new file mode 100644 index 0000000..93d5a12 --- /dev/null +++ b/packages/xstate/src/machines/userMachine.js @@ -0,0 +1,48 @@ +import { assign, fromPromise, setup } from 'xstate'; +import { getUser } from '@timo/common/api'; + +const USER_STATES = { + UNKNOWN: 'unknown', + AUTHENTICATED: 'authenticated', + UNAUTHENTICATED: 'unauthenticated' +}; + +const userMachine = setup({ + actors: { + getUser: fromPromise(getUser) + } +}).createMachine({ + id: 'user', + initial: 'unknown', + context: { + data: null, + error: null + }, + states: { + [USER_STATES.UNKNOWN]: { + invoke: { + src: 'getUser', + onDone: { + target: USER_STATES.AUTHENTICATED, + actions: assign({ + data: (_, event) => event.data + }) + }, + onError: { + target: USER_STATES.UNAUTHENTICATED, + actions: assign({ + error: (_, event) => event.error + }) + } + } + }, + [USER_STATES.AUTHENTICATED]: { + type: 'final' + }, + [USER_STATES.UNAUTHENTICATED]: { + type: 'final' + } + } +}); + +export default userMachine; \ No newline at end of file From 8a1b9e1263b27e9254d9d35ecfcc47b366fbc06c Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 17 Oct 2024 20:22:45 +0200 Subject: [PATCH 02/26] initial implementation of userMachine --- package-lock.json | 32 ++++ packages/xstate/package.json | 1 + packages/xstate/src/App.jsx | 10 +- .../xstate/src/context/UserMachineContext.jsx | 6 + .../contextualComponents/ProtectedRoute.jsx | 32 ++++ .../contextualComponents/TopBarWithUser.jsx | 24 +++ packages/xstate/src/machines/userMachine.js | 165 ++++++++++++++++-- packages/xstate/src/routes/Login/Login.jsx | 38 ++-- .../xstate/src/routes/Profile/Profile.jsx | 10 +- .../Profile/sections/ChangePassword.jsx | 6 +- .../routes/Profile/sections/CustomizeUser.jsx | 23 ++- 11 files changed, 293 insertions(+), 54 deletions(-) create mode 100644 packages/xstate/src/context/UserMachineContext.jsx create mode 100644 packages/xstate/src/contextualComponents/ProtectedRoute.jsx create mode 100644 packages/xstate/src/contextualComponents/TopBarWithUser.jsx diff --git a/package-lock.json b/package-lock.json index b258f64..bf00415 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1323,6 +1323,24 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@xstate/react": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@xstate/react/-/react-4.1.3.tgz", + "integrity": "sha512-zhE+ZfrcCR87bu71Rkh5Z5ruZBivR/7uD/dkelzJqjQdI45IZc9DqTI8lL4Cg5+VN2p5k86KxDsusqW1kW11Tg==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.2", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "xstate": "^5.18.2" + }, + "peerDependenciesMeta": { + "xstate": { + "optional": true + } + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -4384,6 +4402,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -4835,6 +4866,7 @@ "name": "@timo/xstate", "version": "1.0.0", "dependencies": { + "@xstate/react": "^4.1.3", "react": "^18.2.0", "react-dom": "^18.2.0", "xstate": "^5.18.2" diff --git a/packages/xstate/package.json b/packages/xstate/package.json index 21b57ea..f7bc784 100644 --- a/packages/xstate/package.json +++ b/packages/xstate/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@xstate/react": "^4.1.3", "react": "^18.2.0", "react-dom": "^18.2.0", "xstate": "^5.18.2" diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx index a945235..9b1a696 100644 --- a/packages/xstate/src/App.jsx +++ b/packages/xstate/src/App.jsx @@ -1,14 +1,14 @@ import Router from '@timo/common/components/Router'; -import UserContextProvider from '@timo/common/context/UserContextProvider'; import Container from '@timo/common/components/Container'; import Title from '@timo/common/components/Title'; -import TopBarWithUser from '@timo/common/contextualComponents/TopBarWithUser'; -import ProtectedRoute from '@timo/common/contextualComponents/ProtectedRoute'; import Login from './routes/Login/Login'; import Entries from './routes/Entries/Entries'; import NewEntry from './routes/NewEntry/NewEntry'; import Profile from './routes/Profile/Profile'; +import ProtectedRoute from './contextualComponents/ProtectedRoute'; +import TopBarWithUser from './contextualComponents/TopBarWithUser'; +import UserMachineContext from './context/UserMachineContext'; const routes = [ { path: '/', name: 'Entries' }, @@ -19,7 +19,7 @@ const routes = [ ]; const App = () => ( - + {(routeName, history) => { let pageComponent = null; @@ -61,7 +61,7 @@ const App = () => ( ); }} - + ); export default App; diff --git a/packages/xstate/src/context/UserMachineContext.jsx b/packages/xstate/src/context/UserMachineContext.jsx new file mode 100644 index 0000000..e2d96e4 --- /dev/null +++ b/packages/xstate/src/context/UserMachineContext.jsx @@ -0,0 +1,6 @@ +import { createActorContext } from '@xstate/react'; +import userMachine from '../machines/userMachine'; + +const UserMachineContext = createActorContext(userMachine); + +export default UserMachineContext; \ No newline at end of file diff --git a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx new file mode 100644 index 0000000..c9fa9a0 --- /dev/null +++ b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import UserMachineContext from '../context/UserMachineContext'; +import { USER_STATES } from '../machines/userMachine'; + +const FALLBACK_ROUTE = './login'; + +const ProtectedRoute = ({ history, children }) => { + const userState = UserMachineContext.useSelector((state) => state.value); + + useEffect(() => { + if (userState === USER_STATES.UNAUTHENTICATED) { + history.replace(FALLBACK_ROUTE); + } + }); + + if (userState === USER_STATES.UNKNOWN) { + return null; + } + + return children; +}; + +ProtectedRoute.propTypes = { + userHook: PropTypes.func, + children: PropTypes.node.isRequired, + history: PropTypes.shape({ + replace: PropTypes.func.isRequired + }).isRequired +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx new file mode 100644 index 0000000..7784609 --- /dev/null +++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types'; +import TopBar from '@timo/common/components/TopBar'; +import UserMachineContext from '../context/UserMachineContext'; + +const TopBarWithUser = ({ history }) => { + const userData = UserMachineContext.useSelector((state) => state.context.data); + + return ( + history.push('./')} + onAvatarClick={() => history.push('./profile')} + /> + ); +}; + +TopBarWithUser.propTypes = { + history: PropTypes.object.isRequired +}; + +export default TopBarWithUser; diff --git a/packages/xstate/src/machines/userMachine.js b/packages/xstate/src/machines/userMachine.js index 93d5a12..5fc5835 100644 --- a/packages/xstate/src/machines/userMachine.js +++ b/packages/xstate/src/machines/userMachine.js @@ -1,15 +1,37 @@ import { assign, fromPromise, setup } from 'xstate'; -import { getUser } from '@timo/common/api'; +import { getUser, login, register, logout, updatePassword, updateUser } from '@timo/common/api'; -const USER_STATES = { +export const USER_STATES = { UNKNOWN: 'unknown', AUTHENTICATED: 'authenticated', - UNAUTHENTICATED: 'unauthenticated' + UNAUTHENTICATED: 'unauthenticated', + LOGGING_IN: 'logging_in', + LOGGING_OUT: 'logging_out', + REGISTERING: 'registering' +}; + +export const USER_AUTHENTICATED_STATES = { + IDLE: 'idle', + CHANGING_PASSWORD: 'changing_password', + UPDATING: 'updating' +}; + +export const USER_EVENTS = { + LOGIN: 'login', + LOGOUT: 'logout', + REGISTER: 'register', + CHANGE_PASSWORD: 'change_password', + UPDATE: 'update' }; const userMachine = setup({ actors: { - getUser: fromPromise(getUser) + getUser: fromPromise(getUser), + login: fromPromise(async ({ input }) => login(input)), + logout: fromPromise(logout), + register: fromPromise(async ({ input }) => register(input)), + changePassword: fromPromise(async ({ input }) => updatePassword(input)), + updateUser: fromPromise(async ({ input }) => updateUser(input)) } }).createMachine({ id: 'user', @@ -25,22 +47,145 @@ const userMachine = setup({ onDone: { target: USER_STATES.AUTHENTICATED, actions: assign({ - data: (_, event) => event.data + data: ({ event }) => event.data + }) + }, + onError: { + target: USER_STATES.UNAUTHENTICATED + } + } + }, + [USER_STATES.AUTHENTICATED]: { + initial: 'idle', + states: { + [USER_AUTHENTICATED_STATES.IDLE]: { + on: { + [USER_EVENTS.CHANGE_PASSWORD]: { + target: USER_AUTHENTICATED_STATES.CHANGING_PASSWORD + }, + [USER_EVENTS.UPDATE]: { + target: USER_AUTHENTICATED_STATES.UPDATING + }, + [USER_EVENTS.LOGOUT]: { + target: USER_STATES.LOGGING_OUT + } + } + }, + [USER_AUTHENTICATED_STATES.CHANGING_PASSWORD]: { + invoke: { + src: 'changePassword', + onDone: { + target: USER_AUTHENTICATED_STATES.IDLE, + actions: assign({ + error: null + }) + }, + onError: { + target: USER_AUTHENTICATED_STATES.IDLE, + actions: assign({ + error: ({ event }) => ({ + src: USER_AUTHENTICATED_STATES.CHANGING_PASSWORD, + message: event.error.message + }) + }) + } + } + }, + [USER_AUTHENTICATED_STATES.UPDATING]: { + invoke: { + src: 'updateUser', + onDone: { + target: USER_AUTHENTICATED_STATES.IDLE, + actions: assign({ + error: null + }) + }, + onError: { + target: USER_AUTHENTICATED_STATES.IDLE, + actions: assign({ + error: ({ event }) => ({ + src: USER_AUTHENTICATED_STATES.UPDATING, + message: event.error.message + }) + }) + } + } + } + } + }, + [USER_STATES.UNAUTHENTICATED]: { + on: { + [USER_EVENTS.LOGIN]: { + target: USER_STATES.LOGGING_IN + }, + [USER_EVENTS.REGISTER]: { + target: USER_STATES.REGISTERING + } + } + }, + [USER_STATES.LOGGING_IN]: { + invoke: { + src: 'login', + input: ({ event }) => ({ username: event.username, password: event.password }), + onDone: { + target: USER_STATES.AUTHENTICATED, + actions: assign({ + data: ({ event }) => event.data, + error: null }) }, onError: { target: USER_STATES.UNAUTHENTICATED, actions: assign({ - error: (_, event) => event.error + data: null, + error: ({ event }) => ({ + src: USER_STATES.LOGGING_IN, + message: event.error.message + }) }) } } }, - [USER_STATES.AUTHENTICATED]: { - type: 'final' + [USER_STATES.LOGGING_OUT]: { + invoke: { + src: 'logout', + onDone: { + target: USER_STATES.UNAUTHENTICATED, + actions: assign({ + data: null, + error: null + }) + }, + onError: { + target: USER_STATES.AUTHENTICATED, + actions: assign({ + error: ({ event }) => ({ + src: USER_STATES.LOGGING_OUT, + message: event.error.message + }) + }) + } + } }, - [USER_STATES.UNAUTHENTICATED]: { - type: 'final' + [USER_STATES.REGISTERING]: { + invoke: { + src: 'register', + input: ({ event }) => ({ username: event.username, password: event.password }), + onDone: { + target: USER_STATES.AUTHENTICATED, + actions: assign({ + data: ({ event }) => event.data, + error: null + }) + }, + onError: { + target: USER_STATES.UNAUTHENTICATED, + actions: assign({ + data: null, + error: ({ event }) => event.error.message + }) + } + } } } }); diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx index 9113a93..1b3f5d3 100644 --- a/packages/xstate/src/routes/Login/Login.jsx +++ b/packages/xstate/src/routes/Login/Login.jsx @@ -1,22 +1,26 @@ -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { login, register } from '@timo/common/api'; -import useUser from '@timo/common/hooks/useUser'; import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import Title from '@timo/common/components/Title'; import StatusMessage from '@timo/common/components/StatusMessage'; import styles from './Login.module.css'; +import { USER_EVENTS, USER_STATES } from '../../machines/userMachine'; +import UserMachineContext from '../../context/UserMachineContext'; const Login = ({ history }) => { - const user = useUser(); - const [statusMessage, setStatusMessage] = useState(null); + const userState = UserMachineContext.useSelector((state) => state.value); + const error = UserMachineContext.useSelector((state) => state.context.error); + const userMachine = UserMachineContext.useActorRef(); + + const isLoading = userState === USER_STATES.REGISTERING || userState === USER_STATES.LOGGING_IN; + const statusMessage = isLoading ? 'Loading...' : error; useEffect(() => { - if (user.status === 'authenticated') { + if (userState === USER_STATES.AUTHENTICATED) { history.replace('./'); } - }, [history, user]); + }, [history, userState]); const handleFormSubmit = (e) => { e.preventDefault(); @@ -26,23 +30,19 @@ const Login = ({ history }) => { const username = formData.get('username'); const password = formData.get('password'); - setStatusMessage('Loading...'); - if (action == 'login') { - login({ username, password }).then((response) => { - user.setAuthenticatedUser(response); - history.replace('./'); - }).catch((error) => { - setStatusMessage(error.message); + userMachine.send({ + type: USER_EVENTS.LOGIN, + username, + password }); } if (action == 'register') { - register({ username, password }).then((response) => { - user.setAuthenticatedUser(response); - history.replace('./'); - }).catch((error) => { - setStatusMessage(error.message); + userMachine.send({ + type: USER_EVENTS.REGISTER, + username, + password }); } }; diff --git a/packages/xstate/src/routes/Profile/Profile.jsx b/packages/xstate/src/routes/Profile/Profile.jsx index 747d93e..65b2760 100644 --- a/packages/xstate/src/routes/Profile/Profile.jsx +++ b/packages/xstate/src/routes/Profile/Profile.jsx @@ -1,17 +1,17 @@ import Title from '@timo/common/components/Title'; import Button, { ButtonVariants } from '@timo/common/components/Button'; -import useUser from '@timo/common/hooks/useUser'; -import { logout } from '@timo/common/api'; import styles from './Profile.module.css'; import ChangePassword from './sections/ChangePassword'; import CustomizeUser from './sections/CustomizeUser'; +import UserMachineContext from '../../context/UserMachineContext'; +import { USER_EVENTS } from '../../machines/userMachine'; const Profile = () => { - const user = useUser(); + const userMachine = UserMachineContext.useActorRef(); const handleLogoutClick = () => { - logout().then(() => { - user.clearUser(); + userMachine.send({ + type: USER_EVENTS.LOGOUT }); }; diff --git a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx index 54b25aa..26d9b3e 100644 --- a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx +++ b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx @@ -1,13 +1,13 @@ import { useState } from 'react'; import Input from '@timo/common/components/Input'; -import useUser from '@timo/common/hooks/useUser'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; import { updatePassword } from '@timo/common/api'; import styles from '../Profile.module.css'; +import UserMachineContext from '../../../context/UserMachineContext'; const ChangePassword = () => { - const user = useUser(); + const username = UserMachineContext.useSelector((state) => state.context.data?.username); const [passwordStatus, setPasswordStatus] = useState(null); const handlePasswordFormSubmit = (e) => { @@ -20,7 +20,7 @@ const ChangePassword = () => { setPasswordStatus('Loading...'); updatePassword({ - username: user?.data?.username, + username, password, newPassword }).then(() => { diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index 386e541..613d35e 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -2,14 +2,14 @@ import { useEffect, useState } from 'react'; import Avatar from '@timo/common/components/Avatar'; import RadioGroup from '@timo/common/components/RadioGroup'; import Input from '@timo/common/components/Input'; -import useUser from '@timo/common/hooks/useUser'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; import { updateUser } from '@timo/common/api'; import styles from '../Profile.module.css'; +import UserMachineContext from '../../../context/UserMachineContext'; const CustomizeUser = () => { - const user = useUser(); + const userData = UserMachineContext.useSelector((state) => state.context.data); const [customizeStatus, setCustomizeStatus] = useState(null); const [avatar, setAvatar] = useState({ character: undefined, @@ -17,13 +17,13 @@ const CustomizeUser = () => { }); useEffect(() => { - if (user?.data) { + if (userData) { setAvatar({ - character: user.data.avatar_character, - background: user.data.avatar_background + character: userData?.avatar_character, + background: userData?.avatar_background }); } - }, [user]); + }, [userData]); const handleCustomizeFormSubmit = (e) => { e.preventDefault(); @@ -35,14 +35,13 @@ const CustomizeUser = () => { setCustomizeStatus('Loading...'); updateUser({ - id: user?.data?.id, + id: userData?.id, username, avatar_character: avatarCharacter, avatar_background: avatarBackground }).then(() => { setCustomizeStatus('Profile updated'); - // Clear the user in context and force refetch - user.clearUser(); + // TODO: Move to updating the user in the state machine }).catch((error) => { setCustomizeStatus(error.message); }); @@ -80,7 +79,7 @@ const CustomizeUser = () => { { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' } ]} - defaultValue={user?.data?.avatar_background} + defaultValue={userData?.avatar_background} onChange={handleAvatarBackgroundChange} /> { type="text" maxLength={1} pattern="[A-Za-z]" - defaultValue={user?.data?.avatar_character} + defaultValue={userData?.avatar_character} onChange={handleAvatarCharacterChange} labelVisible required @@ -98,7 +97,7 @@ const CustomizeUser = () => { name="username" label="Username" type="text" - defaultValue={user?.data?.username} + defaultValue={userData?.username} labelVisible required /> From 3b1d6c233b8b44b244562cf8e4b5467de3ca9b26 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 18 Oct 2024 15:41:28 +0200 Subject: [PATCH 03/26] cleaned up userMachine --- packages/xstate/src/machines/userMachine.js | 69 +++++++------------ packages/xstate/src/routes/Login/Login.jsx | 2 +- .../routes/Profile/sections/CustomizeUser.jsx | 6 +- 3 files changed, 30 insertions(+), 47 deletions(-) diff --git a/packages/xstate/src/machines/userMachine.js b/packages/xstate/src/machines/userMachine.js index 5fc5835..6eeb7fb 100644 --- a/packages/xstate/src/machines/userMachine.js +++ b/packages/xstate/src/machines/userMachine.js @@ -1,10 +1,11 @@ import { assign, fromPromise, setup } from 'xstate'; -import { getUser, login, register, logout, updatePassword, updateUser } from '@timo/common/api'; +import { getUser, login, register, logout } from '@timo/common/api'; export const USER_STATES = { UNKNOWN: 'unknown', AUTHENTICATED: 'authenticated', UNAUTHENTICATED: 'unauthenticated', + REFRESHING: 'refreshing', LOGGING_IN: 'logging_in', LOGGING_OUT: 'logging_out', REGISTERING: 'registering' @@ -12,16 +13,14 @@ export const USER_STATES = { export const USER_AUTHENTICATED_STATES = { IDLE: 'idle', - CHANGING_PASSWORD: 'changing_password', - UPDATING: 'updating' + REFRESHING: 'refreshing' }; export const USER_EVENTS = { LOGIN: 'login', LOGOUT: 'logout', REGISTER: 'register', - CHANGE_PASSWORD: 'change_password', - UPDATE: 'update' + REFRESH: 'refresh' }; const userMachine = setup({ @@ -29,9 +28,7 @@ const userMachine = setup({ getUser: fromPromise(getUser), login: fromPromise(async ({ input }) => login(input)), logout: fromPromise(logout), - register: fromPromise(async ({ input }) => register(input)), - changePassword: fromPromise(async ({ input }) => updatePassword(input)), - updateUser: fromPromise(async ({ input }) => updateUser(input)) + register: fromPromise(async ({ input }) => register(input)) } }).createMachine({ id: 'user', @@ -47,7 +44,7 @@ const userMachine = setup({ onDone: { target: USER_STATES.AUTHENTICATED, actions: assign({ - data: ({ event }) => event.data + data: ({ event }) => event.output }) }, onError: { @@ -56,61 +53,40 @@ const userMachine = setup({ } }, [USER_STATES.AUTHENTICATED]: { - initial: 'idle', + initial: USER_AUTHENTICATED_STATES.IDLE, states: { [USER_AUTHENTICATED_STATES.IDLE]: { on: { - [USER_EVENTS.CHANGE_PASSWORD]: { - target: USER_AUTHENTICATED_STATES.CHANGING_PASSWORD - }, - [USER_EVENTS.UPDATE]: { - target: USER_AUTHENTICATED_STATES.UPDATING - }, - [USER_EVENTS.LOGOUT]: { - target: USER_STATES.LOGGING_OUT + [USER_EVENTS.REFRESH]: { + target: USER_AUTHENTICATED_STATES.REFRESHING } } }, - [USER_AUTHENTICATED_STATES.CHANGING_PASSWORD]: { + [USER_AUTHENTICATED_STATES.REFRESHING]: { invoke: { - src: 'changePassword', + src: 'getUser', onDone: { target: USER_AUTHENTICATED_STATES.IDLE, actions: assign({ - error: null + data: ({ event }) => event.output }) }, onError: { target: USER_AUTHENTICATED_STATES.IDLE, actions: assign({ error: ({ event }) => ({ - src: USER_AUTHENTICATED_STATES.CHANGING_PASSWORD, - message: event.error.message - }) - }) - } - } - }, - [USER_AUTHENTICATED_STATES.UPDATING]: { - invoke: { - src: 'updateUser', - onDone: { - target: USER_AUTHENTICATED_STATES.IDLE, - actions: assign({ - error: null - }) - }, - onError: { - target: USER_AUTHENTICATED_STATES.IDLE, - actions: assign({ - error: ({ event }) => ({ - src: USER_AUTHENTICATED_STATES.UPDATING, + src: USER_EVENTS.REFRESH, message: event.error.message }) }) } } } + }, + on: { + [USER_EVENTS.LOGOUT]: { + target: USER_STATES.LOGGING_OUT + } } }, [USER_STATES.UNAUTHENTICATED]: { @@ -130,7 +106,7 @@ const userMachine = setup({ onDone: { target: USER_STATES.AUTHENTICATED, actions: assign({ - data: ({ event }) => event.data, + data: ({ event }) => event.output, error: null }) }, @@ -174,7 +150,7 @@ const userMachine = setup({ onDone: { target: USER_STATES.AUTHENTICATED, actions: assign({ - data: ({ event }) => event.data, + data: ({ event }) => event.output, error: null }) }, @@ -182,7 +158,10 @@ const userMachine = setup({ target: USER_STATES.UNAUTHENTICATED, actions: assign({ data: null, - error: ({ event }) => event.error.message + error: ({ event }) => ({ + src: USER_STATES.REGISTERING, + message: event.error.message + }) }) } } diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx index 1b3f5d3..863ffff 100644 --- a/packages/xstate/src/routes/Login/Login.jsx +++ b/packages/xstate/src/routes/Login/Login.jsx @@ -17,7 +17,7 @@ const Login = ({ history }) => { const statusMessage = isLoading ? 'Loading...' : error; useEffect(() => { - if (userState === USER_STATES.AUTHENTICATED) { + if (userState?.[USER_STATES.AUTHENTICATED]) { history.replace('./'); } }, [history, userState]); diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index 613d35e..e63084c 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -7,9 +7,11 @@ import Button from '@timo/common/components/Button'; import { updateUser } from '@timo/common/api'; import styles from '../Profile.module.css'; import UserMachineContext from '../../../context/UserMachineContext'; +import { USER_EVENTS } from '../../../machines/userMachine'; const CustomizeUser = () => { const userData = UserMachineContext.useSelector((state) => state.context.data); + const userMachine = UserMachineContext.useActorRef(); const [customizeStatus, setCustomizeStatus] = useState(null); const [avatar, setAvatar] = useState({ character: undefined, @@ -41,7 +43,9 @@ const CustomizeUser = () => { avatar_background: avatarBackground }).then(() => { setCustomizeStatus('Profile updated'); - // TODO: Move to updating the user in the state machine + userMachine.send({ + type: USER_EVENTS.REFRESH + }); }).catch((error) => { setCustomizeStatus(error.message); }); From 9187e80d4f4cc9febe3dac1970125e36fff8d7d3 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 18 Oct 2024 20:36:47 +0200 Subject: [PATCH 04/26] borked implementation of customizeUserMachine --- .../src/machines/customizeUserMachine.js | 109 ++++++++++++++++++ packages/xstate/src/machines/userMachine.js | 15 ++- .../routes/Profile/sections/CustomizeUser.jsx | 71 +++++------- 3 files changed, 148 insertions(+), 47 deletions(-) create mode 100644 packages/xstate/src/machines/customizeUserMachine.js diff --git a/packages/xstate/src/machines/customizeUserMachine.js b/packages/xstate/src/machines/customizeUserMachine.js new file mode 100644 index 0000000..2211b7c --- /dev/null +++ b/packages/xstate/src/machines/customizeUserMachine.js @@ -0,0 +1,109 @@ +import { setup, assign, fromPromise, sendTo } from 'xstate'; +import { updateUser } from '@timo/common/api'; +import userMachine, { USER_EVENTS } from './userMachine'; + +export const CUSTOMIZE_USER_STATES = { + IDLE: 'idle', + SAVING: 'saving' +}; + +export const CUSTOMIZE_USER_EVENTS = { + CHANGE_AVATAR_CHARACTER: 'changeAvatarCharacter', + CHANGE_AVATAR_BACKGROUND: 'changeAvatarBackground', + SAVE: 'save' +}; + +const customizeUserMachine = setup({ + actors: { + userMachine, + updateUser: fromPromise(async ({ input }) => updateUser(input)) + } +}).createMachine({ + id: 'customizeUser', + initial: CUSTOMIZE_USER_STATES.IDLE, + context: { + userId: null, + avatar: { + character: null, + background: null + }, + statusMessage: null + }, + states: { + [CUSTOMIZE_USER_STATES.IDLE]: { + entry: [ + sendTo('userMachine', ({ self }) => ({ + sender: self, + type: USER_EVENTS.GET + })) + ], + on: { + [USER_EVENTS.GET_RESPONSE]: { + actions: assign({ + userId: ({ event }) => event.data.id, + avatar: ({ event }) => ({ + character: event.data.avatar_character, + background: event.data.avatar_background + }) + }) + }, + [CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_CHARACTER]: { + actions: assign({ + avatar: ({ event, context }) => ({ + ...context.avatar, + character: event.character + }) + }) + }, + [CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_BACKGROUND]: { + actions: assign({ + avatar: ({ event, context }) => ({ + ...context.avatar, + background: event.background + }) + }) + }, + [CUSTOMIZE_USER_EVENTS.SAVE]: { + target: CUSTOMIZE_USER_STATES.SAVING + } + } + }, + [CUSTOMIZE_USER_STATES.SAVING]: { + entry: [ + assign({ + statusMessage: 'Loading...' + }) + ], + invoke: [ + { + src: 'updateUser', + input: ({ event, context }) => ({ + id: context.userId, + username: event.username, + avatar_character: event.avatarCharacter, + avatar_background: event.avatarBackground + }), + onDone: { + target: CUSTOMIZE_USER_STATES.IDLE, + actions: [ + assign({ + statusMessage: 'Profile updated' + }), + sendTo('userMachine', { + type: USER_EVENTS.REFRESH + }) + ] + }, + onError: { + target: CUSTOMIZE_USER_STATES.IDLE, + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + ] + } + } +}); + +export default customizeUserMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/userMachine.js b/packages/xstate/src/machines/userMachine.js index 6eeb7fb..bea7a32 100644 --- a/packages/xstate/src/machines/userMachine.js +++ b/packages/xstate/src/machines/userMachine.js @@ -1,4 +1,4 @@ -import { assign, fromPromise, setup } from 'xstate'; +import { assign, fromPromise, sendTo, setup } from 'xstate'; import { getUser, login, register, logout } from '@timo/common/api'; export const USER_STATES = { @@ -20,7 +20,9 @@ export const USER_EVENTS = { LOGIN: 'login', LOGOUT: 'logout', REGISTER: 'register', - REFRESH: 'refresh' + REFRESH: 'refresh', + GET: 'get', + GET_RESPONSE: 'get_response' }; const userMachine = setup({ @@ -86,6 +88,15 @@ const userMachine = setup({ on: { [USER_EVENTS.LOGOUT]: { target: USER_STATES.LOGGING_OUT + }, + [USER_EVENTS.GET]: { + actions: sendTo( + ({ event }) => event.sender, + { + type: USER_EVENTS.GET_RESPONSE, + data: ({ context }) => context.data + } + ) } } }, diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index e63084c..b6257e9 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -1,31 +1,35 @@ -import { useEffect, useState } from 'react'; import Avatar from '@timo/common/components/Avatar'; import RadioGroup from '@timo/common/components/RadioGroup'; import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; -import { updateUser } from '@timo/common/api'; import styles from '../Profile.module.css'; import UserMachineContext from '../../../context/UserMachineContext'; -import { USER_EVENTS } from '../../../machines/userMachine'; +import { useActor } from '@xstate/react'; +import customizeUserMachine, { CUSTOMIZE_USER_EVENTS } from '../../../machines/customizeUserMachine'; const CustomizeUser = () => { - const userData = UserMachineContext.useSelector((state) => state.context.data); const userMachine = UserMachineContext.useActorRef(); - const [customizeStatus, setCustomizeStatus] = useState(null); - const [avatar, setAvatar] = useState({ - character: undefined, - background: undefined + const updatedMachine = customizeUserMachine.provide({ + actors: { + userMachine + } }); + const [state, send] = useActor(updatedMachine); - useEffect(() => { - if (userData) { - setAvatar({ - character: userData?.avatar_character, - background: userData?.avatar_background - }); - } - }, [userData]); + const handleAvatarBackgroundChange = (e) => { + send({ + type: CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_BACKGROUND, + background: e.target.value + }); + }; + + const handleAvatarCharacterChange = (e) => { + send({ + type: CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_CHARACTER, + character: e.target.value + }); + }; const handleCustomizeFormSubmit = (e) => { e.preventDefault(); @@ -34,34 +38,11 @@ const CustomizeUser = () => { const avatarCharacter = formData.get('avatar-character'); const avatarBackground = formData.get('avatar-background'); - setCustomizeStatus('Loading...'); - - updateUser({ - id: userData?.id, + send({ + type: CUSTOMIZE_USER_EVENTS.SAVE, username, - avatar_character: avatarCharacter, - avatar_background: avatarBackground - }).then(() => { - setCustomizeStatus('Profile updated'); - userMachine.send({ - type: USER_EVENTS.REFRESH - }); - }).catch((error) => { - setCustomizeStatus(error.message); - }); - }; - - const handleAvatarBackgroundChange = (e) => { - setAvatar({ - ...avatar, - background: e.target.value - }); - }; - - const handleAvatarCharacterChange = (e) => { - setAvatar({ - ...avatar, - character: e.target.value + avatarCharacter, + avatarBackground }); }; @@ -69,8 +50,8 @@ const CustomizeUser = () => { <>
From 212ef667fe3c506d83ef7646dc439e6103f75b7e Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Wed, 23 Oct 2024 19:52:38 +0200 Subject: [PATCH 05/26] initial implementation of hierarchial machine --- packages/xstate/src/hooks/useMachine.js | 9 + packages/xstate/src/hooks/useMachineState.js | 10 + ...stomizeUserMachine.js => customizeUser.js} | 22 +-- packages/xstate/src/machines/root.js | 48 +++++ packages/xstate/src/machines/user.js | 162 ++++++++++++++++ packages/xstate/src/machines/userMachine.js | 183 ------------------ 6 files changed, 230 insertions(+), 204 deletions(-) create mode 100644 packages/xstate/src/hooks/useMachine.js create mode 100644 packages/xstate/src/hooks/useMachineState.js rename packages/xstate/src/machines/{customizeUserMachine.js => customizeUser.js} (76%) create mode 100644 packages/xstate/src/machines/root.js create mode 100644 packages/xstate/src/machines/user.js delete mode 100644 packages/xstate/src/machines/userMachine.js diff --git a/packages/xstate/src/hooks/useMachine.js b/packages/xstate/src/hooks/useMachine.js new file mode 100644 index 0000000..551653f --- /dev/null +++ b/packages/xstate/src/hooks/useMachine.js @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { MachineContext } from '../context/MachineContext'; + +const useMachineState = (systemId) => { + const machine = useContext(MachineContext); + return machine.system.get(systemId); +}; + +export default useMachineState; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useMachineState.js b/packages/xstate/src/hooks/useMachineState.js new file mode 100644 index 0000000..7eba801 --- /dev/null +++ b/packages/xstate/src/hooks/useMachineState.js @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { useSelector } from '@xstate/react'; +import { MachineContext } from '../context/MachineContext'; + +const useMachineState = (systemId, selector) => { + const machine = useContext(MachineContext); + return useSelector(machine.system.get(systemId), selector); +}; + +export default useMachineState; \ No newline at end of file diff --git a/packages/xstate/src/machines/customizeUserMachine.js b/packages/xstate/src/machines/customizeUser.js similarity index 76% rename from packages/xstate/src/machines/customizeUserMachine.js rename to packages/xstate/src/machines/customizeUser.js index 2211b7c..f0275cd 100644 --- a/packages/xstate/src/machines/customizeUserMachine.js +++ b/packages/xstate/src/machines/customizeUser.js @@ -1,6 +1,5 @@ -import { setup, assign, fromPromise, sendTo } from 'xstate'; +import { setup, assign, fromPromise } from 'xstate'; import { updateUser } from '@timo/common/api'; -import userMachine, { USER_EVENTS } from './userMachine'; export const CUSTOMIZE_USER_STATES = { IDLE: 'idle', @@ -15,7 +14,6 @@ export const CUSTOMIZE_USER_EVENTS = { const customizeUserMachine = setup({ actors: { - userMachine, updateUser: fromPromise(async ({ input }) => updateUser(input)) } }).createMachine({ @@ -31,22 +29,7 @@ const customizeUserMachine = setup({ }, states: { [CUSTOMIZE_USER_STATES.IDLE]: { - entry: [ - sendTo('userMachine', ({ self }) => ({ - sender: self, - type: USER_EVENTS.GET - })) - ], on: { - [USER_EVENTS.GET_RESPONSE]: { - actions: assign({ - userId: ({ event }) => event.data.id, - avatar: ({ event }) => ({ - character: event.data.avatar_character, - background: event.data.avatar_background - }) - }) - }, [CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_CHARACTER]: { actions: assign({ avatar: ({ event, context }) => ({ @@ -88,9 +71,6 @@ const customizeUserMachine = setup({ actions: [ assign({ statusMessage: 'Profile updated' - }), - sendTo('userMachine', { - type: USER_EVENTS.REFRESH }) ] }, diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js new file mode 100644 index 0000000..5b10ac0 --- /dev/null +++ b/packages/xstate/src/machines/root.js @@ -0,0 +1,48 @@ +import { setup, spawnChild, sendTo } from 'xstate'; +import user, { USER_EVENTS } from './user'; +import customizeUser from './customizeUser'; + +const rootMachine = setup({ + actions: { + getUser: sendTo( + ({ system }) => system.getActor('user'), + { type: USER_EVENTS.GET } + ) + } +}).createMachine({ + systemId: 'root', + entry: [ + spawnChild(user, { systemId: 'user' }), + spawnChild(customizeUser, { systemId: 'customizeUser' }) + ], + initial: 'unknown', + states: { + 'unknown': { + entry: 'getUser', + on: { + authenticate: { + target: 'authenticated' + }, + unauthenticate: { + target: 'unauthenticated' + } + } + }, + 'authenticated': { + on: { + unauthenticate: { + target: 'unauthenticated' + } + } + }, + 'unauthenticated': { + on: { + authenticate: { + target: 'authenticated' + } + } + } + } +}); + +export default rootMachine; diff --git a/packages/xstate/src/machines/user.js b/packages/xstate/src/machines/user.js new file mode 100644 index 0000000..a775ae5 --- /dev/null +++ b/packages/xstate/src/machines/user.js @@ -0,0 +1,162 @@ +import { assign, fromPromise, sendTo, setup } from 'xstate'; +import { getUser, login, register, logout } from '@timo/common/api'; +import { ROOT_EVENTS } from './root'; + +const userMachine = setup({ + actors: { + getUser: fromPromise(getUser), + login: fromPromise(async ({ input }) => login(input)), + logout: fromPromise(logout), + register: fromPromise(async ({ input }) => register(input)) + }, + actions: { + authenticate: sendTo( + ({ system }) => system.getActor('root'), + ({ event }) => ({ type: ROOT_EVENTS.AUTHENTICATE, userData: event.userData }) + ), + unauthenticate: sendTo( + ({ system }) => system.getActor('root'), + { type: ROOT_EVENTS.UNAUTHENTICATE } + ) + } +}).createMachine({ + initial: 'idle', + context: { + error: {} + }, + states: { + 'idle': { + on: { + 'get': { + target: 'getting' + }, + 'login': { + target: 'logging-in' + }, + 'logout': { + target: 'logging-out' + }, + 'register': { + target: 'registering' + } + } + }, + 'getting': { + entry: [ + assign({ + error: ({ context }) => ({ + ...context.error, + 'get': null + }) + }) + ], + invoke: { + src: 'getUser', + onDone: { + target: 'idle', + actions: { + type: 'authenticate', + userData: ({ event }) => event.output + } + }, + onError: { + target: 'idle', + actions: assign({ + error: ({ context, event }) => ({ + ...context.error, + 'get': event.error.message + }) + }) + } + } + }, + 'logging-in': { + entry: [ + assign({ + error: ({ context }) => ({ + ...context.error, + 'login': null + }) + }) + ], + invoke: { + src: 'login', + input: ({ event }) => ({ username: event.username, password: event.password }), + onDone: { + target: 'idle', + actions: { + type: 'authenticate', + userData: ({ event }) => event.output + } + }, + onError: { + target: 'idle', + actions: assign({ + error: ({ context, event }) => ({ + ...context.error, + 'login': event.error.message + }) + }) + } + } + }, + 'logging-out': { + entry: [ + assign({ + error: ({ context }) => ({ + ...context.error, + 'logout': null + }) + }) + ], + invoke: { + src: 'logout', + onDone: { + target: 'idle', + actions: 'unauthenticate' + }, + onError: { + target: 'idle', + actions: assign({ + error: ({ context, event }) => ({ + ...context.error, + 'logout': event.error.message + }) + }) + } + } + }, + 'registering': { + entry: [ + assign({ + error: ({ context }) => ({ + ...context.error, + 'register': null + }) + }) + ], + invoke: { + src: 'register', + input: ({ event }) => ({ username: event.username, password: event.password }), + onDone: { + target: 'idle', + actions: { + type: 'authenticate', + userData: ({ event }) => event.output + } + }, + onError: { + target: 'idle', + actions: assign({ + error: ({ context, event }) => ({ + ...context.error, + 'register': event.error.message + }) + }) + } + } + } + } +}); + +export default userMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/userMachine.js b/packages/xstate/src/machines/userMachine.js deleted file mode 100644 index bea7a32..0000000 --- a/packages/xstate/src/machines/userMachine.js +++ /dev/null @@ -1,183 +0,0 @@ -import { assign, fromPromise, sendTo, setup } from 'xstate'; -import { getUser, login, register, logout } from '@timo/common/api'; - -export const USER_STATES = { - UNKNOWN: 'unknown', - AUTHENTICATED: 'authenticated', - UNAUTHENTICATED: 'unauthenticated', - REFRESHING: 'refreshing', - LOGGING_IN: 'logging_in', - LOGGING_OUT: 'logging_out', - REGISTERING: 'registering' -}; - -export const USER_AUTHENTICATED_STATES = { - IDLE: 'idle', - REFRESHING: 'refreshing' -}; - -export const USER_EVENTS = { - LOGIN: 'login', - LOGOUT: 'logout', - REGISTER: 'register', - REFRESH: 'refresh', - GET: 'get', - GET_RESPONSE: 'get_response' -}; - -const userMachine = setup({ - actors: { - getUser: fromPromise(getUser), - login: fromPromise(async ({ input }) => login(input)), - logout: fromPromise(logout), - register: fromPromise(async ({ input }) => register(input)) - } -}).createMachine({ - id: 'user', - initial: 'unknown', - context: { - data: null, - error: null - }, - states: { - [USER_STATES.UNKNOWN]: { - invoke: { - src: 'getUser', - onDone: { - target: USER_STATES.AUTHENTICATED, - actions: assign({ - data: ({ event }) => event.output - }) - }, - onError: { - target: USER_STATES.UNAUTHENTICATED - } - } - }, - [USER_STATES.AUTHENTICATED]: { - initial: USER_AUTHENTICATED_STATES.IDLE, - states: { - [USER_AUTHENTICATED_STATES.IDLE]: { - on: { - [USER_EVENTS.REFRESH]: { - target: USER_AUTHENTICATED_STATES.REFRESHING - } - } - }, - [USER_AUTHENTICATED_STATES.REFRESHING]: { - invoke: { - src: 'getUser', - onDone: { - target: USER_AUTHENTICATED_STATES.IDLE, - actions: assign({ - data: ({ event }) => event.output - }) - }, - onError: { - target: USER_AUTHENTICATED_STATES.IDLE, - actions: assign({ - error: ({ event }) => ({ - src: USER_EVENTS.REFRESH, - message: event.error.message - }) - }) - } - } - } - }, - on: { - [USER_EVENTS.LOGOUT]: { - target: USER_STATES.LOGGING_OUT - }, - [USER_EVENTS.GET]: { - actions: sendTo( - ({ event }) => event.sender, - { - type: USER_EVENTS.GET_RESPONSE, - data: ({ context }) => context.data - } - ) - } - } - }, - [USER_STATES.UNAUTHENTICATED]: { - on: { - [USER_EVENTS.LOGIN]: { - target: USER_STATES.LOGGING_IN - }, - [USER_EVENTS.REGISTER]: { - target: USER_STATES.REGISTERING - } - } - }, - [USER_STATES.LOGGING_IN]: { - invoke: { - src: 'login', - input: ({ event }) => ({ username: event.username, password: event.password }), - onDone: { - target: USER_STATES.AUTHENTICATED, - actions: assign({ - data: ({ event }) => event.output, - error: null - }) - }, - onError: { - target: USER_STATES.UNAUTHENTICATED, - actions: assign({ - data: null, - error: ({ event }) => ({ - src: USER_STATES.LOGGING_IN, - message: event.error.message - }) - }) - } - } - }, - [USER_STATES.LOGGING_OUT]: { - invoke: { - src: 'logout', - onDone: { - target: USER_STATES.UNAUTHENTICATED, - actions: assign({ - data: null, - error: null - }) - }, - onError: { - target: USER_STATES.AUTHENTICATED, - actions: assign({ - error: ({ event }) => ({ - src: USER_STATES.LOGGING_OUT, - message: event.error.message - }) - }) - } - } - }, - [USER_STATES.REGISTERING]: { - invoke: { - src: 'register', - input: ({ event }) => ({ username: event.username, password: event.password }), - onDone: { - target: USER_STATES.AUTHENTICATED, - actions: assign({ - data: ({ event }) => event.output, - error: null - }) - }, - onError: { - target: USER_STATES.UNAUTHENTICATED, - actions: assign({ - data: null, - error: ({ event }) => ({ - src: USER_STATES.REGISTERING, - message: event.error.message - }) - }) - } - } - } - } -}); - -export default userMachine; \ No newline at end of file From acbdab94aa2ab9986cbe3194b6a215ee423e5d06 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Wed, 23 Oct 2024 21:02:53 +0200 Subject: [PATCH 06/26] switched to machine per route and refactored login to use new machine --- packages/xstate/src/App.jsx | 6 +- .../xstate/src/context/MachineContext.jsx | 23 +++ .../xstate/src/context/UserMachineContext.jsx | 6 - .../contextualComponents/ProtectedRoute.jsx | 9 +- .../contextualComponents/TopBarWithUser.jsx | 4 +- packages/xstate/src/hooks/useMachine.js | 4 +- packages/xstate/src/hooks/useMachineState.js | 7 +- packages/xstate/src/machines/login.js | 72 ++++++++ packages/xstate/src/machines/root.js | 57 ++++-- packages/xstate/src/machines/user.js | 162 ------------------ packages/xstate/src/routes/Login/Login.jsx | 26 ++- 11 files changed, 163 insertions(+), 213 deletions(-) create mode 100644 packages/xstate/src/context/MachineContext.jsx delete mode 100644 packages/xstate/src/context/UserMachineContext.jsx create mode 100644 packages/xstate/src/machines/login.js delete mode 100644 packages/xstate/src/machines/user.js diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx index 9b1a696..c2e3efb 100644 --- a/packages/xstate/src/App.jsx +++ b/packages/xstate/src/App.jsx @@ -8,7 +8,7 @@ import NewEntry from './routes/NewEntry/NewEntry'; import Profile from './routes/Profile/Profile'; import ProtectedRoute from './contextualComponents/ProtectedRoute'; import TopBarWithUser from './contextualComponents/TopBarWithUser'; -import UserMachineContext from './context/UserMachineContext'; +import MachineContextProvider from './context/MachineContext'; const routes = [ { path: '/', name: 'Entries' }, @@ -19,7 +19,7 @@ const routes = [ ]; const App = () => ( - + {(routeName, history) => { let pageComponent = null; @@ -61,7 +61,7 @@ const App = () => ( ); }} - + ); export default App; diff --git a/packages/xstate/src/context/MachineContext.jsx b/packages/xstate/src/context/MachineContext.jsx new file mode 100644 index 0000000..b25fbbd --- /dev/null +++ b/packages/xstate/src/context/MachineContext.jsx @@ -0,0 +1,23 @@ +import PropTypes from 'prop-types'; +import { createActor } from 'xstate'; +import { createContext } from 'react'; +import root from '../machines/root'; + +const rootActor = createActor(root, { systemId: 'root' }); +rootActor.start(); + +export const MachineContext = createContext(); + +const MachineContextProvider = ({ children }) => { + return ( + + {children} + + ); +}; + +MachineContextProvider.propTypes = { + children: PropTypes.node.isRequired +}; + +export default MachineContextProvider; \ No newline at end of file diff --git a/packages/xstate/src/context/UserMachineContext.jsx b/packages/xstate/src/context/UserMachineContext.jsx deleted file mode 100644 index e2d96e4..0000000 --- a/packages/xstate/src/context/UserMachineContext.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createActorContext } from '@xstate/react'; -import userMachine from '../machines/userMachine'; - -const UserMachineContext = createActorContext(userMachine); - -export default UserMachineContext; \ No newline at end of file diff --git a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx index c9fa9a0..08eb841 100644 --- a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx +++ b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx @@ -1,20 +1,19 @@ import { useEffect } from 'react'; import PropTypes from 'prop-types'; -import UserMachineContext from '../context/UserMachineContext'; -import { USER_STATES } from '../machines/userMachine'; +import useMachineState from '../hooks/useMachineState'; const FALLBACK_ROUTE = './login'; const ProtectedRoute = ({ history, children }) => { - const userState = UserMachineContext.useSelector((state) => state.value); + const authState = useMachineState('root', (state) => state.value); useEffect(() => { - if (userState === USER_STATES.UNAUTHENTICATED) { + if (authState === 'unauthenticated') { history.replace(FALLBACK_ROUTE); } }); - if (userState === USER_STATES.UNKNOWN) { + if (authState === 'unknown') { return null; } diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx index 7784609..a609967 100644 --- a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx +++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; import TopBar from '@timo/common/components/TopBar'; -import UserMachineContext from '../context/UserMachineContext'; +import useMachineState from '../hooks/useMachineState'; const TopBarWithUser = ({ history }) => { - const userData = UserMachineContext.useSelector((state) => state.context.data); + const userData = useMachineState('root', (state) => state.context.userData); return ( { +const useMachine = (systemId) => { const machine = useContext(MachineContext); return machine.system.get(systemId); }; -export default useMachineState; \ No newline at end of file +export default useMachine; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useMachineState.js b/packages/xstate/src/hooks/useMachineState.js index 7eba801..ff537c4 100644 --- a/packages/xstate/src/hooks/useMachineState.js +++ b/packages/xstate/src/hooks/useMachineState.js @@ -1,10 +1,9 @@ -import { useContext } from 'react'; import { useSelector } from '@xstate/react'; -import { MachineContext } from '../context/MachineContext'; +import useMachine from './useMachine'; const useMachineState = (systemId, selector) => { - const machine = useContext(MachineContext); - return useSelector(machine.system.get(systemId), selector); + const machine = useMachine(systemId); + return useSelector(machine, selector); }; export default useMachineState; \ No newline at end of file diff --git a/packages/xstate/src/machines/login.js b/packages/xstate/src/machines/login.js new file mode 100644 index 0000000..2397ee7 --- /dev/null +++ b/packages/xstate/src/machines/login.js @@ -0,0 +1,72 @@ +import { setup, fromPromise, assign, sendTo } from 'xstate'; +import { login, register } from '@timo/common/api'; + +const loginMachine = setup({ + actors: { + login: fromPromise(async ({ input }) => login(input)), + register: fromPromise(async ({ input }) => register(input)) + }, + actions: { + authenticate: sendTo( + ({ system }) => system.get('root'), + ({ event }) => ({ type: 'authenticate', params: event.output }) + ) + } +}).createMachine({ + context: { + statusMessage: null + }, + initial: 'idle', + states: { + 'idle': { + on: { + 'login': { + target: 'logging-in', + actions: assign({ + statusMessage: 'Logging in...' + }) + }, + 'register': { + target: 'registering', + actions: assign({ + statusMessage: 'Registering...' + }) + } + } + }, + 'logging-in': { + invoke: { + src: 'login', + input: ({ event }) => ({ username: event.username, password: event.password }), + onDone: { + target: 'idle', + actions: { type: 'authenticate' } + }, + onError: { + target: 'idle', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + }, + 'registering': { + invoke: { + src: 'register', + input: ({ event }) => ({ username: event.username, password: event.password }), + onDone: { + target: 'idle', + actions: { type: 'authenticate'} + }, + onError: { + target: 'idle', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + } + } +}); + +export default loginMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 5b10ac0..995db5b 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -1,29 +1,43 @@ -import { setup, spawnChild, sendTo } from 'xstate'; -import user, { USER_EVENTS } from './user'; -import customizeUser from './customizeUser'; +import { setup, spawnChild, sendTo, assign, fromPromise } from 'xstate'; +import { getUser } from '@timo/common/api'; +import customizeUserMachine from './customizeUser'; +import loginMachine from './login'; const rootMachine = setup({ + actors: { + getUser: fromPromise(getUser) + }, actions: { getUser: sendTo( - ({ system }) => system.getActor('user'), - { type: USER_EVENTS.GET } + ({ system }) => system.get('user'), + { type: 'get' } ) } }).createMachine({ - systemId: 'root', entry: [ - spawnChild(user, { systemId: 'user' }), - spawnChild(customizeUser, { systemId: 'customizeUser' }) + spawnChild(loginMachine, { systemId: 'login' }), + spawnChild(customizeUserMachine, { systemId: 'customizeUser' }) ], initial: 'unknown', + context: { + userData: null + }, states: { 'unknown': { - entry: 'getUser', - on: { - authenticate: { - target: 'authenticated' + invoke: { + src: 'getUser', + onDone: { + target: 'authenticated', + actions: assign({ + userData: ({ event }) => ({ + id: event.output.id, + username: event.output.username, + avatar_character: event.output.avatar_character, + avatar_background: event.output.avatar_background + }) + }) }, - unauthenticate: { + onError: { target: 'unauthenticated' } } @@ -31,14 +45,27 @@ const rootMachine = setup({ 'authenticated': { on: { unauthenticate: { - target: 'unauthenticated' + target: 'unauthenticated', + actions: assign({ + userData: null + }) } } }, 'unauthenticated': { on: { authenticate: { - target: 'authenticated' + target: 'authenticated', + actions:[ + assign({ + userData:({ event }) => ({ + id: event.params.id, + username: event.params.username, + avatar_character: event.params.avatar_character, + avatar_background: event.params.avatar_background + }) + }) + ] } } } diff --git a/packages/xstate/src/machines/user.js b/packages/xstate/src/machines/user.js deleted file mode 100644 index a775ae5..0000000 --- a/packages/xstate/src/machines/user.js +++ /dev/null @@ -1,162 +0,0 @@ -import { assign, fromPromise, sendTo, setup } from 'xstate'; -import { getUser, login, register, logout } from '@timo/common/api'; -import { ROOT_EVENTS } from './root'; - -const userMachine = setup({ - actors: { - getUser: fromPromise(getUser), - login: fromPromise(async ({ input }) => login(input)), - logout: fromPromise(logout), - register: fromPromise(async ({ input }) => register(input)) - }, - actions: { - authenticate: sendTo( - ({ system }) => system.getActor('root'), - ({ event }) => ({ type: ROOT_EVENTS.AUTHENTICATE, userData: event.userData }) - ), - unauthenticate: sendTo( - ({ system }) => system.getActor('root'), - { type: ROOT_EVENTS.UNAUTHENTICATE } - ) - } -}).createMachine({ - initial: 'idle', - context: { - error: {} - }, - states: { - 'idle': { - on: { - 'get': { - target: 'getting' - }, - 'login': { - target: 'logging-in' - }, - 'logout': { - target: 'logging-out' - }, - 'register': { - target: 'registering' - } - } - }, - 'getting': { - entry: [ - assign({ - error: ({ context }) => ({ - ...context.error, - 'get': null - }) - }) - ], - invoke: { - src: 'getUser', - onDone: { - target: 'idle', - actions: { - type: 'authenticate', - userData: ({ event }) => event.output - } - }, - onError: { - target: 'idle', - actions: assign({ - error: ({ context, event }) => ({ - ...context.error, - 'get': event.error.message - }) - }) - } - } - }, - 'logging-in': { - entry: [ - assign({ - error: ({ context }) => ({ - ...context.error, - 'login': null - }) - }) - ], - invoke: { - src: 'login', - input: ({ event }) => ({ username: event.username, password: event.password }), - onDone: { - target: 'idle', - actions: { - type: 'authenticate', - userData: ({ event }) => event.output - } - }, - onError: { - target: 'idle', - actions: assign({ - error: ({ context, event }) => ({ - ...context.error, - 'login': event.error.message - }) - }) - } - } - }, - 'logging-out': { - entry: [ - assign({ - error: ({ context }) => ({ - ...context.error, - 'logout': null - }) - }) - ], - invoke: { - src: 'logout', - onDone: { - target: 'idle', - actions: 'unauthenticate' - }, - onError: { - target: 'idle', - actions: assign({ - error: ({ context, event }) => ({ - ...context.error, - 'logout': event.error.message - }) - }) - } - } - }, - 'registering': { - entry: [ - assign({ - error: ({ context }) => ({ - ...context.error, - 'register': null - }) - }) - ], - invoke: { - src: 'register', - input: ({ event }) => ({ username: event.username, password: event.password }), - onDone: { - target: 'idle', - actions: { - type: 'authenticate', - userData: ({ event }) => event.output - } - }, - onError: { - target: 'idle', - actions: assign({ - error: ({ context, event }) => ({ - ...context.error, - 'register': event.error.message - }) - }) - } - } - } - } -}); - -export default userMachine; \ No newline at end of file diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx index 863ffff..3d8d1ed 100644 --- a/packages/xstate/src/routes/Login/Login.jsx +++ b/packages/xstate/src/routes/Login/Login.jsx @@ -4,23 +4,21 @@ import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import Title from '@timo/common/components/Title'; import StatusMessage from '@timo/common/components/StatusMessage'; +import useMachine from '../../hooks/useMachine'; +import useMachineState from '../../hooks/useMachineState'; + import styles from './Login.module.css'; -import { USER_EVENTS, USER_STATES } from '../../machines/userMachine'; -import UserMachineContext from '../../context/UserMachineContext'; const Login = ({ history }) => { - const userState = UserMachineContext.useSelector((state) => state.value); - const error = UserMachineContext.useSelector((state) => state.context.error); - const userMachine = UserMachineContext.useActorRef(); - - const isLoading = userState === USER_STATES.REGISTERING || userState === USER_STATES.LOGGING_IN; - const statusMessage = isLoading ? 'Loading...' : error; + const authState = useMachineState('root', (state) => state.value); + const statusMessage = useMachineState('login', (state) => state.context.statusMessage); + const loginMachine = useMachine('login'); useEffect(() => { - if (userState?.[USER_STATES.AUTHENTICATED]) { + if (authState === 'authenticated') { history.replace('./'); } - }, [history, userState]); + }, [history, authState]); const handleFormSubmit = (e) => { e.preventDefault(); @@ -31,16 +29,16 @@ const Login = ({ history }) => { const password = formData.get('password'); if (action == 'login') { - userMachine.send({ - type: USER_EVENTS.LOGIN, + loginMachine.send({ + type: 'login', username, password }); } if (action == 'register') { - userMachine.send({ - type: USER_EVENTS.REGISTER, + loginMachine.send({ + type: 'register', username, password }); From 0e91c5af64e8dca9c0647230ac2cda78e5077b97 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 13:06:37 +0200 Subject: [PATCH 07/26] finished using customizeUser state machine --- packages/xstate/src/machines/customizeUser.js | 95 ++++++++++--------- packages/xstate/src/machines/root.js | 30 ++++-- .../routes/Profile/sections/CustomizeUser.jsx | 61 ++++++------ 3 files changed, 103 insertions(+), 83 deletions(-) diff --git a/packages/xstate/src/machines/customizeUser.js b/packages/xstate/src/machines/customizeUser.js index f0275cd..5729eaa 100644 --- a/packages/xstate/src/machines/customizeUser.js +++ b/packages/xstate/src/machines/customizeUser.js @@ -1,81 +1,90 @@ -import { setup, assign, fromPromise } from 'xstate'; +import { setup, assign, fromPromise, sendTo } from 'xstate'; import { updateUser } from '@timo/common/api'; -export const CUSTOMIZE_USER_STATES = { - IDLE: 'idle', - SAVING: 'saving' -}; - -export const CUSTOMIZE_USER_EVENTS = { - CHANGE_AVATAR_CHARACTER: 'changeAvatarCharacter', - CHANGE_AVATAR_BACKGROUND: 'changeAvatarBackground', - SAVE: 'save' -}; - const customizeUserMachine = setup({ actors: { updateUser: fromPromise(async ({ input }) => updateUser(input)) } }).createMachine({ id: 'customizeUser', - initial: CUSTOMIZE_USER_STATES.IDLE, + initial: 'idle', context: { userId: null, - avatar: { - character: null, - background: null - }, + username: null, + avatar_background: null, + avatar_character: null, statusMessage: null }, states: { - [CUSTOMIZE_USER_STATES.IDLE]: { + 'idle': { on: { - [CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_CHARACTER]: { - actions: assign({ - avatar: ({ event, context }) => ({ - ...context.avatar, - character: event.character - }) - }) + 'initialize': { + actions: assign(({ event }) => ({ + userId: event.params.userId, + username: event.params.username, + avatar_background: event.params.avatar_background, + avatar_character: event.params.avatar_character + })) }, - [CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_BACKGROUND]: { - actions: assign({ - avatar: ({ event, context }) => ({ - ...context.avatar, - background: event.background - }) - }) + 'changeAvatarCharacter': { + actions: assign(({ context, event }) => ({ + ...context, + avatar_character: event.value + })) + }, + 'changeAvatarBackground': { + actions: assign(({ context, event }) => ({ + ...context, + avatar_background: event.value + })) + }, + 'changeUsername': { + actions: assign(({ context, event }) => ({ + ...context, + username: event.value + })) }, - [CUSTOMIZE_USER_EVENTS.SAVE]: { - target: CUSTOMIZE_USER_STATES.SAVING + 'save': { + target: 'saving' } } }, - [CUSTOMIZE_USER_STATES.SAVING]: { + 'saving': { entry: [ assign({ - statusMessage: 'Loading...' + statusMessage: 'Saving...' }) ], invoke: [ { src: 'updateUser', - input: ({ event, context }) => ({ + input: ({ context }) => ({ id: context.userId, - username: event.username, - avatar_character: event.avatarCharacter, - avatar_background: event.avatarBackground + username: context.username, + avatar_character: context.avatar_character, + avatar_background: context.avatar_background }), onDone: { - target: CUSTOMIZE_USER_STATES.IDLE, + target: 'idle', actions: [ assign({ statusMessage: 'Profile updated' - }) + }), + sendTo( + ({ system }) => system.get('root'), + ({ context }) => ({ + type: 'updateUserData', + params: { + username: context.username, + avatar_character: context.avatar_character, + avatar_background: context.avatar_background + } + }) + ) ] }, onError: { - target: CUSTOMIZE_USER_STATES.IDLE, + target: 'idle', actions: assign({ statusMessage: ({ event }) => event.error.message }) diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 995db5b..383dafc 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -6,12 +6,6 @@ import loginMachine from './login'; const rootMachine = setup({ actors: { getUser: fromPromise(getUser) - }, - actions: { - getUser: sendTo( - ({ system }) => system.get('user'), - { type: 'get' } - ) } }).createMachine({ entry: [ @@ -43,7 +37,31 @@ const rootMachine = setup({ } }, 'authenticated': { + entry: [ + sendTo( + ({ system }) => system.get('customizeUser'), + ({ context }) => ({ + type: 'initialize', + params: { + userId: context.userData.id, + username: context.userData.username, + avatar_character: context.userData.avatar_character, + avatar_background: context.userData.avatar_background + } + }) + ) + ], on: { + updateUserData: { + actions: assign({ + userData: ({ event, context }) => ({ + id: context.userData.id, + username: event.params.username, + avatar_character: event.params.avatar_character, + avatar_background: event.params.avatar_background + }) + }) + }, unauthenticate: { target: 'unauthenticated', actions: assign({ diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index b6257e9..1827f65 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -3,46 +3,38 @@ import RadioGroup from '@timo/common/components/RadioGroup'; import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; +import useMachineState from '../../../hooks/useMachineState'; +import useMachine from '../../../hooks/useMachine'; import styles from '../Profile.module.css'; -import UserMachineContext from '../../../context/UserMachineContext'; -import { useActor } from '@xstate/react'; -import customizeUserMachine, { CUSTOMIZE_USER_EVENTS } from '../../../machines/customizeUserMachine'; const CustomizeUser = () => { - const userMachine = UserMachineContext.useActorRef(); - const updatedMachine = customizeUserMachine.provide({ - actors: { - userMachine - } - }); - const [state, send] = useActor(updatedMachine); + const customizeUserMachineState = useMachineState('customizeUser', state => state.context); + const { username, avatar_character, avatar_background, statusMessage } = customizeUserMachineState; + const customizeUserMachine = useMachine('customizeUser'); + + const handleCustomizeFormSubmit = (e) => { + e.preventDefault(); + customizeUserMachine.send({ type: 'save' }); + }; const handleAvatarBackgroundChange = (e) => { - send({ - type: CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_BACKGROUND, - background: e.target.value + customizeUserMachine.send({ + type: 'changeAvatarBackground', + value: e.target.value }); }; const handleAvatarCharacterChange = (e) => { - send({ - type: CUSTOMIZE_USER_EVENTS.CHANGE_AVATAR_CHARACTER, - character: e.target.value + customizeUserMachine.send({ + type: 'changeAvatarCharacter', + value: e.target.value }); }; - const handleCustomizeFormSubmit = (e) => { - e.preventDefault(); - const formData = new FormData(e.target); - const username = formData.get('username'); - const avatarCharacter = formData.get('avatar-character'); - const avatarBackground = formData.get('avatar-background'); - - send({ - type: CUSTOMIZE_USER_EVENTS.SAVE, - username, - avatarCharacter, - avatarBackground + const handleUsernameChange = (e) => { + customizeUserMachine.send({ + type: 'changeUsername', + value: e.target.value }); }; @@ -50,8 +42,8 @@ const CustomizeUser = () => { <> @@ -64,7 +56,7 @@ const CustomizeUser = () => { { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' } ]} - defaultValue={userData?.avatar_background} + defaultValue={avatar_background} onChange={handleAvatarBackgroundChange} /> { type="text" maxLength={1} pattern="[A-Za-z]" - defaultValue={userData?.avatar_character} + defaultValue={avatar_character} onChange={handleAvatarCharacterChange} labelVisible required @@ -82,11 +74,12 @@ const CustomizeUser = () => { name="username" label="Username" type="text" - defaultValue={userData?.username} + defaultValue={username} + onChange={handleUsernameChange} labelVisible required /> - {customizeStatus && } + {statusMessage && }
From 5b7d9a86c709e29a272d2d47fc624b2fdc4210c3 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 14:22:19 +0200 Subject: [PATCH 08/26] implemented changePasswordMachine --- .../xstate/src/machines/changePassword.js | 60 +++++++++++++++++++ packages/xstate/src/machines/root.js | 11 +++- .../Profile/sections/ChangePassword.jsx | 29 +++------ .../routes/Profile/sections/CustomizeUser.jsx | 8 ++- 4 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 packages/xstate/src/machines/changePassword.js diff --git a/packages/xstate/src/machines/changePassword.js b/packages/xstate/src/machines/changePassword.js new file mode 100644 index 0000000..8156d4a --- /dev/null +++ b/packages/xstate/src/machines/changePassword.js @@ -0,0 +1,60 @@ +import { setup, assign, fromPromise } from 'xstate'; +import { updatePassword } from '@timo/common/api'; + +const changePasswordMachine = setup({ + actors: { + updatePassword: fromPromise(async ({ input }) => updatePassword(input)) + } +}).createMachine({ + id: 'changePassword', + initial: 'idle', + context: { + username: null, + statusMessage: null + }, + states: { + 'idle': { + on: { + 'initialize': { + actions: assign(({ event }) => ({ + username: event.username + })) + }, + 'save': { + target: 'saving' + } + } + }, + 'saving': { + entry: [ + assign({ + statusMessage: 'Saving...' + }) + ], + invoke: [ + { + src: 'updatePassword', + input: ({ context, event }) => ({ + username: context.username, + password: event.password, + newPassword: event.newPassword + }), + onDone: { + target: 'idle', + actions: assign({ + statusMessage: 'Password updated' + }) + }, + onError: { + target: 'idle', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + ] + } + } +}); + +export default changePasswordMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 383dafc..6292fff 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -2,6 +2,7 @@ import { setup, spawnChild, sendTo, assign, fromPromise } from 'xstate'; import { getUser } from '@timo/common/api'; import customizeUserMachine from './customizeUser'; import loginMachine from './login'; +import changePasswordMachine from './changePassword'; const rootMachine = setup({ actors: { @@ -10,7 +11,8 @@ const rootMachine = setup({ }).createMachine({ entry: [ spawnChild(loginMachine, { systemId: 'login' }), - spawnChild(customizeUserMachine, { systemId: 'customizeUser' }) + spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), + spawnChild(changePasswordMachine, { systemId: 'changePassword' }) ], initial: 'unknown', context: { @@ -49,6 +51,13 @@ const rootMachine = setup({ avatar_background: context.userData.avatar_background } }) + ), + sendTo( + ({ system }) => system.get('changePassword'), + ({ context }) => ({ + type: 'initialize', + username: context.userData.username + }) ) ], on: { diff --git a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx index 26d9b3e..a1e742a 100644 --- a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx +++ b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx @@ -1,32 +1,21 @@ -import { useState } from 'react'; import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; -import { updatePassword } from '@timo/common/api'; import styles from '../Profile.module.css'; -import UserMachineContext from '../../../context/UserMachineContext'; +import useMachineState from '../../../hooks/useMachineState'; +import useMachine from '../../../hooks/useMachine'; const ChangePassword = () => { - const username = UserMachineContext.useSelector((state) => state.context.data?.username); - const [passwordStatus, setPasswordStatus] = useState(null); + const { statusMessage } = useMachineState('changePassword', state => state.context); + const changePasswordMachine = useMachine('changePassword'); const handlePasswordFormSubmit = (e) => { e.preventDefault(); - const formData = new FormData(e.target); - const password = formData.get('password'); - const newPassword = formData.get('newPassword'); - - setPasswordStatus('Loading...'); - - updatePassword({ - username, - password, - newPassword - }).then(() => { - setPasswordStatus('Password updated'); - }).catch((error) => { - setPasswordStatus(error.message); + changePasswordMachine.send({ + type: 'save', + password: formData.get('password'), + newPassword: formData.get('newPassword') }); }; @@ -50,7 +39,7 @@ const ChangePassword = () => { labelVisible required /> - {passwordStatus && } + {statusMessage && }
diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index 1827f65..7aaa739 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -8,9 +8,13 @@ import useMachine from '../../../hooks/useMachine'; import styles from '../Profile.module.css'; const CustomizeUser = () => { - const customizeUserMachineState = useMachineState('customizeUser', state => state.context); - const { username, avatar_character, avatar_background, statusMessage } = customizeUserMachineState; const customizeUserMachine = useMachine('customizeUser'); + const { + username, + avatar_character, + avatar_background, + statusMessage + } = useMachineState('customizeUser', state => state.context); const handleCustomizeFormSubmit = (e) => { e.preventDefault(); From e5a92f97ee0dc804e5116b71479806a3a40ffdbd Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 14:30:19 +0200 Subject: [PATCH 09/26] implemented profile machine --- packages/xstate/src/machines/profile.js | 37 +++++++++++++++++++ packages/xstate/src/machines/root.js | 4 +- .../xstate/src/routes/Profile/Profile.jsx | 9 ++--- 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 packages/xstate/src/machines/profile.js diff --git a/packages/xstate/src/machines/profile.js b/packages/xstate/src/machines/profile.js new file mode 100644 index 0000000..0ab889c --- /dev/null +++ b/packages/xstate/src/machines/profile.js @@ -0,0 +1,37 @@ +import { logout } from '@timo/common/api'; +import { fromPromise, sendTo, setup } from 'xstate'; + +const profileMachine = setup({ + actors: { + logout: fromPromise(logout) + } +}).createMachine({ + id: 'profile', + initial: 'idle', + states: { + 'idle': { + on: { + 'logout': { + target: 'logging-out' + } + } + }, + 'logging-out': { + invoke: { + src: 'logout', + onDone: { + target: 'idle', + actions: sendTo( + ({ system }) => system.get('root'), + { type: 'unauthenticate' } + ) + }, + onError: { + target: 'idle' + } + } + } + } +}); + +export default profileMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 6292fff..071664f 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -3,6 +3,7 @@ import { getUser } from '@timo/common/api'; import customizeUserMachine from './customizeUser'; import loginMachine from './login'; import changePasswordMachine from './changePassword'; +import profileMachine from './profile'; const rootMachine = setup({ actors: { @@ -12,7 +13,8 @@ const rootMachine = setup({ entry: [ spawnChild(loginMachine, { systemId: 'login' }), spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), - spawnChild(changePasswordMachine, { systemId: 'changePassword' }) + spawnChild(changePasswordMachine, { systemId: 'changePassword' }), + spawnChild(profileMachine, { systemId: 'profile' }) ], initial: 'unknown', context: { diff --git a/packages/xstate/src/routes/Profile/Profile.jsx b/packages/xstate/src/routes/Profile/Profile.jsx index 65b2760..131701a 100644 --- a/packages/xstate/src/routes/Profile/Profile.jsx +++ b/packages/xstate/src/routes/Profile/Profile.jsx @@ -3,16 +3,13 @@ import Button, { ButtonVariants } from '@timo/common/components/Button'; import styles from './Profile.module.css'; import ChangePassword from './sections/ChangePassword'; import CustomizeUser from './sections/CustomizeUser'; -import UserMachineContext from '../../context/UserMachineContext'; -import { USER_EVENTS } from '../../machines/userMachine'; +import useMachine from '../../hooks/useMachine'; const Profile = () => { - const userMachine = UserMachineContext.useActorRef(); + const profileMachine = useMachine('profile'); const handleLogoutClick = () => { - userMachine.send({ - type: USER_EVENTS.LOGOUT - }); + profileMachine.send({ type: 'logout' }); }; return ( From e0332b62197e77e913750d4c98debd792faf5625 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 14:33:20 +0200 Subject: [PATCH 10/26] renamed hooks to useSystemMachine for added clarity --- .../xstate/src/contextualComponents/ProtectedRoute.jsx | 4 ++-- .../xstate/src/contextualComponents/TopBarWithUser.jsx | 4 ++-- packages/xstate/src/hooks/useMachineState.js | 9 --------- .../src/hooks/{useMachine.js => useSystemMachine.js} | 4 ++-- packages/xstate/src/hooks/useSystemMachineState.js | 9 +++++++++ packages/xstate/src/routes/Login/Login.jsx | 10 +++++----- packages/xstate/src/routes/Profile/Profile.jsx | 4 ++-- .../src/routes/Profile/sections/ChangePassword.jsx | 8 ++++---- .../src/routes/Profile/sections/CustomizeUser.jsx | 8 ++++---- 9 files changed, 30 insertions(+), 30 deletions(-) delete mode 100644 packages/xstate/src/hooks/useMachineState.js rename packages/xstate/src/hooks/{useMachine.js => useSystemMachine.js} (72%) create mode 100644 packages/xstate/src/hooks/useSystemMachineState.js diff --git a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx index 08eb841..3537d44 100644 --- a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx +++ b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx @@ -1,11 +1,11 @@ import { useEffect } from 'react'; import PropTypes from 'prop-types'; -import useMachineState from '../hooks/useMachineState'; +import useSystemMachineState from '../hooks/useSystemMachineState'; const FALLBACK_ROUTE = './login'; const ProtectedRoute = ({ history, children }) => { - const authState = useMachineState('root', (state) => state.value); + const authState = useSystemMachineState('root', (state) => state.value); useEffect(() => { if (authState === 'unauthenticated') { diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx index a609967..84c74e0 100644 --- a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx +++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; import TopBar from '@timo/common/components/TopBar'; -import useMachineState from '../hooks/useMachineState'; +import useSystemMachineState from '../hooks/useSystemMachineState'; const TopBarWithUser = ({ history }) => { - const userData = useMachineState('root', (state) => state.context.userData); + const userData = useSystemMachineState('root', (state) => state.context.userData); return ( { - const machine = useMachine(systemId); - return useSelector(machine, selector); -}; - -export default useMachineState; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useMachine.js b/packages/xstate/src/hooks/useSystemMachine.js similarity index 72% rename from packages/xstate/src/hooks/useMachine.js rename to packages/xstate/src/hooks/useSystemMachine.js index 3bd587a..ca36e2a 100644 --- a/packages/xstate/src/hooks/useMachine.js +++ b/packages/xstate/src/hooks/useSystemMachine.js @@ -1,9 +1,9 @@ import { useContext } from 'react'; import { MachineContext } from '../context/MachineContext'; -const useMachine = (systemId) => { +const useSystemMachine = (systemId) => { const machine = useContext(MachineContext); return machine.system.get(systemId); }; -export default useMachine; \ No newline at end of file +export default useSystemMachine; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useSystemMachineState.js b/packages/xstate/src/hooks/useSystemMachineState.js new file mode 100644 index 0000000..36e71c6 --- /dev/null +++ b/packages/xstate/src/hooks/useSystemMachineState.js @@ -0,0 +1,9 @@ +import { useSelector } from '@xstate/react'; +import useSystemMachine from './useSystemMachine'; + +const useSystemMachineState = (systemId, selector) => { + const machine = useSystemMachine(systemId); + return useSelector(machine, selector); +}; + +export default useSystemMachineState; \ No newline at end of file diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx index 3d8d1ed..5e3eddf 100644 --- a/packages/xstate/src/routes/Login/Login.jsx +++ b/packages/xstate/src/routes/Login/Login.jsx @@ -4,15 +4,15 @@ import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import Title from '@timo/common/components/Title'; import StatusMessage from '@timo/common/components/StatusMessage'; -import useMachine from '../../hooks/useMachine'; -import useMachineState from '../../hooks/useMachineState'; +import useSystemMachine from '../../hooks/useSystemMachine'; +import useSystemMachineState from '../../hooks/useSystemMachineState'; import styles from './Login.module.css'; const Login = ({ history }) => { - const authState = useMachineState('root', (state) => state.value); - const statusMessage = useMachineState('login', (state) => state.context.statusMessage); - const loginMachine = useMachine('login'); + const authState = useSystemMachineState('root', (state) => state.value); + const statusMessage = useSystemMachineState('login', (state) => state.context.statusMessage); + const loginMachine = useSystemMachine('login'); useEffect(() => { if (authState === 'authenticated') { diff --git a/packages/xstate/src/routes/Profile/Profile.jsx b/packages/xstate/src/routes/Profile/Profile.jsx index 131701a..81fa475 100644 --- a/packages/xstate/src/routes/Profile/Profile.jsx +++ b/packages/xstate/src/routes/Profile/Profile.jsx @@ -3,10 +3,10 @@ import Button, { ButtonVariants } from '@timo/common/components/Button'; import styles from './Profile.module.css'; import ChangePassword from './sections/ChangePassword'; import CustomizeUser from './sections/CustomizeUser'; -import useMachine from '../../hooks/useMachine'; +import useSystemMachine from '../../hooks/useSystemMachine'; const Profile = () => { - const profileMachine = useMachine('profile'); + const profileMachine = useSystemMachine('profile'); const handleLogoutClick = () => { profileMachine.send({ type: 'logout' }); diff --git a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx index a1e742a..6a293be 100644 --- a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx +++ b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx @@ -2,12 +2,12 @@ import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; import styles from '../Profile.module.css'; -import useMachineState from '../../../hooks/useMachineState'; -import useMachine from '../../../hooks/useMachine'; +import useSystemMachineState from '../../../hooks/useSystemMachineState'; +import useSystemMachine from '../../../hooks/useSystemMachine'; const ChangePassword = () => { - const { statusMessage } = useMachineState('changePassword', state => state.context); - const changePasswordMachine = useMachine('changePassword'); + const { statusMessage } = useSystemMachineState('changePassword', state => state.context); + const changePasswordMachine = useSystemMachine('changePassword'); const handlePasswordFormSubmit = (e) => { e.preventDefault(); diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index 7aaa739..78210f3 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -3,18 +3,18 @@ import RadioGroup from '@timo/common/components/RadioGroup'; import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; -import useMachineState from '../../../hooks/useMachineState'; -import useMachine from '../../../hooks/useMachine'; +import useSystemMachineState from '../../../hooks/useSystemMachineState'; +import useSystemMachine from '../../../hooks/useSystemMachine'; import styles from '../Profile.module.css'; const CustomizeUser = () => { - const customizeUserMachine = useMachine('customizeUser'); + const customizeUserMachine = useSystemMachine('customizeUser'); const { username, avatar_character, avatar_background, statusMessage - } = useMachineState('customizeUser', state => state.context); + } = useSystemMachineState('customizeUser', state => state.context); const handleCustomizeFormSubmit = (e) => { e.preventDefault(); From 890558a063c7d8e189e6ce570555ee0514303668 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 16:49:02 +0200 Subject: [PATCH 11/26] implemented entriesMachine --- packages/xstate/src/machines/entries.js | 158 ++++++++++++++++++ packages/xstate/src/machines/root.js | 4 +- .../xstate/src/routes/Entries/Entries.jsx | 117 ++++--------- 3 files changed, 191 insertions(+), 88 deletions(-) create mode 100644 packages/xstate/src/machines/entries.js diff --git a/packages/xstate/src/machines/entries.js b/packages/xstate/src/machines/entries.js new file mode 100644 index 0000000..8b4402c --- /dev/null +++ b/packages/xstate/src/machines/entries.js @@ -0,0 +1,158 @@ +import { setup, fromPromise, assign } from 'xstate'; +import { listEntries, updateEntry, deleteEntry } from '@timo/common/api'; +import getDateString from '@timo/common/utils/getDateString'; + +const now = new Date(); +const firstDateOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); +const lastDateOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); + +const getTotalDuration = (entries) => { + return entries.reduce((total, entry) => { + const diff = new Date(entry.end_time) - new Date(entry.start_time); + return total + diff; + }, 0); +}; + +const getEntriesGroupedByDate = (entries) => { + const formatter = new Intl.DateTimeFormat('default', { dateStyle: 'medium' }); + const groupedEntries = entries + .toReversed() // Entries are sorted by id which is ascending, so we need to reverse them + .reduce((grouped, entry) => { + const date = formatter.format(new Date(entry.start_time)); + if (!grouped[date]) { + grouped[date] = []; + } + grouped[date].push(entry); // Order of object keys is not guaranteed but YOLO + return grouped; + }, {}); + return Object.entries(groupedEntries) + .map(([date, dayEntries]) => [ + date, + dayEntries.toReversed(), // Day entries are descending after being grouped, reverse them + getTotalDuration(dayEntries) + ]); +}; + +const entriesMachine = setup({ + actors: { + getEntries: fromPromise(async ({ input }) => listEntries(input)), + updateEntry: fromPromise(async ({ input }) => updateEntry(input)), + deleteEntry: fromPromise(async ({ input }) => deleteEntry(input)) + } +}).createMachine({ + initial: 'loading', + context: { + groupedEntries: [], + totalDuration: 0, + statusMessage: null, + filter: { + startDate: getDateString(firstDateOfMonth), + endDate: getDateString(lastDateOfMonth) + }, + itemStatusMessage: {} + }, + states: { + 'loading': { + invoke: { + src: 'getEntries', + input: ({ context }) => ({ + from: context.filter.startDate, + to: context.filter.endDate + }), + onDone: { + target: 'idle', + actions: assign(({ event }) => ({ + groupedEntries: getEntriesGroupedByDate(event.output), + totalDuration: getTotalDuration(event.output), + statusMessage: event.output.length === 0 ? 'No entries found' : null + })) + }, + onError: { + target: 'idle', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + }, + 'idle': { + on: { + 'filter': { + target: 'loading', + actions: assign(({ event }) => ({ + filter: { + startDate: event.startDate, + endDate: event.endDate + } + })) + }, + 'updateEntry': { + target: 'updating-entry' + }, + 'deleteEntry': { + target: 'deleting-entry' + } + } + }, + 'updating-entry': { + entry: [ + assign({ + itemStatusMessage: ({ context, event }) => ({ + ...context.itemStatusMessage, + [event.id]: 'Saving...' + }) + }) + ], + invoke: { + src: 'updateEntry', + input: ({ event }) => event.updatedEntry, + onDone: { + target: 'loading', + actions: assign({ + itemStatusMessage: ({ context, event }) => ({ + ...context.itemStatusMessage, + [event.id]: null + }) + }) + }, + onError: { + target: 'idle', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + }, + 'deleting-entry': { + entry: [ + assign({ + itemStatusMessage: ({ context, event }) => ({ + ...context.itemStatusMessage, + [event.id]: 'Deleting...' + }) + }) + ], + invoke: { + src: 'deleteEntry', + input: ({ event }) => event.entryId, + onDone: { + target: 'loading', + actions: assign({ + itemStatusMessage: ({ context, event }) => ({ + ...context.itemStatusMessage, + [event.id]: null + }) + }) + }, + onError: { + target: 'idle', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + } + } +}); + +export default entriesMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 071664f..6c971f9 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -4,6 +4,7 @@ import customizeUserMachine from './customizeUser'; import loginMachine from './login'; import changePasswordMachine from './changePassword'; import profileMachine from './profile'; +import entriesMachine from './entries'; const rootMachine = setup({ actors: { @@ -14,7 +15,8 @@ const rootMachine = setup({ spawnChild(loginMachine, { systemId: 'login' }), spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), spawnChild(changePasswordMachine, { systemId: 'changePassword' }), - spawnChild(profileMachine, { systemId: 'profile' }) + spawnChild(profileMachine, { systemId: 'profile' }), + spawnChild(entriesMachine, { systemId: 'entries' }) ], initial: 'unknown', context: { diff --git a/packages/xstate/src/routes/Entries/Entries.jsx b/packages/xstate/src/routes/Entries/Entries.jsx index f4116d5..170733e 100644 --- a/packages/xstate/src/routes/Entries/Entries.jsx +++ b/packages/xstate/src/routes/Entries/Entries.jsx @@ -1,109 +1,53 @@ -import { useEffect, useState, createRef } from 'react'; import PropTypes from 'prop-types'; -import { listEntries, updateEntry, deleteEntry } from '@timo/common/api'; import Entry from '@timo/common/components/Entry'; import Title from '@timo/common/components/Title'; import Input from '@timo/common/components/Input'; import Button from '@timo/common/components/Button'; import StatusMessage from '@timo/common/components/StatusMessage'; import formatDuration from '@timo/common/utils/formatDuration'; -import getDateString from '@timo/common/utils/getDateString'; import styles from './Entries.module.css'; import { ButtonVariants } from '@timo/common/components/Button/Button'; - -const getTotalDuration = (entries) => { - return entries.reduce((total, entry) => { - const diff = new Date(entry.end_time) - new Date(entry.start_time); - return total + diff; - }, 0); -}; - -const getEntriesGroupedByDate = (entries) => { - const formatter = new Intl.DateTimeFormat('default', { dateStyle: 'medium' }); - // Entries are sorted by id which is ascending, so we need to reverse them - return entries.toReversed().reduce((grouped, entry) => { - const date = formatter.format(new Date(entry.start_time)); - if (!grouped[date]) { - grouped[date] = []; - } - grouped[date].push(entry); - return grouped; - }, {}); -}; - -const now = new Date(); -const firstDateOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); -const lastDateOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); +import useSystemMachine from '../../hooks/useSystemMachine'; +import useSystemMachineState from '../../hooks/useSystemMachineState'; const Entries = ({ history }) => { - const [entries, setEntries] = useState(null); - const [statusMessage, setStatusMessage] = useState(null); - const [entryStatusMessage, setEntryStatusMessage] = useState({ id: null, message: null }); - const formRef = createRef(null); + const { + groupedEntries, + totalDuration, + statusMessage, + filter, + itemStatusMessage + } = useSystemMachineState('entries', state => state.context); + const entriesMachine = useSystemMachine('entries'); const handleEdit = (updatedEntry) => { - setEntryStatusMessage({ id: updatedEntry.id, message: 'Saving...' }); - updateEntry(updatedEntry).then(() => { - setEntries(null); - setEntryStatusMessage({ id: null, message: null }); - }).catch((error) => { - setEntryStatusMessage({ id: updatedEntry.id, message: error.message }); + entriesMachine.send({ + type: 'updateEntry', + updatedEntry }); }; const handleDelete = (entryId) => { - setEntryStatusMessage({ id: entryId, message: 'Deleting...' }); - deleteEntry(entryId).then(() => { - setEntries(null); - setEntryStatusMessage({ id: null, message: null }); - }).catch((error) => { - setEntryStatusMessage({ id: entryId, message: error.message }); + entriesMachine.send({ + type: 'deleteEntry', + entryId }); }; - const handleListEntriesResponse = (entries) => { - setEntries(entries); - if (entries.length === 0) { - setStatusMessage('No entries found'); - } else { - setStatusMessage(null); - } - }; - - const handleListEntriesError = (error) => { - setStatusMessage(error.message); - }; - const handleFilter = (e) => { e.preventDefault(); const formData = new FormData(e.target); - const from = formData.get('from'); - const to = formData.get('to'); - setStatusMessage('Loading...'); - listEntries({ from, to }) - .then(handleListEntriesResponse) - .catch(handleListEntriesError); + entriesMachine.send({ + type: 'filter', + startDate: formData.get('from'), + endDate: formData.get('to') + }); }; const handleNewClick = () => { history.push('./new'); }; - useEffect(() => { - const formData = new FormData(formRef.current); - const from = formData.get('from'); - const to = formData.get('to'); - - if (entries === null) { - setStatusMessage('Loading...'); - listEntries({ - from, - to - }).then(handleListEntriesResponse) - .catch(handleListEntriesError); - } - }, [entries]); - return ( <> Time entries @@ -111,26 +55,25 @@ const Entries = ({ history }) => {
- - - + + + {statusMessage && } - {entries?.length > 0 && ( + {totalDuration > 0 && ( <>

Total

-
{formatDuration(getTotalDuration(entries))}
+
{formatDuration(totalDuration)}
- {Object.entries(getEntriesGroupedByDate(entries)).map(([date, dayEntries]) => ( + {groupedEntries.map(([date, dayEntries, dayEntriesDuration]) => (

{date}

-
{formatDuration(getTotalDuration(dayEntries))}
+
{formatDuration(dayEntriesDuration)}
- {/* Entries are descending after being grouped, reverse them */} - {dayEntries.toReversed().map((entry) => ( + {dayEntries.map((entry) => ( { end_time={entry.end_time} onEdit={handleEdit} onDelete={handleDelete} - status={entryStatusMessage.id === entry.id ? entryStatusMessage.message : null} + status={itemStatusMessage[entry.id]} /> ))}
From f7d16dfd12c41f2d667ae8f4decf77568776a893 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 18:36:47 +0200 Subject: [PATCH 12/26] implemented timerMachine --- packages/xstate/src/components/Timer.jsx | 17 ++ packages/xstate/src/machines/entries.js | 3 + packages/xstate/src/machines/newEntry.js | 119 ++++++++++++++ packages/xstate/src/machines/root.js | 4 +- .../xstate/src/routes/NewEntry/NewEntry.jsx | 150 +++++++----------- .../src/routes/NewEntry/NewEntry.module.css | 1 + 6 files changed, 201 insertions(+), 93 deletions(-) create mode 100644 packages/xstate/src/components/Timer.jsx create mode 100644 packages/xstate/src/machines/newEntry.js diff --git a/packages/xstate/src/components/Timer.jsx b/packages/xstate/src/components/Timer.jsx new file mode 100644 index 0000000..7969594 --- /dev/null +++ b/packages/xstate/src/components/Timer.jsx @@ -0,0 +1,17 @@ +import styles from '@timo/common/components/Timer/Timer.module.css'; +import useSystemMachineState from '../hooks/useSystemMachineState'; + +const Timer = () => { + const timerValue = useSystemMachineState('newEntry', state => state.context.timerValue); + + // Format duration to HH:MM:SS + const formattedValue = new Date(timerValue * 1000).toISOString().slice(11, 19); + + return ( +
+ {formattedValue} +
+ ); +}; + +export default Timer; \ No newline at end of file diff --git a/packages/xstate/src/machines/entries.js b/packages/xstate/src/machines/entries.js index 8b4402c..ff1f2dd 100644 --- a/packages/xstate/src/machines/entries.js +++ b/packages/xstate/src/machines/entries.js @@ -86,6 +86,9 @@ const entriesMachine = setup({ } })) }, + 'refresh': { + target: 'loading' + }, 'updateEntry': { target: 'updating-entry' }, diff --git a/packages/xstate/src/machines/newEntry.js b/packages/xstate/src/machines/newEntry.js new file mode 100644 index 0000000..51136b4 --- /dev/null +++ b/packages/xstate/src/machines/newEntry.js @@ -0,0 +1,119 @@ +import { setup, fromPromise, fromCallback, assign, sendTo } from 'xstate'; +import { createEntry } from '@timo/common/api'; + +const newEntriesMachine = setup({ + actors: { + createEntry: fromPromise(async ({ input }) => createEntry(input)), + timer: fromCallback(({ sendBack, receive}) => { + const interval = setInterval(() => { + sendBack({ + type: 'tick' + }); + }, 1000); + receive((event) => { + if (event.type === 'stop') { + clearInterval(interval); + } + }); + }) + } +}).createMachine({ + initial: 'idle', + context: { + timerValue: 0, + statusMessage: null + }, + states: { + 'idle': { + entry: [ + assign({ + timerValue: 0 + }) + ], + on: { + 'start': { + target: 'active' + } + } + }, + 'active': { + entry: [ + assign({ + statusMessage: null + }) + ], + invoke: { + src: 'timer', + id: 'timer' + }, + on: { + 'tick': { + actions: assign({ + timerValue: ({ context }) => context.timerValue + 1 + }) + }, + 'pause': { + target: 'paused' + }, + 'finish': { + target: 'finishing' + } + }, + exit: [ + sendTo('timer', { + type: 'stop' + }) + ] + }, + 'paused': { + on: { + 'resume': { + target: 'active' + }, + 'finish': { + target: 'finishing' + } + } + }, + 'finishing': { + entry: [ + assign({ + statusMessage: 'Saving...' + }) + ], + invoke: { + src: 'createEntry', + input: ({ event, context }) => { + const endTimestamp = Math.floor(Date.now() / 1000); + return { + description: event.description, + start_time: `@${endTimestamp - context.timerValue}`, + end_time: `@${endTimestamp}` + }; + }, + onDone: { + target: 'idle', + actions: [ + assign({ + statusMessage: 'Time logged successfully' + }), + sendTo( + ({ system }) => system.get('entries'), + { + type: 'refresh' + } + ) + ] + }, + onError: { + target: 'paused', + actions: assign({ + statusMessage: ({ event }) => event.error.message + }) + } + } + } + } +}); + +export default newEntriesMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 6c971f9..fda6b68 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -5,6 +5,7 @@ import loginMachine from './login'; import changePasswordMachine from './changePassword'; import profileMachine from './profile'; import entriesMachine from './entries'; +import newEntriesMachine from './newEntry'; const rootMachine = setup({ actors: { @@ -16,7 +17,8 @@ const rootMachine = setup({ spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), spawnChild(changePasswordMachine, { systemId: 'changePassword' }), spawnChild(profileMachine, { systemId: 'profile' }), - spawnChild(entriesMachine, { systemId: 'entries' }) + spawnChild(entriesMachine, { systemId: 'entries' }), + spawnChild(newEntriesMachine, { systemId: 'newEntry' }) ], initial: 'unknown', context: { diff --git a/packages/xstate/src/routes/NewEntry/NewEntry.jsx b/packages/xstate/src/routes/NewEntry/NewEntry.jsx index 27da0d6..1bfde94 100644 --- a/packages/xstate/src/routes/NewEntry/NewEntry.jsx +++ b/packages/xstate/src/routes/NewEntry/NewEntry.jsx @@ -1,110 +1,76 @@ -import { useEffect, useState } from 'react'; -import { createEntry } from '@timo/common/api'; -import Timer from '@timo/common/components/Timer'; import Title from '@timo/common/components/Title'; import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import StatusMessage from '@timo/common/components/StatusMessage'; +import useSystemMachineState from '../../hooks/useSystemMachineState'; +import useSystemMachine from '../../hooks/useSystemMachine'; +import Timer from '../../components/Timer'; import styles from './NewEntry.module.css'; -const TimerState = { - ACTIVE: 'active', - PAUSED: 'paused', - STOPPED: 'stopped' -}; - const NewEntry = () => { - const [duration, setDuration] = useState(0); - const [timerState, setTimerState] = useState(TimerState.STOPPED); - const [description, setDescription] = useState(''); - const [statusMessage, setStatusMessage] = useState(''); - - const handleStartClick = () => { - if (description.length === 0) { - setStatusMessage('Description is required'); - } else { - setTimerState(TimerState.ACTIVE); - setStatusMessage(''); - } - }; - - const handleStopClick = () => { - setTimerState(TimerState.STOPPED); - }; - - const handlePauseClick = () => { - setTimerState(TimerState.PAUSED); - }; + const timerState = useSystemMachineState('newEntry', state => state.value); + const statusMessage = useSystemMachineState('newEntry', state => state.context.statusMessage); + const newEntryMachine = useSystemMachine('newEntry'); - const handleDescriptionChange = (e) => { - setDescription(e.target.value); + const handleSubmit = (e) => { + e.preventDefault(); + const action = e.nativeEvent.submitter.value; + const formData = new FormData(e.target); + newEntryMachine.send({ + type: action, + description: formData.get('description') + }); }; - useEffect(() => { - if (timerState === TimerState.STOPPED && duration > 0) { - setDuration(0); - const endTimestamp = Math.floor(Date.now() / 1000); - createEntry({ - description, - start_time: `@${endTimestamp - duration}`, - end_time: `@${endTimestamp}` - }).then(() => { - setStatusMessage('Time logged successfully'); - }).catch((error) => { - setStatusMessage(error.message); - }); - } - }); - return ( <> New time entry -
- - -
- -
- {(timerState === TimerState.STOPPED && duration === 0) && ( - +
+
+ + +
+ {statusMessage && ( + )} - {(timerState === TimerState.PAUSED && duration !== 0) && ( - <> - - - - )} - {timerState === TimerState.ACTIVE && ( - <> - - - - )} -
+ )} + {timerState === 'paused' && ( + <> + + + + )} + {timerState === 'active' && ( + <> + + + + )} + + ); }; diff --git a/packages/xstate/src/routes/NewEntry/NewEntry.module.css b/packages/xstate/src/routes/NewEntry/NewEntry.module.css index 8a010a6..8083a78 100644 --- a/packages/xstate/src/routes/NewEntry/NewEntry.module.css +++ b/packages/xstate/src/routes/NewEntry/NewEntry.module.css @@ -21,6 +21,7 @@ justify-content: center; align-items: center; gap: 8px; + margin-top: 12px; } .entries { From f3ce64cdc984ee7239b7a808fd1cbd09ccda4c0c Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 18:40:45 +0200 Subject: [PATCH 13/26] added new finished state to fix bug --- packages/xstate/src/machines/newEntry.js | 34 +++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/xstate/src/machines/newEntry.js b/packages/xstate/src/machines/newEntry.js index 51136b4..dd1509d 100644 --- a/packages/xstate/src/machines/newEntry.js +++ b/packages/xstate/src/machines/newEntry.js @@ -92,18 +92,7 @@ const newEntriesMachine = setup({ }; }, onDone: { - target: 'idle', - actions: [ - assign({ - statusMessage: 'Time logged successfully' - }), - sendTo( - ({ system }) => system.get('entries'), - { - type: 'refresh' - } - ) - ] + target: 'finished' }, onError: { target: 'paused', @@ -112,6 +101,27 @@ const newEntriesMachine = setup({ }) } } + }, + 'finished': { + entry: [ + assign({ + statusMessage: 'Time logged successfully' + }), + sendTo( + ({ system }) => system.get('entries'), + { + type: 'refresh' + } + ) + ], + after: { + 2000: { + target: 'idle', + actions: assign({ + statusMessage: null + }) + } + } } } }); From 90fd425c5b9dc6ae635bf7b21e7e0cf3d93c7dec Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 18:44:28 +0200 Subject: [PATCH 14/26] fixed bug with item status message --- packages/xstate/src/machines/entries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate/src/machines/entries.js b/packages/xstate/src/machines/entries.js index ff1f2dd..3411efa 100644 --- a/packages/xstate/src/machines/entries.js +++ b/packages/xstate/src/machines/entries.js @@ -114,7 +114,7 @@ const entriesMachine = setup({ actions: assign({ itemStatusMessage: ({ context, event }) => ({ ...context.itemStatusMessage, - [event.id]: null + [event.updatedEntry.id]: null }) }) }, @@ -131,7 +131,7 @@ const entriesMachine = setup({ assign({ itemStatusMessage: ({ context, event }) => ({ ...context.itemStatusMessage, - [event.id]: 'Deleting...' + [event.entryId]: 'Deleting...' }) }) ], From ddafe8d5ea783b88f62b78f447dad10f41f041c0 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 18:46:12 +0200 Subject: [PATCH 15/26] removed unnecessary prop spread --- packages/xstate/src/machines/customizeUser.js | 9 +++------ packages/xstate/src/machines/entries.js | 12 ++++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/xstate/src/machines/customizeUser.js b/packages/xstate/src/machines/customizeUser.js index 5729eaa..46c55b3 100644 --- a/packages/xstate/src/machines/customizeUser.js +++ b/packages/xstate/src/machines/customizeUser.js @@ -27,20 +27,17 @@ const customizeUserMachine = setup({ })) }, 'changeAvatarCharacter': { - actions: assign(({ context, event }) => ({ - ...context, + actions: assign(({ event }) => ({ avatar_character: event.value })) }, 'changeAvatarBackground': { - actions: assign(({ context, event }) => ({ - ...context, + actions: assign(({ event }) => ({ avatar_background: event.value })) }, 'changeUsername': { - actions: assign(({ context, event }) => ({ - ...context, + actions: assign(({ event }) => ({ username: event.value })) }, diff --git a/packages/xstate/src/machines/entries.js b/packages/xstate/src/machines/entries.js index 3411efa..577d537 100644 --- a/packages/xstate/src/machines/entries.js +++ b/packages/xstate/src/machines/entries.js @@ -100,8 +100,7 @@ const entriesMachine = setup({ 'updating-entry': { entry: [ assign({ - itemStatusMessage: ({ context, event }) => ({ - ...context.itemStatusMessage, + itemStatusMessage: ({ event }) => ({ [event.id]: 'Saving...' }) }) @@ -112,8 +111,7 @@ const entriesMachine = setup({ onDone: { target: 'loading', actions: assign({ - itemStatusMessage: ({ context, event }) => ({ - ...context.itemStatusMessage, + itemStatusMessage: ({ event }) => ({ [event.updatedEntry.id]: null }) }) @@ -129,8 +127,7 @@ const entriesMachine = setup({ 'deleting-entry': { entry: [ assign({ - itemStatusMessage: ({ context, event }) => ({ - ...context.itemStatusMessage, + itemStatusMessage: ({ event }) => ({ [event.entryId]: 'Deleting...' }) }) @@ -141,8 +138,7 @@ const entriesMachine = setup({ onDone: { target: 'loading', actions: assign({ - itemStatusMessage: ({ context, event }) => ({ - ...context.itemStatusMessage, + itemStatusMessage: ({ event }) => ({ [event.id]: null }) }) From d2650526dea8c9ea5be5ef10deae31f39c9f5412 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 18:46:59 +0200 Subject: [PATCH 16/26] removed unnecessary array --- packages/xstate/src/machines/root.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index fda6b68..cfc1675 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -89,16 +89,14 @@ const rootMachine = setup({ on: { authenticate: { target: 'authenticated', - actions:[ - assign({ - userData:({ event }) => ({ - id: event.params.id, - username: event.params.username, - avatar_character: event.params.avatar_character, - avatar_background: event.params.avatar_background - }) + actions: assign({ + userData:({ event }) => ({ + id: event.params.id, + username: event.params.username, + avatar_character: event.params.avatar_character, + avatar_background: event.params.avatar_background }) - ] + }) } } } From a549e14fa239fa9c5cfbd3b9146a07f7f617015b Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 19:35:34 +0200 Subject: [PATCH 17/26] initial implementation of history in rootMachine --- packages/xstate/src/App.jsx | 19 ++-- packages/xstate/src/components/Router.jsx | 28 ++++++ .../contextualComponents/ProtectedRoute.jsx | 31 ------- .../contextualComponents/TopBarWithUser.jsx | 13 ++- packages/xstate/src/machines/root.js | 90 ++++++++++++++++--- .../xstate/src/routes/Entries/Entries.jsx | 13 ++- packages/xstate/src/routes/Login/Login.jsx | 17 +--- 7 files changed, 122 insertions(+), 89 deletions(-) create mode 100644 packages/xstate/src/components/Router.jsx delete mode 100644 packages/xstate/src/contextualComponents/ProtectedRoute.jsx diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx index c2e3efb..a53b3aa 100644 --- a/packages/xstate/src/App.jsx +++ b/packages/xstate/src/App.jsx @@ -6,7 +6,6 @@ import Login from './routes/Login/Login'; import Entries from './routes/Entries/Entries'; import NewEntry from './routes/NewEntry/NewEntry'; import Profile from './routes/Profile/Profile'; -import ProtectedRoute from './contextualComponents/ProtectedRoute'; import TopBarWithUser from './contextualComponents/TopBarWithUser'; import MachineContextProvider from './context/MachineContext'; @@ -21,31 +20,25 @@ const routes = [ const App = () => ( - {(routeName, history) => { + {(routeName) => { let pageComponent = null; switch (routeName) { case 'Login': - pageComponent = ; + pageComponent = ; break; case 'NewEntry': pageComponent = ( - - - + ); break; case 'Entries': pageComponent = ( - - - + ); break; case 'Profile': pageComponent = ( - - - + ); break; default: @@ -55,7 +48,7 @@ const App = () => ( } return ( - + {pageComponent} ); diff --git a/packages/xstate/src/components/Router.jsx b/packages/xstate/src/components/Router.jsx new file mode 100644 index 0000000..30b74b0 --- /dev/null +++ b/packages/xstate/src/components/Router.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import useSystemMachineState from '../hooks/useSystemMachineState'; + +const BASE_URL = import.meta.env.VITE_BASE_URL; + +const Router = ({ routes, children }) => { + const currentPath = useSystemMachineState('root', (state) => state.context.currentPath); + + const currentRoute = routes.find(route => `${BASE_URL}${route.path}` === currentPath); + + if (!currentRoute) { + return children(null); + } + + return children(currentRoute.name); +}; + +Router.propTypes = { + routes: PropTypes.arrayOf( + PropTypes.shape({ + path: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }) + ).isRequired, + children: PropTypes.func.isRequired +}; + +export default Router; \ No newline at end of file diff --git a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx b/packages/xstate/src/contextualComponents/ProtectedRoute.jsx deleted file mode 100644 index 3537d44..0000000 --- a/packages/xstate/src/contextualComponents/ProtectedRoute.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import useSystemMachineState from '../hooks/useSystemMachineState'; - -const FALLBACK_ROUTE = './login'; - -const ProtectedRoute = ({ history, children }) => { - const authState = useSystemMachineState('root', (state) => state.value); - - useEffect(() => { - if (authState === 'unauthenticated') { - history.replace(FALLBACK_ROUTE); - } - }); - - if (authState === 'unknown') { - return null; - } - - return children; -}; - -ProtectedRoute.propTypes = { - userHook: PropTypes.func, - children: PropTypes.node.isRequired, - history: PropTypes.shape({ - replace: PropTypes.func.isRequired - }).isRequired -}; - -export default ProtectedRoute; \ No newline at end of file diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx index 84c74e0..d60303a 100644 --- a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx +++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx @@ -1,9 +1,10 @@ -import PropTypes from 'prop-types'; import TopBar from '@timo/common/components/TopBar'; +import useSystemMachine from '../hooks/useSystemMachine'; import useSystemMachineState from '../hooks/useSystemMachineState'; -const TopBarWithUser = ({ history }) => { +const TopBarWithUser = () => { const userData = useSystemMachineState('root', (state) => state.context.userData); + const rootMachine = useSystemMachine('root'); return ( { character: userData?.avatar_character, background: userData?.avatar_background }} - onIconClick={() => history.push('./')} - onAvatarClick={() => history.push('./profile')} + onIconClick={() => rootMachine.send({ type: 'pushLocation', location: './' })} + onAvatarClick={() => rootMachine.send({ type: 'pushLocation', location: './profile' })} /> ); }; -TopBarWithUser.propTypes = { - history: PropTypes.object.isRequired -}; - export default TopBarWithUser; diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index cfc1675..44a729f 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -1,4 +1,5 @@ -import { setup, spawnChild, sendTo, assign, fromPromise } from 'xstate'; +import { setup, spawnChild, sendTo, assign, fromPromise, fromCallback } from 'xstate'; +import history from 'history/browser'; import { getUser } from '@timo/common/api'; import customizeUserMachine from './customizeUser'; import loginMachine from './login'; @@ -9,10 +10,27 @@ import newEntriesMachine from './newEntry'; const rootMachine = setup({ actors: { - getUser: fromPromise(getUser) + getUser: fromPromise(getUser), + history: fromCallback(({ sendBack, receive }) => { + history.listen((location) => { + sendBack({ + type: 'locationChanged', + location + }); + }); + receive((event) => { + if (event.type === 'pushLocation') { + history.push(event.location); + } + if (event.type === 'replaceLocation') { + history.replace(event.location); + } + }); + }) } }).createMachine({ entry: [ + spawnChild('history', { systemId: 'history' }), spawnChild(loginMachine, { systemId: 'login' }), spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), spawnChild(changePasswordMachine, { systemId: 'changePassword' }), @@ -22,7 +40,8 @@ const rootMachine = setup({ ], initial: 'unknown', context: { - userData: null + userData: null, + currentPath: history.location.pathname }, states: { 'unknown': { @@ -77,26 +96,69 @@ const rootMachine = setup({ }) }) }, - unauthenticate: { - target: 'unauthenticated', + locationChanged: { actions: assign({ - userData: null + currentPath: ({ event }) => event.location.pathname }) + }, + pushLocation: { + actions: [ + sendTo( + ({ system }) => system.get('history'), + ({ event }) => ({ + type: 'pushLocation', + location: event.location + }) + ) + ] + }, + replaceLocation: { + actions: sendTo( + ({ system }) => system.get('history'), + ({ event }) => ({ + type: 'replaceLocation', + location: event.location + }) + ) + }, + unauthenticate: { + target: 'unauthenticated' } } }, 'unauthenticated': { + entry: [ + assign({ + userData: null + }), + sendTo( + ({ system }) => system.get('history'), + { + type: 'pushLocation', + location: './login' + } + ) + ], on: { authenticate: { target: 'authenticated', - actions: assign({ - userData:({ event }) => ({ - id: event.params.id, - username: event.params.username, - avatar_character: event.params.avatar_character, - avatar_background: event.params.avatar_background - }) - }) + actions: [ + assign({ + userData:({ event }) => ({ + id: event.params.id, + username: event.params.username, + avatar_character: event.params.avatar_character, + avatar_background: event.params.avatar_background + }) + }), + sendTo( + ({ system }) => system.get('history'), + { + type: 'replaceLocation', + location: './' + } + ) + ] } } } diff --git a/packages/xstate/src/routes/Entries/Entries.jsx b/packages/xstate/src/routes/Entries/Entries.jsx index 170733e..84ad697 100644 --- a/packages/xstate/src/routes/Entries/Entries.jsx +++ b/packages/xstate/src/routes/Entries/Entries.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import Entry from '@timo/common/components/Entry'; import Title from '@timo/common/components/Title'; import Input from '@timo/common/components/Input'; @@ -10,7 +9,7 @@ import { ButtonVariants } from '@timo/common/components/Button/Button'; import useSystemMachine from '../../hooks/useSystemMachine'; import useSystemMachineState from '../../hooks/useSystemMachineState'; -const Entries = ({ history }) => { +const Entries = () => { const { groupedEntries, totalDuration, @@ -19,6 +18,7 @@ const Entries = ({ history }) => { itemStatusMessage } = useSystemMachineState('entries', state => state.context); const entriesMachine = useSystemMachine('entries'); + const rootMachine = useSystemMachine('root'); const handleEdit = (updatedEntry) => { entriesMachine.send({ @@ -45,7 +45,10 @@ const Entries = ({ history }) => { }; const handleNewClick = () => { - history.push('./new'); + rootMachine.send({ + type: 'pushLocation', + location: './new' + }); }; return ( @@ -94,8 +97,4 @@ const Entries = ({ history }) => { ); }; -Entries.propTypes = { - history: PropTypes.object.isRequired -}; - export default Entries; diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx index 5e3eddf..da5add0 100644 --- a/packages/xstate/src/routes/Login/Login.jsx +++ b/packages/xstate/src/routes/Login/Login.jsx @@ -1,5 +1,3 @@ -import { useEffect } from 'react'; -import PropTypes from 'prop-types'; import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import Title from '@timo/common/components/Title'; @@ -9,17 +7,10 @@ import useSystemMachineState from '../../hooks/useSystemMachineState'; import styles from './Login.module.css'; -const Login = ({ history }) => { - const authState = useSystemMachineState('root', (state) => state.value); +const Login = () => { const statusMessage = useSystemMachineState('login', (state) => state.context.statusMessage); const loginMachine = useSystemMachine('login'); - useEffect(() => { - if (authState === 'authenticated') { - history.replace('./'); - } - }, [history, authState]); - const handleFormSubmit = (e) => { e.preventDefault(); const action = e.nativeEvent.submitter.value; @@ -61,10 +52,4 @@ const Login = ({ history }) => { ); }; -Login.propTypes = { - history: PropTypes.shape({ - replace: PropTypes.func.isRequired - }).isRequired -}; - export default Login; \ No newline at end of file From ba61d8f5ff14952fc8c89ae98ceafef5d7be22cb Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Thu, 24 Oct 2024 20:01:30 +0200 Subject: [PATCH 18/26] fixed bugs with history --- packages/xstate/src/App.jsx | 2 +- packages/xstate/src/components/Router.jsx | 5 ++-- packages/xstate/src/machines/root.js | 28 +++++++++++++++-------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx index a53b3aa..42dfdf9 100644 --- a/packages/xstate/src/App.jsx +++ b/packages/xstate/src/App.jsx @@ -1,4 +1,3 @@ -import Router from '@timo/common/components/Router'; import Container from '@timo/common/components/Container'; import Title from '@timo/common/components/Title'; @@ -8,6 +7,7 @@ import NewEntry from './routes/NewEntry/NewEntry'; import Profile from './routes/Profile/Profile'; import TopBarWithUser from './contextualComponents/TopBarWithUser'; import MachineContextProvider from './context/MachineContext'; +import Router from './components/Router'; const routes = [ { path: '/', name: 'Entries' }, diff --git a/packages/xstate/src/components/Router.jsx b/packages/xstate/src/components/Router.jsx index 30b74b0..6d18085 100644 --- a/packages/xstate/src/components/Router.jsx +++ b/packages/xstate/src/components/Router.jsx @@ -5,13 +5,14 @@ const BASE_URL = import.meta.env.VITE_BASE_URL; const Router = ({ routes, children }) => { const currentPath = useSystemMachineState('root', (state) => state.context.currentPath); + if (!currentPath) { + return null; + } const currentRoute = routes.find(route => `${BASE_URL}${route.path}` === currentPath); - if (!currentRoute) { return children(null); } - return children(currentRoute.name); }; diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 44a729f..3dd84f9 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -12,7 +12,7 @@ const rootMachine = setup({ actors: { getUser: fromPromise(getUser), history: fromCallback(({ sendBack, receive }) => { - history.listen((location) => { + history.listen(({ location }) => { sendBack({ type: 'locationChanged', location @@ -31,17 +31,12 @@ const rootMachine = setup({ }).createMachine({ entry: [ spawnChild('history', { systemId: 'history' }), - spawnChild(loginMachine, { systemId: 'login' }), - spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), - spawnChild(changePasswordMachine, { systemId: 'changePassword' }), - spawnChild(profileMachine, { systemId: 'profile' }), - spawnChild(entriesMachine, { systemId: 'entries' }), - spawnChild(newEntriesMachine, { systemId: 'newEntry' }) + spawnChild(loginMachine, { systemId: 'login' }) ], initial: 'unknown', context: { userData: null, - currentPath: history.location.pathname + currentPath: null }, states: { 'unknown': { @@ -65,6 +60,16 @@ const rootMachine = setup({ }, 'authenticated': { entry: [ + spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), + spawnChild(changePasswordMachine, { systemId: 'changePassword' }), + spawnChild(profileMachine, { systemId: 'profile' }), + spawnChild(entriesMachine, { systemId: 'entries' }), + spawnChild(newEntriesMachine, { systemId: 'newEntry' }), + + assign({ + currentPath: history.location.pathname + }), + sendTo( ({ system }) => system.get('customizeUser'), ({ context }) => ({ @@ -134,12 +139,17 @@ const rootMachine = setup({ sendTo( ({ system }) => system.get('history'), { - type: 'pushLocation', + type: 'replaceLocation', location: './login' } ) ], on: { + locationChanged: { + actions: assign({ + currentPath: ({ event }) => event.location.pathname + }) + }, authenticate: { target: 'authenticated', actions: [ From 32e84cc65c9ed7073da6b7e7624de5657979eb1f Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 25 Oct 2024 11:33:39 +0200 Subject: [PATCH 19/26] removed reference to non existing register route --- packages/jotai/src/App.jsx | 1 - packages/mobx/src/App.jsx | 1 - packages/react/src/App.jsx | 1 - packages/tanstack-query/src/App.jsx | 1 - packages/valtio/src/App.jsx | 1 - packages/xstate/src/App.jsx | 1 - packages/zustand/src/App.jsx | 1 - 7 files changed, 7 deletions(-) diff --git a/packages/jotai/src/App.jsx b/packages/jotai/src/App.jsx index 9abc093..e45e1d9 100644 --- a/packages/jotai/src/App.jsx +++ b/packages/jotai/src/App.jsx @@ -13,7 +13,6 @@ import Router from './components/Router'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; diff --git a/packages/mobx/src/App.jsx b/packages/mobx/src/App.jsx index 1632b33..5596517 100644 --- a/packages/mobx/src/App.jsx +++ b/packages/mobx/src/App.jsx @@ -16,7 +16,6 @@ import Profile from './routes/Profile/Profile'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; diff --git a/packages/react/src/App.jsx b/packages/react/src/App.jsx index a945235..fbcbd95 100644 --- a/packages/react/src/App.jsx +++ b/packages/react/src/App.jsx @@ -13,7 +13,6 @@ import Profile from './routes/Profile/Profile'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; diff --git a/packages/tanstack-query/src/App.jsx b/packages/tanstack-query/src/App.jsx index 77d9680..1795471 100644 --- a/packages/tanstack-query/src/App.jsx +++ b/packages/tanstack-query/src/App.jsx @@ -17,7 +17,6 @@ import Profile from './routes/Profile/Profile'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; diff --git a/packages/valtio/src/App.jsx b/packages/valtio/src/App.jsx index 1632b33..5596517 100644 --- a/packages/valtio/src/App.jsx +++ b/packages/valtio/src/App.jsx @@ -16,7 +16,6 @@ import Profile from './routes/Profile/Profile'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx index 42dfdf9..7f6ffc8 100644 --- a/packages/xstate/src/App.jsx +++ b/packages/xstate/src/App.jsx @@ -12,7 +12,6 @@ import Router from './components/Router'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; diff --git a/packages/zustand/src/App.jsx b/packages/zustand/src/App.jsx index 43b388f..9ed5ec0 100644 --- a/packages/zustand/src/App.jsx +++ b/packages/zustand/src/App.jsx @@ -16,7 +16,6 @@ import ProtectedRoute from './contextualComponents/ProtectedRoute'; const routes = [ { path: '/', name: 'Entries' }, { path: '/login', name: 'Login' }, - { path: '/register', name: 'Register' }, { path: '/new', name: 'NewEntry' }, { path: '/profile', name: 'Profile' } ]; From bfeaa726e2f95327d47419a022e7f1251e3332af Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 25 Oct 2024 14:07:54 +0200 Subject: [PATCH 20/26] refactored root machine to contain route state and switched to invoked machines instead of spawned machines --- packages/xstate/src/App.jsx | 22 +- packages/xstate/src/components/Router.jsx | 28 +-- packages/xstate/src/components/Timer.jsx | 4 +- .../contextualComponents/TopBarWithUser.jsx | 12 +- packages/xstate/src/hooks/useChildMachine.js | 10 + .../xstate/src/hooks/useChildMachineState.js | 9 + packages/xstate/src/hooks/useRootMachine.js | 8 + packages/xstate/src/hooks/useSystemMachine.js | 9 - .../xstate/src/hooks/useSystemMachineState.js | 9 - packages/xstate/src/machines/newEntry.js | 4 +- packages/xstate/src/machines/root.js | 214 ++++++++++-------- .../xstate/src/routes/Entries/Entries.jsx | 15 +- packages/xstate/src/routes/Login/Login.jsx | 8 +- .../xstate/src/routes/NewEntry/NewEntry.jsx | 10 +- .../xstate/src/routes/Profile/Profile.jsx | 4 +- .../Profile/sections/ChangePassword.jsx | 8 +- .../routes/Profile/sections/CustomizeUser.jsx | 8 +- 17 files changed, 200 insertions(+), 182 deletions(-) create mode 100644 packages/xstate/src/hooks/useChildMachine.js create mode 100644 packages/xstate/src/hooks/useChildMachineState.js create mode 100644 packages/xstate/src/hooks/useRootMachine.js delete mode 100644 packages/xstate/src/hooks/useSystemMachine.js delete mode 100644 packages/xstate/src/hooks/useSystemMachineState.js diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx index 7f6ffc8..b4910a9 100644 --- a/packages/xstate/src/App.jsx +++ b/packages/xstate/src/App.jsx @@ -1,5 +1,4 @@ import Container from '@timo/common/components/Container'; -import Title from '@timo/common/components/Title'; import Login from './routes/Login/Login'; import Entries from './routes/Entries/Entries'; @@ -9,41 +8,30 @@ import TopBarWithUser from './contextualComponents/TopBarWithUser'; import MachineContextProvider from './context/MachineContext'; import Router from './components/Router'; -const routes = [ - { path: '/', name: 'Entries' }, - { path: '/login', name: 'Login' }, - { path: '/new', name: 'NewEntry' }, - { path: '/profile', name: 'Profile' } -]; - const App = () => ( - + {(routeName) => { let pageComponent = null; switch (routeName) { - case 'Login': + case 'login': pageComponent = ; break; - case 'NewEntry': + case 'newEntry': pageComponent = ( ); break; - case 'Entries': + case 'entries': pageComponent = ( ); break; - case 'Profile': + case 'profile': pageComponent = ( ); break; - default: - pageComponent = ( - Page not found - ); } return ( diff --git a/packages/xstate/src/components/Router.jsx b/packages/xstate/src/components/Router.jsx index 6d18085..d8029be 100644 --- a/packages/xstate/src/components/Router.jsx +++ b/packages/xstate/src/components/Router.jsx @@ -1,28 +1,18 @@ import PropTypes from 'prop-types'; -import useSystemMachineState from '../hooks/useSystemMachineState'; +import useRootMachine from '../hooks/useRootMachine'; +import { useSelector } from '@xstate/react'; -const BASE_URL = import.meta.env.VITE_BASE_URL; - -const Router = ({ routes, children }) => { - const currentPath = useSystemMachineState('root', (state) => state.context.currentPath); - if (!currentPath) { - return null; - } - - const currentRoute = routes.find(route => `${BASE_URL}${route.path}` === currentPath); - if (!currentRoute) { - return children(null); +const Router = ({ children }) => { + const rootMachine = useRootMachine(); + const state = useSelector(rootMachine, state => state.value); + const route = state?.authenticated || state?.unauthenticated; + if (route && route !== 'unknown') { + return children(route); } - return children(currentRoute.name); + return null; }; Router.propTypes = { - routes: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }) - ).isRequired, children: PropTypes.func.isRequired }; diff --git a/packages/xstate/src/components/Timer.jsx b/packages/xstate/src/components/Timer.jsx index 7969594..d7e2d22 100644 --- a/packages/xstate/src/components/Timer.jsx +++ b/packages/xstate/src/components/Timer.jsx @@ -1,8 +1,8 @@ import styles from '@timo/common/components/Timer/Timer.module.css'; -import useSystemMachineState from '../hooks/useSystemMachineState'; +import useChildMachineState from '../hooks/useChildMachineState'; const Timer = () => { - const timerValue = useSystemMachineState('newEntry', state => state.context.timerValue); + const timerValue = useChildMachineState('newEntry', state => state.context.timerValue); // Format duration to HH:MM:SS const formattedValue = new Date(timerValue * 1000).toISOString().slice(11, 19); diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx index d60303a..041f97c 100644 --- a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx +++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx @@ -1,10 +1,10 @@ import TopBar from '@timo/common/components/TopBar'; -import useSystemMachine from '../hooks/useSystemMachine'; -import useSystemMachineState from '../hooks/useSystemMachineState'; +import useRootMachine from '../hooks/useRootMachine'; +import { useSelector } from '@xstate/react'; const TopBarWithUser = () => { - const userData = useSystemMachineState('root', (state) => state.context.userData); - const rootMachine = useSystemMachine('root'); + const rootMachine = useRootMachine(); + const userData = useSelector(rootMachine, (state) => state.context.userData); return ( { character: userData?.avatar_character, background: userData?.avatar_background }} - onIconClick={() => rootMachine.send({ type: 'pushLocation', location: './' })} - onAvatarClick={() => rootMachine.send({ type: 'pushLocation', location: './profile' })} + onIconClick={() => rootMachine.send({ type: 'pushRoute', route: 'entries' })} + onAvatarClick={() => rootMachine.send({ type: 'pushRoute', route: 'profile' })} /> ); }; diff --git a/packages/xstate/src/hooks/useChildMachine.js b/packages/xstate/src/hooks/useChildMachine.js new file mode 100644 index 0000000..2a71034 --- /dev/null +++ b/packages/xstate/src/hooks/useChildMachine.js @@ -0,0 +1,10 @@ +import { useSelector } from '@xstate/react'; +import useRootMachine from './useRootMachine'; + +const useChildMachine = (childId) => { + const rootMachine = useRootMachine(); + const childMachines = useSelector(rootMachine, (state) => state.children); + return childMachines[childId]; +}; + +export default useChildMachine; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useChildMachineState.js b/packages/xstate/src/hooks/useChildMachineState.js new file mode 100644 index 0000000..782dd21 --- /dev/null +++ b/packages/xstate/src/hooks/useChildMachineState.js @@ -0,0 +1,9 @@ +import { useSelector } from '@xstate/react'; +import useChildMachine from './useChildMachine'; + +const useChildMachineState = (childId, selector) => { + const machine = useChildMachine(childId); + return useSelector(machine, selector); +}; + +export default useChildMachineState; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useRootMachine.js b/packages/xstate/src/hooks/useRootMachine.js new file mode 100644 index 0000000..082c661 --- /dev/null +++ b/packages/xstate/src/hooks/useRootMachine.js @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import { MachineContext } from '../context/MachineContext'; + +const useRootMachine = () => { + return useContext(MachineContext); +}; + +export default useRootMachine; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useSystemMachine.js b/packages/xstate/src/hooks/useSystemMachine.js deleted file mode 100644 index ca36e2a..0000000 --- a/packages/xstate/src/hooks/useSystemMachine.js +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from 'react'; -import { MachineContext } from '../context/MachineContext'; - -const useSystemMachine = (systemId) => { - const machine = useContext(MachineContext); - return machine.system.get(systemId); -}; - -export default useSystemMachine; \ No newline at end of file diff --git a/packages/xstate/src/hooks/useSystemMachineState.js b/packages/xstate/src/hooks/useSystemMachineState.js deleted file mode 100644 index 36e71c6..0000000 --- a/packages/xstate/src/hooks/useSystemMachineState.js +++ /dev/null @@ -1,9 +0,0 @@ -import { useSelector } from '@xstate/react'; -import useSystemMachine from './useSystemMachine'; - -const useSystemMachineState = (systemId, selector) => { - const machine = useSystemMachine(systemId); - return useSelector(machine, selector); -}; - -export default useSystemMachineState; \ No newline at end of file diff --git a/packages/xstate/src/machines/newEntry.js b/packages/xstate/src/machines/newEntry.js index dd1509d..6302676 100644 --- a/packages/xstate/src/machines/newEntry.js +++ b/packages/xstate/src/machines/newEntry.js @@ -1,7 +1,7 @@ import { setup, fromPromise, fromCallback, assign, sendTo } from 'xstate'; import { createEntry } from '@timo/common/api'; -const newEntriesMachine = setup({ +const newEntryMachine = setup({ actors: { createEntry: fromPromise(async ({ input }) => createEntry(input)), timer: fromCallback(({ sendBack, receive}) => { @@ -126,4 +126,4 @@ const newEntriesMachine = setup({ } }); -export default newEntriesMachine; \ No newline at end of file +export default newEntryMachine; \ No newline at end of file diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 3dd84f9..7816079 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -6,7 +6,16 @@ import loginMachine from './login'; import changePasswordMachine from './changePassword'; import profileMachine from './profile'; import entriesMachine from './entries'; -import newEntriesMachine from './newEntry'; +import newEntryMachine from './newEntry'; + +const BASE_URL = import.meta.env.VITE_BASE_URL; + +const routes = { + login: `${BASE_URL}/login`, + entries: `${BASE_URL}/`, + newEntry: `${BASE_URL}/new`, + profile: `${BASE_URL}/profile` +}; const rootMachine = setup({ actors: { @@ -19,24 +28,42 @@ const rootMachine = setup({ }); }); receive((event) => { + if (event.type === 'getLocation') { + sendBack({ + type: 'locationChanged', + location: history.location + }); + } if (event.type === 'pushLocation') { history.push(event.location); } if (event.type === 'replaceLocation') { - history.replace(event.location); + history.push(event.location); } }); - }) + }), + customizeUser: customizeUserMachine, + login: loginMachine, + changePassword: changePasswordMachine, + profile: profileMachine, + entries: entriesMachine, + newEntry: newEntryMachine + }, + guards: { + isNewEntryRoute: ({ event }) => { + return event.location.pathname === routes.newEntry; + }, + isProfileRoute: ({ event }) => { + return event.location.pathname === routes.profile; + } } }).createMachine({ entry: [ - spawnChild('history', { systemId: 'history' }), - spawnChild(loginMachine, { systemId: 'login' }) + spawnChild('history', { id: 'history' }) ], initial: 'unknown', context: { - userData: null, - currentPath: null + userData: null }, states: { 'unknown': { @@ -59,97 +86,107 @@ const rootMachine = setup({ } }, 'authenticated': { - entry: [ - spawnChild(customizeUserMachine, { systemId: 'customizeUser' }), - spawnChild(changePasswordMachine, { systemId: 'changePassword' }), - spawnChild(profileMachine, { systemId: 'profile' }), - spawnChild(entriesMachine, { systemId: 'entries' }), - spawnChild(newEntriesMachine, { systemId: 'newEntry' }), - - assign({ - currentPath: history.location.pathname - }), - - sendTo( - ({ system }) => system.get('customizeUser'), - ({ context }) => ({ - type: 'initialize', - params: { - userId: context.userData.id, - username: context.userData.username, - avatar_character: context.userData.avatar_character, - avatar_background: context.userData.avatar_background - } - }) - ), - sendTo( - ({ system }) => system.get('changePassword'), - ({ context }) => ({ - type: 'initialize', - username: context.userData.username - }) - ) - ], - on: { - updateUserData: { - actions: assign({ - userData: ({ event, context }) => ({ - id: context.userData.id, - username: event.params.username, - avatar_character: event.params.avatar_character, - avatar_background: event.params.avatar_background - }) - }) + invoke: { + // Invoke entries here so that it doesn't reload as we navigate across routes + src: 'entries', + id: 'entries', + systemId: 'entries' + }, + initial: 'unknown', + states: { + 'unknown': { + entry: [ + sendTo('history', { type: 'getLocation' }) + ] }, - locationChanged: { - actions: assign({ - currentPath: ({ event }) => event.location.pathname - }) + 'entries': {}, + 'newEntry': { + invoke: { + src: 'newEntry', + id: 'newEntry' + } }, - pushLocation: { - actions: [ + 'profile': { + invoke: [ + { + src: 'profile', + id: 'profile' + }, + { + src: 'customizeUser', + id: 'customizeUser' + }, + { + src: 'changePassword', + id: 'changePassword' + } + ], + entry: [ + sendTo( + 'customizeUser', + ({ context }) => ({ + type: 'initialize', + params: { + userId: context.userData.id, + username: context.userData.username, + avatar_character: context.userData.avatar_character, + avatar_background: context.userData.avatar_background + } + }) + ), sendTo( - ({ system }) => system.get('history'), - ({ event }) => ({ - type: 'pushLocation', - location: event.location + 'changePassword', + ({ context }) => ({ + type: 'initialize', + username: context.userData.username }) ) ] + } + }, + on: { + 'locationChanged': [ + { + target: '.newEntry', + guard: 'isNewEntryRoute' + }, + { + target: '.profile', + guard: 'isProfileRoute' + }, + // Fallback to entries route if non specified or not found + { + target: '.entries' + } + ], + 'pushRoute': { + actions: sendTo('history', ({ event }) => ({ + type: 'pushLocation', + location: routes[event.route] + })) }, - replaceLocation: { - actions: sendTo( - ({ system }) => system.get('history'), - ({ event }) => ({ - type: 'replaceLocation', - location: event.location - }) - ) - }, - unauthenticate: { + 'unauthenticate': { target: 'unauthenticated' } } }, 'unauthenticated': { - entry: [ - assign({ - userData: null - }), - sendTo( - ({ system }) => system.get('history'), - { - type: 'replaceLocation', - location: './login' + initial: 'login', + states: { + 'login': { + entry: [ + sendTo('history', { + type: 'replaceLocation', + location: routes.login + }) + ], + invoke: { + src: loginMachine, + id: 'login' } - ) - ], + } + }, on: { - locationChanged: { - actions: assign({ - currentPath: ({ event }) => event.location.pathname - }) - }, authenticate: { target: 'authenticated', actions: [ @@ -160,14 +197,7 @@ const rootMachine = setup({ avatar_character: event.params.avatar_character, avatar_background: event.params.avatar_background }) - }), - sendTo( - ({ system }) => system.get('history'), - { - type: 'replaceLocation', - location: './' - } - ) + }) ] } } diff --git a/packages/xstate/src/routes/Entries/Entries.jsx b/packages/xstate/src/routes/Entries/Entries.jsx index 84ad697..ef17b55 100644 --- a/packages/xstate/src/routes/Entries/Entries.jsx +++ b/packages/xstate/src/routes/Entries/Entries.jsx @@ -6,8 +6,9 @@ import StatusMessage from '@timo/common/components/StatusMessage'; import formatDuration from '@timo/common/utils/formatDuration'; import styles from './Entries.module.css'; import { ButtonVariants } from '@timo/common/components/Button/Button'; -import useSystemMachine from '../../hooks/useSystemMachine'; -import useSystemMachineState from '../../hooks/useSystemMachineState'; +import useChildMachineState from '../../hooks/useChildMachineState'; +import useChildMachine from '../../hooks/useChildMachine'; +import useRootMachine from '../../hooks/useRootMachine'; const Entries = () => { const { @@ -16,9 +17,9 @@ const Entries = () => { statusMessage, filter, itemStatusMessage - } = useSystemMachineState('entries', state => state.context); - const entriesMachine = useSystemMachine('entries'); - const rootMachine = useSystemMachine('root'); + } = useChildMachineState('entries', state => state.context); + const entriesMachine = useChildMachine('entries'); + const rootMachine = useRootMachine(); const handleEdit = (updatedEntry) => { entriesMachine.send({ @@ -46,8 +47,8 @@ const Entries = () => { const handleNewClick = () => { rootMachine.send({ - type: 'pushLocation', - location: './new' + type: 'pushRoute', + route: 'newEntry' }); }; diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx index da5add0..bfdafd0 100644 --- a/packages/xstate/src/routes/Login/Login.jsx +++ b/packages/xstate/src/routes/Login/Login.jsx @@ -2,14 +2,14 @@ import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import Title from '@timo/common/components/Title'; import StatusMessage from '@timo/common/components/StatusMessage'; -import useSystemMachine from '../../hooks/useSystemMachine'; -import useSystemMachineState from '../../hooks/useSystemMachineState'; +import useChildMachine from '../../hooks/useChildMachine'; +import useChildMachineState from '../../hooks/useChildMachineState'; import styles from './Login.module.css'; const Login = () => { - const statusMessage = useSystemMachineState('login', (state) => state.context.statusMessage); - const loginMachine = useSystemMachine('login'); + const statusMessage = useChildMachineState('login', (state) => state.context.statusMessage); + const loginMachine = useChildMachine('login'); const handleFormSubmit = (e) => { e.preventDefault(); diff --git a/packages/xstate/src/routes/NewEntry/NewEntry.jsx b/packages/xstate/src/routes/NewEntry/NewEntry.jsx index 1bfde94..548b2be 100644 --- a/packages/xstate/src/routes/NewEntry/NewEntry.jsx +++ b/packages/xstate/src/routes/NewEntry/NewEntry.jsx @@ -2,15 +2,15 @@ import Title from '@timo/common/components/Title'; import Input from '@timo/common/components/Input'; import Button, { ButtonVariants } from '@timo/common/components/Button'; import StatusMessage from '@timo/common/components/StatusMessage'; -import useSystemMachineState from '../../hooks/useSystemMachineState'; -import useSystemMachine from '../../hooks/useSystemMachine'; +import useChildMachine from '../../hooks/useChildMachine'; +import useChildMachineState from '../../hooks/useChildMachineState'; import Timer from '../../components/Timer'; import styles from './NewEntry.module.css'; const NewEntry = () => { - const timerState = useSystemMachineState('newEntry', state => state.value); - const statusMessage = useSystemMachineState('newEntry', state => state.context.statusMessage); - const newEntryMachine = useSystemMachine('newEntry'); + const timerState = useChildMachineState('newEntry', state => state.value); + const statusMessage = useChildMachineState('newEntry', state => state.context.statusMessage); + const newEntryMachine = useChildMachine('newEntry'); const handleSubmit = (e) => { e.preventDefault(); diff --git a/packages/xstate/src/routes/Profile/Profile.jsx b/packages/xstate/src/routes/Profile/Profile.jsx index 81fa475..d893967 100644 --- a/packages/xstate/src/routes/Profile/Profile.jsx +++ b/packages/xstate/src/routes/Profile/Profile.jsx @@ -3,10 +3,10 @@ import Button, { ButtonVariants } from '@timo/common/components/Button'; import styles from './Profile.module.css'; import ChangePassword from './sections/ChangePassword'; import CustomizeUser from './sections/CustomizeUser'; -import useSystemMachine from '../../hooks/useSystemMachine'; +import useChildMachine from '../../hooks/useChildMachine'; const Profile = () => { - const profileMachine = useSystemMachine('profile'); + const profileMachine = useChildMachine('profile'); const handleLogoutClick = () => { profileMachine.send({ type: 'logout' }); diff --git a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx index 6a293be..7d2684f 100644 --- a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx +++ b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx @@ -2,12 +2,12 @@ import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; import styles from '../Profile.module.css'; -import useSystemMachineState from '../../../hooks/useSystemMachineState'; -import useSystemMachine from '../../../hooks/useSystemMachine'; +import useChildMachine from '../../../hooks/useChildMachine'; +import useChildMachineState from '../../../hooks/useChildMachineState'; const ChangePassword = () => { - const { statusMessage } = useSystemMachineState('changePassword', state => state.context); - const changePasswordMachine = useSystemMachine('changePassword'); + const { statusMessage } = useChildMachineState('changePassword', state => state.context); + const changePasswordMachine = useChildMachine('changePassword'); const handlePasswordFormSubmit = (e) => { e.preventDefault(); diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx index 78210f3..b4413e7 100644 --- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx +++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx @@ -3,18 +3,18 @@ import RadioGroup from '@timo/common/components/RadioGroup'; import Input from '@timo/common/components/Input'; import StatusMessage from '@timo/common/components/StatusMessage'; import Button from '@timo/common/components/Button'; -import useSystemMachineState from '../../../hooks/useSystemMachineState'; -import useSystemMachine from '../../../hooks/useSystemMachine'; +import useChildMachine from '../../../hooks/useChildMachine'; +import useChildMachineState from '../../../hooks/useChildMachineState'; import styles from '../Profile.module.css'; const CustomizeUser = () => { - const customizeUserMachine = useSystemMachine('customizeUser'); + const customizeUserMachine = useChildMachine('customizeUser'); const { username, avatar_character, avatar_background, statusMessage - } = useSystemMachineState('customizeUser', state => state.context); + } = useChildMachineState('customizeUser', state => state.context); const handleCustomizeFormSubmit = (e) => { e.preventDefault(); From 43d9a21e79ca4f4c0756c96a94cf720275221541 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 25 Oct 2024 14:10:09 +0200 Subject: [PATCH 21/26] removed unnecessary array --- packages/xstate/src/machines/root.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 7816079..6b3bd31 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -189,16 +189,14 @@ const rootMachine = setup({ on: { authenticate: { target: 'authenticated', - actions: [ - assign({ - userData:({ event }) => ({ - id: event.params.id, - username: event.params.username, - avatar_character: event.params.avatar_character, - avatar_background: event.params.avatar_background - }) + actions: assign({ + userData:({ event }) => ({ + id: event.params.id, + username: event.params.username, + avatar_character: event.params.avatar_character, + avatar_background: event.params.avatar_background }) - ] + }) } } } From f51b7e8f0bc712fcaaaca1856d7ce6babd70a3fc Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 25 Oct 2024 17:23:04 +0200 Subject: [PATCH 22/26] added id --- packages/xstate/src/machines/entries.js | 1 + packages/xstate/src/machines/login.js | 1 + packages/xstate/src/machines/newEntry.js | 1 + packages/xstate/src/machines/root.js | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/xstate/src/machines/entries.js b/packages/xstate/src/machines/entries.js index 577d537..0a7bbef 100644 --- a/packages/xstate/src/machines/entries.js +++ b/packages/xstate/src/machines/entries.js @@ -40,6 +40,7 @@ const entriesMachine = setup({ deleteEntry: fromPromise(async ({ input }) => deleteEntry(input)) } }).createMachine({ + id: 'entries', initial: 'loading', context: { groupedEntries: [], diff --git a/packages/xstate/src/machines/login.js b/packages/xstate/src/machines/login.js index 2397ee7..c5d2708 100644 --- a/packages/xstate/src/machines/login.js +++ b/packages/xstate/src/machines/login.js @@ -13,6 +13,7 @@ const loginMachine = setup({ ) } }).createMachine({ + id: 'login', context: { statusMessage: null }, diff --git a/packages/xstate/src/machines/newEntry.js b/packages/xstate/src/machines/newEntry.js index 6302676..337a627 100644 --- a/packages/xstate/src/machines/newEntry.js +++ b/packages/xstate/src/machines/newEntry.js @@ -18,6 +18,7 @@ const newEntryMachine = setup({ }) } }).createMachine({ + id: 'newEntry', initial: 'idle', context: { timerValue: 0, diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index 6b3bd31..ef8cac8 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -58,6 +58,7 @@ const rootMachine = setup({ } } }).createMachine({ + id: 'root', entry: [ spawnChild('history', { id: 'history' }) ], From a1acda90cc001740de03bf5dad7279e47cc82fb3 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Wed, 30 Oct 2024 19:14:57 +0100 Subject: [PATCH 23/26] removed unnecessary assignment --- packages/xstate/src/hooks/useChildMachine.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/xstate/src/hooks/useChildMachine.js b/packages/xstate/src/hooks/useChildMachine.js index 2a71034..59b0f65 100644 --- a/packages/xstate/src/hooks/useChildMachine.js +++ b/packages/xstate/src/hooks/useChildMachine.js @@ -3,8 +3,7 @@ import useRootMachine from './useRootMachine'; const useChildMachine = (childId) => { const rootMachine = useRootMachine(); - const childMachines = useSelector(rootMachine, (state) => state.children); - return childMachines[childId]; + return useSelector(rootMachine, (state) => state.children[childId]); }; export default useChildMachine; \ No newline at end of file From c2c1de4104e4c92ba83b97066924c9e1357e8867 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 1 Nov 2024 13:13:12 +0100 Subject: [PATCH 24/26] implemented useRootMachineState --- packages/xstate/src/components/Router.jsx | 6 ++---- .../xstate/src/contextualComponents/TopBarWithUser.jsx | 4 ++-- packages/xstate/src/hooks/useRootMachineState.js | 9 +++++++++ 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 packages/xstate/src/hooks/useRootMachineState.js diff --git a/packages/xstate/src/components/Router.jsx b/packages/xstate/src/components/Router.jsx index d8029be..08eae91 100644 --- a/packages/xstate/src/components/Router.jsx +++ b/packages/xstate/src/components/Router.jsx @@ -1,10 +1,8 @@ import PropTypes from 'prop-types'; -import useRootMachine from '../hooks/useRootMachine'; -import { useSelector } from '@xstate/react'; +import useRootMachineState from '../hooks/useRootMachineState'; const Router = ({ children }) => { - const rootMachine = useRootMachine(); - const state = useSelector(rootMachine, state => state.value); + const state = useRootMachineState(state => state.value); const route = state?.authenticated || state?.unauthenticated; if (route && route !== 'unknown') { return children(route); diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx index 041f97c..d1cb48a 100644 --- a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx +++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx @@ -1,10 +1,10 @@ import TopBar from '@timo/common/components/TopBar'; import useRootMachine from '../hooks/useRootMachine'; -import { useSelector } from '@xstate/react'; +import useRootMachineState from '../hooks/useRootMachineState'; const TopBarWithUser = () => { const rootMachine = useRootMachine(); - const userData = useSelector(rootMachine, (state) => state.context.userData); + const userData = useRootMachineState(state => state.context.userData); return ( { + const rootMachine = useRootMachine(); + return useSelector(rootMachine, selector); +}; + +export default useRootMachineState; \ No newline at end of file From 564a44088f0fc988d380cf330f9742f030bf0e89 Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Fri, 1 Nov 2024 13:46:46 +0100 Subject: [PATCH 25/26] replaced initialize events with inputs --- .../xstate/src/machines/changePassword.js | 11 ++----- packages/xstate/src/machines/customizeUser.js | 20 ++++-------- packages/xstate/src/machines/root.js | 32 ++++++------------- 3 files changed, 19 insertions(+), 44 deletions(-) diff --git a/packages/xstate/src/machines/changePassword.js b/packages/xstate/src/machines/changePassword.js index 8156d4a..90bda1c 100644 --- a/packages/xstate/src/machines/changePassword.js +++ b/packages/xstate/src/machines/changePassword.js @@ -8,18 +8,13 @@ const changePasswordMachine = setup({ }).createMachine({ id: 'changePassword', initial: 'idle', - context: { - username: null, + context: ({ input }) => ({ + username: input.username, statusMessage: null - }, + }), states: { 'idle': { on: { - 'initialize': { - actions: assign(({ event }) => ({ - username: event.username - })) - }, 'save': { target: 'saving' } diff --git a/packages/xstate/src/machines/customizeUser.js b/packages/xstate/src/machines/customizeUser.js index 46c55b3..c090fa2 100644 --- a/packages/xstate/src/machines/customizeUser.js +++ b/packages/xstate/src/machines/customizeUser.js @@ -8,24 +8,16 @@ const customizeUserMachine = setup({ }).createMachine({ id: 'customizeUser', initial: 'idle', - context: { - userId: null, - username: null, - avatar_background: null, - avatar_character: null, + context: ({ input }) => ({ + userId: input.userId, + username: input.username, + avatar_background: input.avatar_background, + avatar_character: input.avatar_character, statusMessage: null - }, + }), states: { 'idle': { on: { - 'initialize': { - actions: assign(({ event }) => ({ - userId: event.params.userId, - username: event.params.username, - avatar_background: event.params.avatar_background, - avatar_character: event.params.avatar_character - })) - }, 'changeAvatarCharacter': { actions: assign(({ event }) => ({ avatar_character: event.value diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js index ef8cac8..e91831a 100644 --- a/packages/xstate/src/machines/root.js +++ b/packages/xstate/src/machines/root.js @@ -115,33 +115,21 @@ const rootMachine = setup({ }, { src: 'customizeUser', - id: 'customizeUser' + id: 'customizeUser', + input: ({ context }) => ({ + userId: context.userData.id, + username: context.userData.username, + avatar_character: context.userData.avatar_character, + avatar_background: context.userData.avatar_background + }) }, { src: 'changePassword', - id: 'changePassword' - } - ], - entry: [ - sendTo( - 'customizeUser', - ({ context }) => ({ - type: 'initialize', - params: { - userId: context.userData.id, - username: context.userData.username, - avatar_character: context.userData.avatar_character, - avatar_background: context.userData.avatar_background - } - }) - ), - sendTo( - 'changePassword', - ({ context }) => ({ - type: 'initialize', + id: 'changePassword', + input: ({ context }) => ({ username: context.userData.username }) - ) + } ] } }, From ff3da74799835c0c8ad4ed0007502c740105b36e Mon Sep 17 00:00:00 2001 From: Prabashwara Seneviratne Date: Wed, 18 Dec 2024 13:19:52 +0100 Subject: [PATCH 26/26] added robots.txt --- prepare-deploy.js | 5 ++++- public/robots.txt | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 public/robots.txt diff --git a/prepare-deploy.js b/prepare-deploy.js index 5afd57e..c65eb8c 100644 --- a/prepare-deploy.js +++ b/prepare-deploy.js @@ -15,4 +15,7 @@ fs.readdirSync(source, { withFileTypes: true }).forEach(dirent => { fs.cpSync(distPath, outputPath, { recursive: true }); } } -}); \ No newline at end of file +}); + +const public = path.join(__dirname, 'public'); +fs.cpSync(public, destination, { recursive: true }); \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file