diff --git a/packages/web/package.json b/packages/web/package.json index 4d5ed509..b65cbe61 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -8,6 +8,7 @@ "@automatisch/types": "0.1.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", + "@hookform/resolvers": "^2.8.8", "@mui/icons-material": "^5.0.1", "@mui/lab": "^5.0.0-alpha.60", "@mui/material": "^5.0.2", @@ -35,7 +36,8 @@ "slate-history": "^0.66.0", "slate-react": "^0.72.9", "typescript": "^4.1.2", - "web-vitals": "^1.0.1" + "web-vitals": "^1.0.1", + "yup": "^0.32.11" }, "main": "index.js", "scripts": { diff --git a/packages/web/src/components/Form/index.tsx b/packages/web/src/components/Form/index.tsx index 2c0a7d5a..b430a125 100644 --- a/packages/web/src/components/Form/index.tsx +++ b/packages/web/src/components/Form/index.tsx @@ -7,14 +7,26 @@ type FormProps = { defaultValues?: UseFormProps['defaultValues']; onSubmit?: SubmitHandler; render?: (props: UseFormReturn) => React.ReactNode; + resolver?: UseFormProps["resolver"]; + mode?: UseFormProps["mode"]; }; const noop = () => null; export default function Form(props: FormProps): React.ReactElement { - const { children, onSubmit = noop, defaultValues, render, ...formProps } = props; + const { + children, + onSubmit = noop, + defaultValues, + resolver, + render, + mode, + ...formProps + } = props; const methods: UseFormReturn = useForm({ defaultValues, + resolver, + mode, }); React.useEffect(() => { diff --git a/packages/web/src/graphql/mutations/update-user.ts b/packages/web/src/graphql/mutations/update-user.ts new file mode 100644 index 00000000..fa188cc7 --- /dev/null +++ b/packages/web/src/graphql/mutations/update-user.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_USER = gql` + mutation UpdateUser($input: UpdateUserInput) { + updateUser(input: $input) { + id + email + updatedAt + } + } +`; diff --git a/packages/web/src/graphql/queries/get-current-user.ts b/packages/web/src/graphql/queries/get-current-user.ts new file mode 100644 index 00000000..487b6730 --- /dev/null +++ b/packages/web/src/graphql/queries/get-current-user.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const GET_CURRENT_USER = gql` + query GetCurrentUser { + getCurrentUser { + id + email + createdAt + updatedAt + } + } +`; diff --git a/packages/web/src/hooks/useCurrentUser.ts b/packages/web/src/hooks/useCurrentUser.ts new file mode 100644 index 00000000..40f9287c --- /dev/null +++ b/packages/web/src/hooks/useCurrentUser.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@apollo/client'; +import { IUser } from '@automatisch/types'; + +import { GET_CURRENT_USER } from 'graphql/queries/get-current-user'; + +export default function useCurrentUser(): IUser { + const { data } = useQuery(GET_CURRENT_USER, { fetchPolicy: 'cache-and-network' }); + + return data?.getCurrentUser; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 0a1dc4f7..6b86a817 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -43,9 +43,11 @@ "flows.title": "Flows", "executions.title": "Executions", "profileSettings.title": "My Profile", - "profileSettings.email": "E-mail", - "profileSettings.updateEmail": "Update e-mail", + "profileSettings.email": "Email", + "profileSettings.updateEmail": "Update email", "profileSettings.newPassword": "New password", "profileSettings.confirmNewPassword": "Confirm new password", + "profileSettings.updatedEmail": "Your email has been updated.", + "profileSettings.updatedPassword": "Your password has been updated.", "profileSettings.updatePassword": "Update password" } diff --git a/packages/web/src/pages/ProfileSettings/index.tsx b/packages/web/src/pages/ProfileSettings/index.tsx index 0ae0bff9..51c93f77 100644 --- a/packages/web/src/pages/ProfileSettings/index.tsx +++ b/packages/web/src/pages/ProfileSettings/index.tsx @@ -1,13 +1,28 @@ import * as React from 'react'; +import { useMutation } from '@apollo/client'; import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Grid'; import Button from '@mui/material/Button'; +import { useSnackbar } from 'notistack'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from "yup"; import PageTitle from 'components/PageTitle'; import Container from 'components/Container'; import Form from 'components/Form'; import TextField from 'components/TextField'; +import { UPDATE_USER } from 'graphql/mutations/update-user'; import useFormatMessage from 'hooks/useFormatMessage'; +import useCurrentUser from 'hooks/useCurrentUser'; + +const emailValidationSchema = yup.object({ + email: yup.string().email().required(), +}).required(); + +const passwordValidationSchema = yup.object({ + password: yup.string().required(), + confirmPassword: yup.string().required().oneOf([yup.ref('password')], 'Passwords must match'), +}).required(); const StyledForm = styled(Form)` display: flex; @@ -16,11 +31,54 @@ const StyledForm = styled(Form)` `; function ProfileSettings() { + const [passwordDefaultValues, setPasswordDefaultValues] = React.useState({}); + const { enqueueSnackbar } = useSnackbar(); + const currentUser = useCurrentUser(); const formatMessage = useFormatMessage(); + const [updateUser] = useMutation(UPDATE_USER); + + const handleEmailUpdate = async (data: any) => { + const email = data.email; + + await updateUser({ + variables: { + input: { + email, + } + }, + optimisticResponse: { + updateUser: { + __typename: 'User', + email, + } + } + }); + + enqueueSnackbar(formatMessage('profileSettings.updatedEmail'), { variant: 'success' }); + } + + const handlePasswordUpdate = async (data: any) => { + const password = data.password; + + setPasswordDefaultValues({ + password, + confirmPassword: data.confirmPassword, + }) + + await updateUser({ + variables: { + input: { + password, + } + }, + }); + + enqueueSnackbar(formatMessage('profileSettings.updatedPassword'), { variant: 'success' }); + } return ( - + {formatMessage('profileSettings.title')} @@ -28,46 +86,71 @@ function ProfileSettings() { - - + ( + <> + - - + + + )} + /> - - + ( + <> + - + - - + + + )} + /> diff --git a/yarn.lock b/yarn.lock index 6c2d993a..4020f8ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2243,6 +2243,11 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@hookform/resolvers@^2.8.8": + version "2.8.8" + resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.8.8.tgz#17cf806485435877fdafce9f3bee6ff68f7f87b6" + integrity sha512-meAEDur1IJBfKyTo9yPYAuzjIfrxA7m9Ov+1nxaW/YupsqMeseWifoUjWK03+hz/RJizsVQAaUjVxFEkyu0GWg== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -4416,6 +4421,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== +"@types/lodash@^4.14.175": + version "4.14.181" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.181.tgz#d1d3740c379fda17ab175165ba04e2d03389385d" + integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag== + "@types/luxon@^2.0.8": version "2.0.9" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6" @@ -12150,7 +12160,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.11: +lodash-es@^4.17.11, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -13012,6 +13022,11 @@ nan@^2.14.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + nanoid@^3.1.30: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" @@ -19450,6 +19465,19 @@ yup@^0.31.0: property-expr "^2.0.4" toposort "^2.0.2" +yup@^0.32.11: + version "0.32.11" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" + integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/lodash" "^4.14.175" + lodash "^4.17.21" + lodash-es "^4.17.21" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2" + zen-observable-ts@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz#c2f5ccebe812faf0cfcde547e6004f65b1a6d769"