diff --git a/airbyte-webapp/src/packages/cloud/lib/domain/users/UserService.ts b/airbyte-webapp/src/packages/cloud/lib/domain/users/UserService.ts index 32fd911ccf64..92c550286bd2 100644 --- a/airbyte-webapp/src/packages/cloud/lib/domain/users/UserService.ts +++ b/airbyte-webapp/src/packages/cloud/lib/domain/users/UserService.ts @@ -24,6 +24,14 @@ export class UserService extends AirbyteRequestService { return this.fetch(`${this.url}/update`, params); } + public async changeName(authUserId: string, userId: string, name: string): Promise { + return this.fetch(`${this.url}/update`, { + authUserId, + userId, + name, + }); + } + public async changeEmail(email: string): Promise { return this.fetch(`${this.url}/update`, { email, diff --git a/airbyte-webapp/src/packages/cloud/lib/domain/users/types.ts b/airbyte-webapp/src/packages/cloud/lib/domain/users/types.ts index 8a72261cb01e..e3dbbc3ba6db 100644 --- a/airbyte-webapp/src/packages/cloud/lib/domain/users/types.ts +++ b/airbyte-webapp/src/packages/cloud/lib/domain/users/types.ts @@ -3,6 +3,7 @@ export type UserStatus = "invited" | "registered" | "disabled"; export interface User { email: string; name: string; + authUserId: string; userId: string; status?: UserStatus; intercomHash: string; diff --git a/airbyte-webapp/src/packages/cloud/locales/en.json b/airbyte-webapp/src/packages/cloud/locales/en.json index 2bba0236b801..b497135b4ac3 100644 --- a/airbyte-webapp/src/packages/cloud/locales/en.json +++ b/airbyte-webapp/src/packages/cloud/locales/en.json @@ -59,10 +59,12 @@ "settings.accountSettings.firstName": "First name", "settings.accountSettings.enterPassword": "Enter current password", "settings.accountSettings.firstName.placeholder": "Christopher", - "settings.accountSettings.fullName": "Full name", - "settings.accountSettings.fullName.placeholder": "Christopher Smith", + "settings.accountSettings.name": "Full name", + "settings.accountSettings.name.placeholder": "Christopher Smith", + "settings.accountSettings.name.empty.error": "Name cannot be empty", "settings.accountSettings.lastName": "Last name", "settings.accountSettings.lastName.placeholder": "Smith", + "settings.accountSettings.updateName": "Update Name", "settings.accountSettings.email": "Email", "settings.accountSettings.updateEmail": "Update Email", "settings.accountSettings.password": "Password", @@ -75,6 +77,8 @@ "settings.accountSettings.updatePasswordError": "Password update failed: ", "settings.accountSettings.updatePasswordSuccess": "Your password has been updated!", "settings.accountSettings.updatePassword": "Update password", + "settings.accountSettings.updateNameError": "Name update failed: ", + "settings.accountSettings.updateNameSuccess": "Your name has been updated!", "settings.userSettings": "User settings", "settings.workspaceSettings": "Workspace settings", "settings.generalSettings": "General Settings", diff --git a/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx b/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx index 695fba9d3874..d59e466c4448 100644 --- a/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx +++ b/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx @@ -32,6 +32,7 @@ export type AuthSignUp = (form: { }) => Promise; export type AuthChangeEmail = (email: string, password: string) => Promise; +export type AuthChangeName = (name: string) => Promise; export type AuthSendEmailVerification = () => Promise; export type AuthVerifyEmail = (code: string) => Promise; @@ -48,6 +49,7 @@ interface AuthContextApi { signUp: AuthSignUp; updatePassword: AuthUpdatePassword; updateEmail: AuthChangeEmail; + updateName: AuthChangeName; requirePasswordReset: AuthRequirePasswordReset; confirmPasswordReset: AuthConfirmPasswordReset; sendEmailVerification: AuthSendEmailVerification; @@ -58,7 +60,7 @@ interface AuthContextApi { export const AuthContext = React.createContext(null); export const AuthenticationProvider: React.FC = ({ children }) => { - const [state, { loggedIn, emailVerified, authInited, loggedOut }] = useTypesafeReducer< + const [state, { loggedIn, emailVerified, authInited, loggedOut, updateUserName }] = useTypesafeReducer< AuthServiceState, typeof actions >(authStateReducer, initialState, actions); @@ -113,6 +115,14 @@ export const AuthenticationProvider: React.FC = ({ children }) => { queryClient.removeQueries(); loggedOut(); }, + async updateName(name: string): Promise { + if (!state.currentUser) { + return; + } + await userService.changeName(state.currentUser.authUserId, state.currentUser.userId, name); + await authService.updateProfile(name); + updateUserName({ value: name }); + }, async updateEmail(email, password): Promise { await userService.changeEmail(email); return authService.updateEmail(email, password); diff --git a/airbyte-webapp/src/packages/cloud/services/auth/reducer.ts b/airbyte-webapp/src/packages/cloud/services/auth/reducer.ts index aa94e9990e89..9297934c9a8d 100644 --- a/airbyte-webapp/src/packages/cloud/services/auth/reducer.ts +++ b/airbyte-webapp/src/packages/cloud/services/auth/reducer.ts @@ -7,6 +7,7 @@ export const actions = { loggedIn: createAction("LOGGED_IN")<{ user: User; emailVerified: boolean }>(), emailVerified: createAction("EMAIL_VERIFIED")(), loggedOut: createAction("LOGGED_OUT")(), + updateUserName: createAction("UPDATE_USER_NAME")<{ value: string }>(), }; type Actions = ActionType; @@ -57,4 +58,16 @@ export const authStateReducer = createReducer(initial emailVerified: false, loggedOut: true, }; + }) + .handleAction(actions.updateUserName, (state, action): AuthServiceState => { + if (!state.currentUser) { + return state; + } + return { + ...state, + currentUser: { + ...state.currentUser, + name: action.payload.value, + }, + }; }); diff --git a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/AccountSettingsView.tsx b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/AccountSettingsView.tsx index 536cc88b420b..730cbc85b306 100644 --- a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/AccountSettingsView.tsx +++ b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/AccountSettingsView.tsx @@ -1,16 +1,14 @@ -import { Field, FieldProps, Form, Formik } from "formik"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage } from "react-intl"; import { useMutation } from "react-query"; import styled from "styled-components"; -import { LabeledInput, LoadingButton } from "components"; +import { LoadingButton } from "components"; -import { useAuthService, useCurrentUser } from "packages/cloud/services/auth/AuthService"; -import { RowFieldItem } from "packages/cloud/views/auth/components/FormComponents"; -import { Content, SettingsCard } from "pages/SettingsPage/pages/SettingsComponents"; +import { useAuthService } from "packages/cloud/services/auth/AuthService"; +import { SettingsCard } from "pages/SettingsPage/pages/SettingsComponents"; -import { EmailSection, PasswordSection } from "./components"; +import { EmailSection, PasswordSection, NameSection } from "./components"; const Header = styled.div` display: flex; @@ -18,47 +16,12 @@ const Header = styled.div` `; const AccountSettingsView: React.FC = () => { - const { formatMessage } = useIntl(); const authService = useAuthService(); const { mutateAsync: logout, isLoading: isLoggingOut } = useMutation(() => authService.logout()); - const user = useCurrentUser(); return ( <> - }> - - { - throw new Error("Not implemented"); - }} - > - {() => ( -
- - - {({ field, meta }: FieldProps) => ( - } - disabled - placeholder={formatMessage({ - id: "settings.accountSettings.fullName.placeholder", - })} - type="text" - error={!!meta.error && meta.touched} - message={meta.touched && meta.error && formatMessage({ id: meta.error })} - /> - )} - - -
- )} -
-
-
+ { + const { formatMessage } = useIntl(); + const user = useCurrentUser(); + const { changeName, successMessage, errorMessage } = useChangeName(); + + return ( + }> + + + changeName(values, formikHelpers).then(() => formikHelpers.resetForm({ values })) + } + > + {({ isSubmitting, isValid, dirty }) => ( +
+ + + {({ field, meta }: FieldProps) => ( + } + placeholder={formatMessage({ + id: "settings.accountSettings.name.placeholder", + })} + type="text" + error={!!meta.error && meta.touched} + message={meta.touched && meta.error && formatMessage({ id: meta.error })} + /> + )} + + + + + + + + )} +
+
+
+ ); +}; diff --git a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/hooks/index.ts b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/hooks/index.ts new file mode 100644 index 000000000000..724ee81107b5 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/hooks/index.ts @@ -0,0 +1 @@ +export { useChangeName } from "./useName"; diff --git a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/hooks/useName.ts b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/hooks/useName.ts new file mode 100644 index 000000000000..e724ddf56705 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/hooks/useName.ts @@ -0,0 +1,47 @@ +import { FormikHelpers } from "formik/dist/types"; +import { useState } from "react"; +import { useIntl } from "react-intl"; + +import { useAuthService } from "packages/cloud/services/auth/AuthService"; + +import { FormValues } from "../types"; + +type UseNameHook = () => { + successMessage: string; + errorMessage: string; + changeName: (values: FormValues, { setSubmitting, setFieldValue }: FormikHelpers) => Promise; +}; + +export const useChangeName: UseNameHook = () => { + const { updateName } = useAuthService(); + const { formatMessage } = useIntl(); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const changeName = async (values: FormValues, { setSubmitting }: FormikHelpers) => { + setSubmitting(true); + + setSuccessMessage(""); + setErrorMessage(""); + + try { + await updateName(values.name); + + setSuccessMessage( + formatMessage({ + id: "settings.accountSettings.updateNameSuccess", + }) + ); + } catch (err) { + setErrorMessage( + formatMessage({ + id: "settings.accountSettings.updateNameError", + }) + JSON.stringify(err) + ); + } + + setSubmitting(false); + }; + + return { successMessage, errorMessage, changeName }; +}; diff --git a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/index.ts b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/index.ts new file mode 100644 index 000000000000..cee415a533b7 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/index.ts @@ -0,0 +1 @@ +export { NameSection } from "./NameSection"; diff --git a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/types.ts b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/types.ts new file mode 100644 index 000000000000..540196b78428 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/NameSection/types.ts @@ -0,0 +1,3 @@ +export interface FormValues { + name: string; +} diff --git a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/index.ts b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/index.ts index d4d8fbca8b22..317f214fdfac 100644 --- a/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/index.ts +++ b/airbyte-webapp/src/packages/cloud/views/users/AccountSettingsView/components/index.ts @@ -1,2 +1,3 @@ export * from "./EmailSection"; export * from "./PasswordSection"; +export * from "./NameSection";