mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
[CORL-445] Change Password (#2426)
* fix: reversed `new-password` autocomplete option * feat: initial implementation * fix: localization and testing * fix: updated snapshot
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { createMutationContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
createMutation,
|
||||
createMutationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { AUTH_POPUP_ID } from "coral-stream/local";
|
||||
|
||||
@@ -36,3 +39,5 @@ export const withSetAuthPopupStateMutation = createMutationContainer(
|
||||
"setAuthPopupState",
|
||||
commit
|
||||
);
|
||||
|
||||
export default createMutation("setAuthPopupState", commit);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { createMutationContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
createMutation,
|
||||
createMutationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { AUTH_POPUP_ID } from "../local";
|
||||
|
||||
@@ -34,3 +37,5 @@ export const withShowAuthPopupMutation = createMutationContainer(
|
||||
"showAuthPopup",
|
||||
commit
|
||||
);
|
||||
|
||||
export default createMutation("showAuthPopup", commit);
|
||||
|
||||
@@ -21,7 +21,8 @@ export interface ProfileProps {
|
||||
viewer: PropTypesOf<typeof UserBoxContainer>["viewer"] &
|
||||
PropTypesOf<typeof CommentHistoryContainer>["viewer"] &
|
||||
PropTypesOf<typeof SettingsContainer>["viewer"];
|
||||
settings: PropTypesOf<typeof UserBoxContainer>["settings"];
|
||||
settings: PropTypesOf<typeof UserBoxContainer>["settings"] &
|
||||
PropTypesOf<typeof SettingsContainer>["settings"];
|
||||
}
|
||||
|
||||
const Profile: FunctionComponent<ProfileProps> = props => {
|
||||
@@ -58,7 +59,7 @@ const Profile: FunctionComponent<ProfileProps> = props => {
|
||||
<CommentHistoryContainer viewer={props.viewer} story={props.story} />
|
||||
</TabPane>
|
||||
<TabPane tabID="SETTINGS">
|
||||
<SettingsContainer viewer={props.viewer} />
|
||||
<SettingsContainer viewer={props.viewer} settings={props.settings} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</HorizontalGutter>
|
||||
|
||||
@@ -41,6 +41,7 @@ const enhanced = withFragmentContainer<ProfileContainerProps>({
|
||||
settings: graphql`
|
||||
fragment ProfileContainer_settings on Settings {
|
||||
...UserBoxContainer_settings
|
||||
...SettingsContainer_settings
|
||||
}
|
||||
`,
|
||||
})(ProfileContainer);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { FORM_ERROR, FormApi } from "final-form";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validatePassword,
|
||||
} from "coral-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
FieldSet,
|
||||
Flex,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputLabel,
|
||||
PasswordField,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import UpdatePasswordMutation from "./UpdatePasswordMutation";
|
||||
|
||||
interface Props {
|
||||
onResetPassword: () => void;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
const ChangePassword: FunctionComponent<Props> = ({ onResetPassword }) => {
|
||||
const updatePassword = useMutation(UpdatePasswordMutation);
|
||||
const onSubmit = useCallback(
|
||||
async (input: FormProps, form: FormApi) => {
|
||||
try {
|
||||
await updatePassword(input);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
}
|
||||
|
||||
return {
|
||||
[FORM_ERROR]: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Reset the form now that we're done.
|
||||
form.reset();
|
||||
|
||||
return;
|
||||
},
|
||||
[updatePassword]
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testid="profile-settings-changePassword">
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({
|
||||
handleSubmit,
|
||||
submitting,
|
||||
submitError,
|
||||
pristine,
|
||||
submitSucceeded,
|
||||
}) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
<Localized id="profile-settings-changePassword">
|
||||
<Typography variant="heading3">Change Password</Typography>
|
||||
</Localized>
|
||||
<HorizontalGutter container={<FieldSet />}>
|
||||
<Field
|
||||
name="oldPassword"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField container={<FieldSet />}>
|
||||
<Localized id="profile-settings-changePassword-oldPassword">
|
||||
<InputLabel htmlFor={input.name}>
|
||||
Old Password
|
||||
</InputLabel>
|
||||
</Localized>
|
||||
<PasswordField
|
||||
fullWidth
|
||||
id={input.name}
|
||||
disabled={submitting}
|
||||
color={colorFromMeta(meta)}
|
||||
autoComplete="current-password"
|
||||
{...input}
|
||||
/>
|
||||
<ValidationMessage fullWidth meta={meta} />
|
||||
|
||||
<Flex justifyContent="flex-end">
|
||||
<Localized id="profile-settings-changePassword-forgotPassword">
|
||||
<Typography variant="bodyCopy">
|
||||
<Button
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
onClick={onResetPassword}
|
||||
>
|
||||
Forgot your password?
|
||||
</Button>
|
||||
</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name="newPassword"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField container={<FieldSet />}>
|
||||
<Localized id="profile-settings-changePassword-newPassword">
|
||||
<InputLabel htmlFor={input.name}>
|
||||
New Password
|
||||
</InputLabel>
|
||||
</Localized>
|
||||
<PasswordField
|
||||
fullWidth
|
||||
id={input.name}
|
||||
disabled={submitting}
|
||||
color={colorFromMeta(meta)}
|
||||
autoComplete="new-password"
|
||||
{...input}
|
||||
/>
|
||||
<ValidationMessage fullWidth meta={meta} />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
{submitSucceeded && (
|
||||
<Localized id="profile-settings-changePassword-updated">
|
||||
<CallOut color="success" fullWidth>
|
||||
Your password has been updated
|
||||
</CallOut>
|
||||
</Localized>
|
||||
)}
|
||||
<Flex justifyContent="flex-end">
|
||||
<Localized id="profile-settings-changePassword-button">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
type="submit"
|
||||
disabled={submitting || pristine}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePassword;
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import {
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { ChangePasswordContainer_settings } from "coral-stream/__generated__/ChangePasswordContainer_settings.graphql";
|
||||
import { ChangePasswordContainerLocal } from "coral-stream/__generated__/ChangePasswordContainerLocal.graphql";
|
||||
import SetAuthPopupStateMutation from "coral-stream/common/UserBox/SetAuthPopupStateMutation";
|
||||
import ShowAuthPopupMutation from "coral-stream/mutations/ShowAuthPopupMutation";
|
||||
import { Popup } from "coral-ui/components";
|
||||
|
||||
import ChangePassword from "./ChangePassword";
|
||||
|
||||
interface Props {
|
||||
local: ChangePasswordContainerLocal;
|
||||
settings: ChangePasswordContainer_settings;
|
||||
}
|
||||
|
||||
const ChangePasswordContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
local: {
|
||||
authPopup: { open, focus, view },
|
||||
},
|
||||
}) => {
|
||||
const setAuthPopupState = useMutation(SetAuthPopupStateMutation);
|
||||
const showAuthPopup = useMutation(ShowAuthPopupMutation);
|
||||
const onResetPassword = useCallback(() => {
|
||||
showAuthPopup({ view: "FORGOT_PASSWORD" });
|
||||
}, [showAuthPopup]);
|
||||
const onFocus = useCallback(() => {
|
||||
setAuthPopupState({ focus: true });
|
||||
}, [setAuthPopupState]);
|
||||
const onBlur = useCallback(() => {
|
||||
setAuthPopupState({ focus: true });
|
||||
}, [setAuthPopupState]);
|
||||
const onClose = useCallback(() => {
|
||||
setAuthPopupState({ open: false });
|
||||
}, [setAuthPopupState]);
|
||||
|
||||
if (
|
||||
!settings.auth.integrations.local.enabled ||
|
||||
!settings.auth.integrations.local.targetFilter.stream
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup
|
||||
href={`${urls.embed.auth}?view=${view}`}
|
||||
title="Coral Auth"
|
||||
features="menubar=0,resizable=0,width=350,height=450,top=100,left=500"
|
||||
open={open}
|
||||
focus={focus}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ChangePassword onResetPassword={onResetPassword} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment ChangePasswordContainerLocal on Local {
|
||||
authPopup {
|
||||
open
|
||||
focus
|
||||
view
|
||||
}
|
||||
}
|
||||
`
|
||||
)(
|
||||
withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment ChangePasswordContainer_settings on Settings {
|
||||
auth {
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ChangePasswordContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
@@ -1,7 +1,3 @@
|
||||
.root {
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--palette-text-primary);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
@@ -23,10 +23,7 @@ interface Props {
|
||||
const IgnoreUserSettingsContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
const removeUserIgnore = useMutation(RemoveUserIgnoreMutation);
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
data-testid="profile-settings-ignoredCommenters"
|
||||
>
|
||||
<div data-testid="profile-settings-ignoredCommenters">
|
||||
<Localized id="profile-settings-ignoredCommenters">
|
||||
<Typography variant="heading3">Ignored Commenters</Typography>
|
||||
</Localized>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
max-width: 440px;
|
||||
}
|
||||
@@ -2,17 +2,26 @@ import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { SettingsContainer_viewer as ViewerData } from "coral-stream/__generated__/SettingsContainer_viewer.graphql";
|
||||
import { SettingsContainer_settings } from "coral-stream/__generated__/SettingsContainer_settings.graphql";
|
||||
import { SettingsContainer_viewer } from "coral-stream/__generated__/SettingsContainer_viewer.graphql";
|
||||
import { HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import ChangePasswordContainer from "./ChangePasswordContainer";
|
||||
import IgnoreUserSettingsContainer from "./IgnoreUserSettingsContainer";
|
||||
|
||||
import styles from "./SettingsContainer.css";
|
||||
|
||||
interface Props {
|
||||
viewer: ViewerData;
|
||||
viewer: SettingsContainer_viewer;
|
||||
settings: SettingsContainer_settings;
|
||||
}
|
||||
|
||||
const SettingsContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
return <IgnoreUserSettingsContainer viewer={viewer} />;
|
||||
};
|
||||
const SettingsContainer: FunctionComponent<Props> = ({ viewer, settings }) => (
|
||||
<HorizontalGutter spacing={5} className={styles.root}>
|
||||
<IgnoreUserSettingsContainer viewer={viewer} />
|
||||
<ChangePasswordContainer settings={settings} />
|
||||
</HorizontalGutter>
|
||||
);
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
@@ -20,6 +29,11 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...IgnoreUserSettingsContainer_viewer
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment SettingsContainer_settings on Settings {
|
||||
...ChangePasswordContainer_settings
|
||||
}
|
||||
`,
|
||||
})(SettingsContainer);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { UpdatePasswordMutation as MutationTypes } from "coral-stream/__generated__/UpdatePasswordMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const UpdatePasswordMutation = createMutation(
|
||||
"updatePassword",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdatePasswordMutation($input: UpdatePasswordInput!) {
|
||||
updatePassword(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default UpdatePasswordMutation;
|
||||
@@ -93,6 +93,19 @@ export const settings = createFixture<GQLSettings>({
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsWithoutLocalAuth = createFixture<GQLSettings>(
|
||||
{
|
||||
auth: {
|
||||
integrations: {
|
||||
local: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings
|
||||
);
|
||||
|
||||
export const baseUser = createFixture<GQLUser>({
|
||||
createdAt: "2018-02-06T18:24:00.000Z",
|
||||
status: {
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders the empty settings pane 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root App-root HorizontalGutter-full"
|
||||
>
|
||||
<ul
|
||||
className="TabBar-root TabBar-primary coral coral-tabBar"
|
||||
role="tablist"
|
||||
>
|
||||
<li
|
||||
className="Tab-root"
|
||||
id="tab-COMMENTS"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabPane-COMMENTS"
|
||||
aria-selected={false}
|
||||
className="BaseButton-root Tab-button Tab-primary coral coral-tabBar-allComments"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Comments
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="Tab-root"
|
||||
id="tab-PROFILE"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabPane-PROFILE"
|
||||
aria-selected={true}
|
||||
className="BaseButton-root Tab-button Tab-primary Tab-active coral coral-tabBar-myProfile"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
My Profile
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<section
|
||||
aria-labelledby="tab-PROFILE"
|
||||
className="App-tabContent"
|
||||
data-testid="current-tab-pane"
|
||||
id="tabPane-PROFILE"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root"
|
||||
>
|
||||
<div
|
||||
className="Flex-flex Flex-halfItemGutter Flex-wrap gutter"
|
||||
>
|
||||
<div
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Signed in as
|
||||
<span
|
||||
className="Box-root Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
|
||||
>
|
||||
Passivo
|
||||
</span>
|
||||
.
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
|
||||
>
|
||||
<span>
|
||||
Not you?
|
||||
</span>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
className="TabBar-root TabBar-secondary"
|
||||
role="tablist"
|
||||
>
|
||||
<li
|
||||
className="Tab-root"
|
||||
id="tab-MY_COMMENTS"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabPane-MY_COMMENTS"
|
||||
aria-selected={false}
|
||||
className="BaseButton-root Tab-button Tab-secondary"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
My Comments
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="Tab-root"
|
||||
id="tab-SETTINGS"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabPane-SETTINGS"
|
||||
aria-selected={true}
|
||||
className="BaseButton-root Tab-button Tab-secondary Tab-active"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Settings
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<section
|
||||
aria-labelledby="tab-SETTINGS"
|
||||
id="tabPane-SETTINGS"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root SettingsContainer-root HorizontalGutter-spacing-5"
|
||||
>
|
||||
<div
|
||||
data-testid="profile-settings-ignoredCommenters"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary"
|
||||
>
|
||||
Ignored Commenters
|
||||
</h1>
|
||||
<p
|
||||
className="IgnoreUserSettingsContainer-description"
|
||||
>
|
||||
Once you ignore someone, all of their comments are hidden from you.
|
||||
Commenters you ignore will still be able to see your comments.
|
||||
</p>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
<div
|
||||
className="IgnoreUserSettingsContainer-empty"
|
||||
>
|
||||
You are not currently ignoring anyone
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="profile-settings-changePassword"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary"
|
||||
>
|
||||
Change Password
|
||||
</h1>
|
||||
<fieldset
|
||||
className="FieldSet-root Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<fieldset
|
||||
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="oldPassword"
|
||||
>
|
||||
Old Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="current-password"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="oldPassword"
|
||||
name="oldPassword"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder=""
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
|
||||
>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Forgot your password?
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="newPassword"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder=""
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-disabled"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,92 +0,0 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
waitForElement,
|
||||
waitUntilThrow,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { commenters, settings, stories, viewerPassive } from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
const viewer = viewerPassive;
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () => viewer,
|
||||
story: () => story,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("SETTINGS", "profileTab");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const section = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("profile-settings-ignoredCommenters")
|
||||
);
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
section,
|
||||
};
|
||||
}
|
||||
|
||||
it("render empty ignored users list", async () => {
|
||||
const { section } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(section).getByText("You are not currently ignoring anyone", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("render ignored users list", async () => {
|
||||
const { section } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
viewer: () =>
|
||||
pureMerge<typeof viewer>(viewer, {
|
||||
ignoredUsers: [commenters[0], commenters[1]],
|
||||
}),
|
||||
},
|
||||
Mutation: {
|
||||
removeUserIgnore: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: commenters[0].id,
|
||||
});
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
within(section).getByText(commenters[0].username!);
|
||||
within(section).getByText(commenters[1].username!);
|
||||
|
||||
// Stop ignoring first users.
|
||||
within(section)
|
||||
.getAllByText("Stop ignoring", { selector: "button" })[0]
|
||||
.props.onClick();
|
||||
|
||||
// First user should dissappear from list.
|
||||
await waitUntilThrow(() =>
|
||||
within(section).getByText(commenters[0].username!)
|
||||
);
|
||||
within(section).getByText(commenters[1].username!);
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
waitForElement,
|
||||
waitUntilThrow,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import {
|
||||
commenters,
|
||||
settings,
|
||||
settingsWithoutLocalAuth,
|
||||
stories,
|
||||
viewerPassive,
|
||||
} from "../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
const viewer = viewerPassive;
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () => viewer,
|
||||
story: () => story,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue("SETTINGS", "profileTab");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const ignoredCommenters = await waitForElement(() =>
|
||||
within(testRenderer.root).queryByTestID(
|
||||
"profile-settings-ignoredCommenters"
|
||||
)
|
||||
);
|
||||
|
||||
const changePassword = within(testRenderer.root).queryByTestID(
|
||||
"profile-settings-changePassword"
|
||||
);
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
ignoredCommenters,
|
||||
changePassword,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders the empty settings pane", async () => {
|
||||
const {
|
||||
testRenderer: { root },
|
||||
} = await createTestRenderer();
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("doesn't show the change password pane when local auth is disabled", async () => {
|
||||
const { changePassword } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settingsWithoutLocalAuth,
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(changePassword).toBeNull();
|
||||
});
|
||||
|
||||
it("render password change form", async () => {
|
||||
const updatePassword = sinon.stub().callsFake((_: any, { input }) => {
|
||||
expectAndFail(input).toMatchObject({
|
||||
oldPassword: "testtest",
|
||||
newPassword: "testtest",
|
||||
});
|
||||
return {
|
||||
clientMutationId: input.clientMutationId,
|
||||
};
|
||||
});
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
updatePassword,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const changePassword = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("profile-settings-changePassword")
|
||||
);
|
||||
const form = within(changePassword).getByType("form");
|
||||
const oldPassword = await waitForElement(() =>
|
||||
within(form).getByID("oldPassword", { exact: false })
|
||||
);
|
||||
const newPassword = await waitForElement(() =>
|
||||
within(form).getByID("newPassword", { exact: false })
|
||||
);
|
||||
|
||||
// Submit an empty form.
|
||||
act(() => {
|
||||
form.props.onSubmit();
|
||||
});
|
||||
within(changePassword).getAllByText("field is required", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// Password too short.
|
||||
act(() => {
|
||||
oldPassword.props.onChange("test");
|
||||
newPassword.props.onChange("test");
|
||||
});
|
||||
within(changePassword).getAllByText(
|
||||
"Password must contain at least 8 characters",
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
oldPassword.props.onChange("testtest");
|
||||
newPassword.props.onChange("testtest");
|
||||
await form.props.onSubmit();
|
||||
});
|
||||
|
||||
expect(updatePassword.calledOnce).toBeTruthy();
|
||||
});
|
||||
|
||||
it("render empty ignored users list", async () => {
|
||||
const { ignoredCommenters } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(ignoredCommenters).getByText(
|
||||
"You are not currently ignoring anyone",
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("render ignored users list", async () => {
|
||||
const { ignoredCommenters } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
viewer: () =>
|
||||
pureMerge<typeof viewer>(viewer, {
|
||||
ignoredUsers: [commenters[0], commenters[1]],
|
||||
}),
|
||||
},
|
||||
Mutation: {
|
||||
removeUserIgnore: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: commenters[0].id,
|
||||
});
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
within(ignoredCommenters).getByText(commenters[0].username!);
|
||||
within(ignoredCommenters).getByText(commenters[1].username!);
|
||||
|
||||
// Stop ignoring first users.
|
||||
within(ignoredCommenters)
|
||||
.getAllByText("Stop ignoring", { selector: "button" })[0]
|
||||
.props.onClick();
|
||||
|
||||
// First user should dissappear from list.
|
||||
await waitUntilThrow(() =>
|
||||
within(ignoredCommenters).getByText(commenters[0].username!)
|
||||
);
|
||||
within(ignoredCommenters).getByText(commenters[1].username!);
|
||||
});
|
||||
Reference in New Issue
Block a user