[CORL-761] stream-side account tab for sso (#2834)

* move download comments to my comments tab

* only show download comments if available

* move ignored users management to same tab as notifications, rename to preferences

* fix query and ts defs

* add url to jwt

* make account tab go to external url if provided

* ensure url is an optional jwt field

* update tabs for stream profile

* update classnames for tabs

* fix tests
This commit is contained in:
Tessa Thornton
2020-02-19 14:39:58 -05:00
committed by GitHub
parent e42c2b925d
commit 4b637a2dd5
35 changed files with 459 additions and 576 deletions
+10
View File
@@ -106,6 +106,12 @@ const CLASSES = {
myComments:
"coral coral-tabBarSecondary-tab coral-tabBarMyProfile-myComments",
/**
* preferences is the button for the "Preferences" tab.
*/
preferences:
"coral coral-tabBarSecondary-tab coral-tabBarMyProfile-preferences",
/**
* notifications is the button for the "Notifications" tab.
*/
@@ -744,6 +750,10 @@ const CLASSES = {
$root: "coral coral-notifications",
},
preferencesTabPane: {
$root: "coral coral-preferences",
},
/**
* accountTabPane is the tab pane that shows account settings.
*/
+1 -1
View File
@@ -12,7 +12,7 @@ enum TAB {
enum PROFILE_TAB {
MY_COMMENTS
ACCOUNT
NOTIFICATIONS
PREFERENCES
}
enum COMMENTS_TAB {
@@ -1,4 +0,0 @@
export {
default,
default as CommentHistoryContainer,
} from "./CommentHistoryContainer";
@@ -13,7 +13,7 @@ import { Button, CallOut, Flex, Icon, Typography } from "coral-ui/components";
import { DownloadCommentsContainer_viewer } from "coral-stream/__generated__/DownloadCommentsContainer_viewer.graphql";
import RequestCommentsDownloadMutation from "./RequestCommentsDownloadMutation";
import RequestCommentsDownloadMutation from "../Settings/RequestCommentsDownloadMutation";
import styles from "./DownloadCommentsContainer.css";
@@ -0,0 +1,64 @@
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { MyCommentsContainer_settings } from "coral-stream/__generated__/MyCommentsContainer_settings.graphql";
import { MyCommentsContainer_story } from "coral-stream/__generated__/MyCommentsContainer_story.graphql";
import { MyCommentsContainer_viewer } from "coral-stream/__generated__/MyCommentsContainer_viewer.graphql";
import CommentHistoryContainer from "./CommentHistoryContainer";
import DownloadCommentsContainer from "./DownloadCommentsContainer";
import { HorizontalGutter } from "coral-ui/components/v2";
// import styles from "./MyComments.css";
interface Props {
settings: MyCommentsContainer_settings;
viewer: MyCommentsContainer_viewer;
story: MyCommentsContainer_story;
}
const MyCommentsContainer: FunctionComponent<Props> = ({
settings,
viewer,
story,
}) => {
return (
<HorizontalGutter spacing={6}>
<CommentHistoryContainer
settings={settings}
viewer={viewer}
story={story}
/>
{settings.accountFeatures.downloadComments && (
<DownloadCommentsContainer viewer={viewer} />
)}
</HorizontalGutter>
);
};
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment MyCommentsContainer_viewer on User {
...CommentHistoryContainer_viewer
...DownloadCommentsContainer_viewer
}
`,
story: graphql`
fragment MyCommentsContainer_story on Story {
...CommentHistoryContainer_story
}
`,
settings: graphql`
fragment MyCommentsContainer_settings on Settings {
accountFeatures {
downloadComments
}
...CommentHistoryContainer_settings
}
`,
})(MyCommentsContainer);
export default enhanced;
@@ -0,0 +1 @@
export { default, default as MyCommentsContainer } from "./MyCommentsContainer";
@@ -16,8 +16,8 @@ import {
import { IgnoreUserSettingsContainer_viewer as ViewerData } from "coral-stream/__generated__/IgnoreUserSettingsContainer_viewer.graphql";
import Username from "../Settings/Username";
import RemoveUserIgnoreMutation from "./RemoveUserIgnoreMutation";
import Username from "./Username";
import styles from "./IgnoreUserSettingsContainer.css";
@@ -0,0 +1,33 @@
import React, { FunctionComponent } from "react";
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
import { HorizontalGutter } from "coral-ui/components/v2";
import IgnoreUserSettingsContainer from "./IgnoreUserSettingsContainer";
import NotificationSettingsContainer from "./NotificationSettingsContainer";
import { PreferencesContainer_viewer } from "coral-stream/__generated__/PreferencesContainer_viewer.graphql";
interface Props {
viewer: PreferencesContainer_viewer;
}
const PreferencesContainer: FunctionComponent<Props> = props => {
return (
<HorizontalGutter spacing={4}>
<NotificationSettingsContainer viewer={props.viewer} />
<IgnoreUserSettingsContainer viewer={props.viewer} />
</HorizontalGutter>
);
};
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment PreferencesContainer_viewer on User {
...NotificationSettingsContainer_viewer
...IgnoreUserSettingsContainer_viewer
}
`,
})(PreferencesContainer);
export default enhanced;
@@ -0,0 +1,4 @@
export {
default,
default as PreferencesContainer,
} from "./PreferencesContainer";
+53 -31
View File
@@ -1,5 +1,5 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent, useCallback } from "react";
import React, { FunctionComponent, useCallback, useMemo } from "react";
import { useViewerEvent } from "coral-framework/lib/events";
import { graphql, useLocal } from "coral-framework/lib/relay";
@@ -17,23 +17,23 @@ import {
import { ProfileLocal } from "coral-stream/__generated__/ProfileLocal.graphql";
import CommentHistoryContainer from "./CommentHistory";
import DeletionRequestCalloutContainer from "./DeletionRequest/DeletionRequestCalloutContainer";
import MyCommentsContainer from "./MyComments";
import PreferencesContainer from "./Preferences";
import AccountSettingsContainer from "./Settings";
import NotificationSettingsContainer from "./Settings/NotificationSettingsContainer";
export interface ProfileProps {
story: PropTypesOf<typeof CommentHistoryContainer>["story"];
isSSO: boolean;
ssoURL: string | null;
story: PropTypesOf<typeof MyCommentsContainer>["story"];
viewer: PropTypesOf<typeof UserBoxContainer>["viewer"] &
PropTypesOf<typeof CommentHistoryContainer>["viewer"] &
PropTypesOf<typeof AccountSettingsContainer>["viewer"] &
PropTypesOf<typeof MyCommentsContainer>["viewer"] &
PropTypesOf<typeof AccountSettingsContainer>["viewer"] &
PropTypesOf<typeof DeletionRequestCalloutContainer>["viewer"] &
PropTypesOf<typeof AccountSettingsContainer>["viewer"] &
PropTypesOf<typeof NotificationSettingsContainer>["viewer"];
PropTypesOf<typeof PreferencesContainer>["viewer"];
settings: PropTypesOf<typeof UserBoxContainer>["settings"] &
PropTypesOf<typeof AccountSettingsContainer>["settings"] &
PropTypesOf<typeof CommentHistoryContainer>["settings"];
PropTypesOf<typeof MyCommentsContainer>["settings"];
}
const Profile: FunctionComponent<ProfileProps> = props => {
@@ -45,13 +45,31 @@ const Profile: FunctionComponent<ProfileProps> = props => {
`);
const onTabClick = useCallback(
(tab: ProfileLocal["profileTab"]) => {
if (local.profileTab !== tab) {
if (
tab === "ACCOUNT" &&
props.isSSO &&
props.ssoURL &&
props.ssoURL.length > 0
) {
window.open(props.ssoURL);
} else if (local.profileTab !== tab) {
emitSetProfileTabEvent({ tab });
setLocal({ profileTab: tab });
}
},
[setLocal, local.profileTab]
[setLocal, local.profileTab, props.ssoURL, props.isSSO]
);
const showAccountTab = useMemo(() => {
if (!props.isSSO) {
return true;
}
if (props.ssoURL) {
return true;
}
return false;
}, [props.ssoURL, props.isSSO]);
return (
<HorizontalGutter size="double">
<UserBoxContainer viewer={props.viewer} settings={props.settings} />
@@ -67,43 +85,47 @@ const Profile: FunctionComponent<ProfileProps> = props => {
</Localized>
</Tab>
<Tab
tabID="NOTIFICATIONS"
className={CLASSES.tabBarMyProfile.notifications}
tabID="PREFERENCES"
className={CLASSES.tabBarMyProfile.preferences}
>
<Localized id="profile-notificationsTab">
<span>Notifications</span>
</Localized>
</Tab>
<Tab tabID="ACCOUNT" className={CLASSES.tabBarMyProfile.settings}>
<Localized id="profile-accountTab">
<span>Account</span>
<Localized id="profile-preferencesTab">
<span>Preferences</span>
</Localized>
</Tab>
{showAccountTab && (
<Tab tabID="ACCOUNT" className={CLASSES.tabBarMyProfile.settings}>
<Localized id="profile-accountTab">
<span>Account</span>
</Localized>
</Tab>
)}
</TabBar>
<TabContent activeTab={local.profileTab}>
<TabPane
className={CLASSES.myCommentsTabPane.$root}
tabID="MY_COMMENTS"
>
<CommentHistoryContainer
<MyCommentsContainer
settings={props.settings}
viewer={props.viewer}
story={props.story}
/>
</TabPane>
<TabPane
className={CLASSES.notificationsTabPane.$root}
tabID="NOTIFICATIONS"
className={CLASSES.preferencesTabPane.$root}
tabID="PREFERENCES"
>
<NotificationSettingsContainer viewer={props.viewer} />
</TabPane>
<TabPane className={CLASSES.accountTabPane.$root} tabID="ACCOUNT">
<AccountSettingsContainer
viewer={props.viewer}
settings={props.settings}
/>
<DeletionRequestCalloutContainer viewer={props.viewer} />
<PreferencesContainer viewer={props.viewer} />
</TabPane>
{showAccountTab && (
<TabPane className={CLASSES.accountTabPane.$root} tabID="ACCOUNT">
<AccountSettingsContainer
viewer={props.viewer}
settings={props.settings}
/>
<DeletionRequestCalloutContainer viewer={props.viewer} />
</TabPane>
)}
</TabContent>
</HorizontalGutter>
);
@@ -1,3 +1,4 @@
import { isUndefined } from "lodash";
import React from "react";
import { graphql } from "react-relay";
@@ -17,11 +18,16 @@ interface ProfileContainerProps {
export class ProfileContainer extends React.Component<ProfileContainerProps> {
public render() {
const ssoProfile = this.props.viewer.profiles.find(
profile => profile.__typename === "SSOProfile"
);
return (
<Profile
viewer={this.props.viewer}
story={this.props.story}
settings={this.props.settings}
isSSO={!isUndefined(ssoProfile)}
ssoURL={this.props.viewer.ssoURL}
/>
);
}
@@ -29,25 +35,27 @@ export class ProfileContainer extends React.Component<ProfileContainerProps> {
const enhanced = withFragmentContainer<ProfileContainerProps>({
story: graphql`
fragment ProfileContainer_story on Story {
...CommentHistoryContainer_story
...MyCommentsContainer_story
}
`,
viewer: graphql`
fragment ProfileContainer_viewer on User {
...UserBoxContainer_viewer
...CommentHistoryContainer_viewer
...AccountSettingsContainer_viewer
...ChangeUsernameContainer_viewer
...ChangeEmailContainer_viewer
...MyCommentsContainer_viewer
...DeletionRequestCalloutContainer_viewer
...NotificationSettingsContainer_viewer
...PreferencesContainer_viewer
profiles {
__typename
}
ssoURL
}
`,
settings: graphql`
fragment ProfileContainer_settings on Settings {
...UserBoxContainer_settings
...AccountSettingsContainer_settings
...CommentHistoryContainer_settings
...MyCommentsContainer_settings
}
`,
})(ProfileContainer);
@@ -12,8 +12,7 @@ import ChangeEmailContainer from "./ChangeEmail";
import ChangePasswordContainer from "./ChangePasswordContainer";
import ChangeUsernameContainer from "./ChangeUsername";
import DeleteAccountContainer from "./DeleteAccount/DeleteAccountContainer";
import DownloadCommentsContainer from "./DownloadCommentsContainer";
import IgnoreUserSettingsContainer from "./IgnoreUserSettingsContainer";
// import DownloadCommentsContainer from "../CommentHistory/DownloadCommentsContainer";
import styles from "./AccountSettingsContainer.css";
@@ -26,7 +25,7 @@ const AccountSettingsContainer: FunctionComponent<Props> = ({
viewer,
settings,
}) => (
<HorizontalGutter size="oneAndAHalf">
<HorizontalGutter size="oneAndAHalf" data-testid="profile-manageAccount">
<Localized id="accountSettings-manage-account">
<Typography variant="heading1">Manage your account</Typography>
</Localized>
@@ -34,10 +33,6 @@ const AccountSettingsContainer: FunctionComponent<Props> = ({
<ChangeUsernameContainer settings={settings} viewer={viewer} />
<ChangeEmailContainer settings={settings} viewer={viewer} />
<ChangePasswordContainer settings={settings} />
<IgnoreUserSettingsContainer viewer={viewer} />
{settings.accountFeatures.downloadComments && (
<DownloadCommentsContainer viewer={viewer} />
)}
{settings.accountFeatures.deleteAccount && (
<DeleteAccountContainer viewer={viewer} settings={settings} />
)}
@@ -48,25 +43,20 @@ const AccountSettingsContainer: FunctionComponent<Props> = ({
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment AccountSettingsContainer_viewer on User {
...IgnoreUserSettingsContainer_viewer
...DownloadCommentsContainer_viewer
...DeleteAccountContainer_viewer
...ChangeUsernameContainer_viewer
...ChangeEmailContainer_viewer
...UserBoxContainer_viewer
}
`,
settings: graphql`
fragment AccountSettingsContainer_settings on Settings {
accountFeatures {
downloadComments
deleteAccount
}
...ChangePasswordContainer_settings
...DeleteAccountContainer_settings
...ChangeEmailContainer_settings
...ChangeUsernameContainer_settings
...UserBoxContainer_settings
}
`,
})(AccountSettingsContainer);
@@ -2,102 +2,128 @@
exports[`renders the empty settings pane 1`] = `
<div
className="Box-root HorizontalGutter-root coral coral-stream App-root HorizontalGutter-full"
className="Box-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
data-testid="profile-manageAccount"
>
<nav>
<h2
className="AriaInfo-root"
>
Navigation
</h2>
<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-tab coral-tabBar-comments"
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-tab 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>
</nav>
<main>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent coral coral-myProfile"
data-testid="current-tab-pane"
id="tabPane-PROFILE"
role="tabpanel"
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Manage your account
</h1>
<div
className="Box-root HorizontalGutter-root AccountSettingsContainer-root HorizontalGutter-full"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
data-testid="profile-changeUsername"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignBaseline"
>
<div>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark coral coral-myUsername"
>
Username
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Passivo
</p>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-myUsername-editButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
<div
className="Box-root HorizontalGutter-root coral coral-myEmail HorizontalGutter-spacing-5"
data-testid="profile-changeEmail"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
>
<div>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark"
>
Email
</h1>
<div
className="Box-root Flex-root Flex-flex"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
/>
<span>
 
</span>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextSecondary coral coral-myEmail-unverified"
>
(Unverified)
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-myEmail-editButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
<div
className="CallOut-root CallOut-colorRegular coral coral-verifyEmail"
>
<div
className="Box-root Flex-root coral coral-viewerBox"
className="CallOut-inner"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap gutter"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary UserBoxAuthenticated-userBoxText"
>
Signed in as
<span
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary"
<div>
<i
aria-hidden="true"
className="Icon-root Icon-lg"
>
Passivo
</span>
.
email
</i>
</div>
<div
className="Box-root Flex-root Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary UserBoxAuthenticated-userBoxText Flex-flex"
>
<span>
Not you? 
</span>
<div>
<h1
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary Typography-gutterBottom"
>
Verify your email address
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
An email has been sent to {$email} to verify your account.
You must verify your new email address before it can be used
to sign in to your account or to receive notifications.
</p>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantRegular UserBoxAuthenticated-userBoxButton coral coral-viewerBox-logoutButton"
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantRegular ChangeEmailContainer-resendButton coral coral-verifyEmail-resendButton"
data-color="primary"
data-variant="regular"
onBlur={[Function]}
@@ -108,371 +134,79 @@ exports[`renders the empty settings pane 1`] = `
onTouchEnd={[Function]}
type="button"
>
Sign Out
Resend verification
</button>
</div>
</div>
</div>
<ul
className="TabBar-root TabBar-secondary coral coral-tabBarSecondary coral-tabBarMyProfile"
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 coral coral-tabBarSecondary-tab coral-tabBarMyProfile-myComments"
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-NOTIFICATIONS"
role="presentation"
>
<button
aria-controls="tabPane-NOTIFICATIONS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-secondary coral coral-tabBarSecondary-tab coral-tabBarMyProfile-notifications"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
<span>
Notifications
</span>
</button>
</li>
<li
className="Tab-root"
id="tab-ACCOUNT"
role="presentation"
>
<button
aria-controls="tabPane-ACCOUNT"
aria-selected={true}
className="BaseButton-root Tab-button Tab-secondary Tab-active coral coral-tabBarSecondary-tab coral-tabBarMyProfile-settings"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
<span>
Account
</span>
</button>
</li>
</ul>
<section
aria-labelledby="tab-ACCOUNT"
className="coral coral-account"
id="tabPane-ACCOUNT"
role="tabpanel"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Manage your account
</h1>
<div
className="Box-root HorizontalGutter-root AccountSettingsContainer-root HorizontalGutter-full"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
data-testid="profile-changeUsername"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignBaseline"
>
<div>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark coral coral-myUsername"
>
Username
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Passivo
</p>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-myUsername-editButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
<div
className="Box-root HorizontalGutter-root coral coral-myEmail HorizontalGutter-spacing-5"
data-testid="profile-changeEmail"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
>
<div>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark"
>
Email
</h1>
<div
className="Box-root Flex-root Flex-flex"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
/>
<span>
 
</span>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextSecondary coral coral-myEmail-unverified"
>
(Unverified)
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-myEmail-editButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
<div
className="CallOut-root CallOut-colorRegular coral coral-verifyEmail"
>
<div
className="CallOut-inner"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div>
<i
aria-hidden="true"
className="Icon-root Icon-lg"
>
email
</i>
</div>
<div>
<h1
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary Typography-gutterBottom"
>
Verify your email address
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
An email has been sent to {$email} to verify your account.
You must verify your new email address before it can be used
to sign in to your account or to receive notifications.
</p>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantRegular ChangeEmailContainer-resendButton coral coral-verifyEmail-resendButton"
data-color="primary"
data-variant="regular"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Resend verification
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="coral coral-myPassword"
data-testid="profile-account-changePassword"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark"
>
Password
</h1>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-myPassword-editButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
<div
className="coral coral-ignoredCommenters"
data-testid="profile-account-ignoredCommenters"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark"
>
Ignored Commenters
</h1>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-ignoredComments-manageButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Manage
</button>
</div>
</div>
<div
className="DownloadCommentsContainer-root coral coral-downloadCommentHistory"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignFlexStart"
>
<div
className="DownloadCommentsContainer-content"
>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark DownloadCommentsContainer-title"
>
Download my comment history
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary DownloadCommentsContainer-description"
>
You will receive an email with a link to download your comment history.
You can make
<strong>
one download request every 14 days.
</strong>
</p>
</div>
<div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-downloadCommentHistory-requestButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Request
</button>
</div>
</div>
</div>
<div
className="DeleteAccountContainer-root coral coral-deleteMyAccount"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignFlexStart"
data-testid="profile-account-deleteAccount"
>
<div
className="DeleteAccountContainer-content"
>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark DeleteAccountContainer-title"
>
Delete My Account
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary DeleteAccountContainer-section"
>
Deleting your account will permanently erase your profile and remove
all your comments from this site.
</p>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-deleteMyAccount-requestButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Request
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</section>
</main>
</div>
<div
className="coral coral-myPassword"
data-testid="profile-account-changePassword"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark"
>
Password
</h1>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-myPassword-editButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Edit
</button>
</div>
</div>
<div
className="DeleteAccountContainer-root coral coral-deleteMyAccount"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignFlexStart"
data-testid="profile-account-deleteAccount"
>
<div
className="DeleteAccountContainer-content"
>
<h1
className="Box-root Typography-root Typography-heading2 Typography-colorTextDark DeleteAccountContainer-title"
>
Delete My Account
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary DeleteAccountContainer-section"
>
Deleting your account will permanently erase your profile and remove
all your comments from this site.
</p>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlineFilled coral coral-deleteMyAccount-requestButton"
data-color="primary"
data-variant="outlineFilled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Request
</button>
</div>
</div>
</div>
</div>
`;
@@ -7,12 +7,10 @@ import {
createResolversStub,
CreateTestRendererParams,
waitForElement,
waitUntilThrow,
within,
} from "coral-framework/testHelpers";
import {
commenters,
settings,
settingsWithoutLocalAuth,
stories,
@@ -46,19 +44,9 @@ async function createTestRenderer(
},
});
const ignoredCommenters = await waitForElement(() =>
within(testRenderer.root).queryByTestID("profile-account-ignoredCommenters")
);
const changePassword = within(testRenderer.root).queryByTestID(
"profile-account-changePassword"
);
return {
testRenderer,
context,
ignoredCommenters,
changePassword,
};
}
@@ -66,18 +54,29 @@ it("renders the empty settings pane", async () => {
const {
testRenderer: { root },
} = await createTestRenderer();
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
const account = await waitForElement(() =>
within(root).getByTestID("profile-manageAccount")
);
expect(within(account).toJSON()).toMatchSnapshot();
expect(await within(account).axe()).toHaveNoViolations();
});
it("doesn't show the change password pane when local auth is disabled", async () => {
const { changePassword } = await createTestRenderer({
const {
testRenderer: { root },
} = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
settings: () => settingsWithoutLocalAuth,
},
}),
});
const account = await waitForElement(() =>
within(root).getByTestID("profile-manageAccount")
);
const changePassword = within(account).queryByTestID(
"profile-account-changePassword"
);
expect(changePassword).toBeNull();
});
@@ -143,57 +142,3 @@ it("render password change form", async () => {
expect(updatePassword.calledOnce).toBeTruthy();
});
it("render empty ignored users list", async () => {
const { ignoredCommenters } = await createTestRenderer();
const editButton = within(ignoredCommenters).getByText("Manage");
act(() => {
editButton.props.onClick();
});
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 {};
},
},
}),
});
const editButton = within(ignoredCommenters).getByText("Manage");
act(() => {
editButton.props.onClick();
});
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!);
});
@@ -7,10 +7,11 @@ import {
createResolversStub,
CreateTestRendererParams,
waitForElement,
waitUntilThrow,
within,
} from "coral-framework/testHelpers";
import { settings, stories, viewerPassive } from "../fixtures";
import { commenters, settings, stories, viewerPassive } from "../fixtures";
import create from "./create";
const story = stories[0];
@@ -32,16 +33,20 @@ async function createTestRenderer(
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("NOTIFICATIONS", "profileTab");
localRecord.setValue("PREFERENCES", "profileTab");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const ignoredCommenters = await waitForElement(() =>
within(testRenderer.root).queryByTestID("profile-account-ignoredCommenters")
);
return {
testRenderer,
context,
ignoredCommenters,
};
}
@@ -142,3 +147,57 @@ it("render notifications form", async () => {
// The save button should now be disabled.
expect(save.props.disabled).toEqual(true);
});
it("render empty ignored users list", async () => {
const { ignoredCommenters } = await createTestRenderer();
const editButton = within(ignoredCommenters).getByText("Manage");
act(() => {
editButton.props.onClick();
});
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 {};
},
},
}),
});
const editButton = within(ignoredCommenters).getByText("Manage");
act(() => {
editButton.props.onClick();
});
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!);
});
@@ -41,6 +41,7 @@ export interface SSOUserProfile {
username: string;
badges?: string[];
role?: GQLUSER_ROLE;
url?: string;
}
export interface SSOToken {
@@ -64,8 +65,9 @@ export const SSOUserProfileSchema = Joi.object()
username: Joi.string().required(),
badges: Joi.array().items(Joi.string()),
role: Joi.string().only(Object.values(GQLUSER_ROLE)),
url: Joi.string().uri(),
})
.optionalKeys(["badges", "role"]);
.optionalKeys(["badges", "role", "url"]);
export const SSOTokenSchema = Joi.object()
.keys({
@@ -91,7 +93,7 @@ export async function findOrCreateSSOUser(
const {
jti,
exp,
user: { id, email, username, badges, role },
user: { id, email, username, badges, role, url },
iat,
} = decodedToken;
@@ -109,6 +111,7 @@ export async function findOrCreateSSOUser(
type: "sso",
id,
});
if (!user) {
if (!integration.allowRegistration) {
// Registration is disabled, so we can't create the user user here.
@@ -131,6 +134,7 @@ export async function findOrCreateSSOUser(
id,
username,
role: role || GQLUSER_ROLE.COMMENTER,
ssoURL: url,
badges,
email,
emailVerified: true,
@@ -2098,6 +2098,11 @@ type User {
roles: [ADMIN, MODERATOR]
permit: [SUSPENDED, BANNED, PENDING_DELETION]
)
"""
ssoURL is the url for managing sso account
"""
ssoURL: String
}
"""
+2 -1
View File
@@ -30,11 +30,12 @@ export function getSSOProfile(user: Pick<User, "profiles">) {
export function needsSSOUpdate(
token: SSOUserProfile,
user: Pick<User, "email" | "username" | "badges" | "role">
user: Pick<User, "email" | "username" | "badges" | "role" | "ssoURL">
) {
return (
user.email !== token.email ||
user.username !== token.username ||
(user.ssoURL && user.ssoURL !== token.url) ||
(token.role && user.role !== token.role) ||
!isEqual(user.badges, token.badges)
);
+6
View File
@@ -392,6 +392,11 @@ export interface User extends TenantResource {
*/
badges?: string[];
/**
* ssoURL is the url where a user can manage their sso account
*/
ssoURL?: string;
/**
* emailVerificationID is used to store state regarding the verification state
* of an email address to prevent replay attacks.
@@ -485,6 +490,7 @@ export interface FindOrCreateUserInput {
avatar?: string;
email?: string;
badges?: string[];
ssoURL?: string;
emailVerified?: boolean;
role: GQLUSER_ROLE;
profile: Profile;
+1
View File
@@ -188,6 +188,7 @@ comments-featured-replies = Replies
profile-myCommentsTab = My Comments
profile-myCommentsTab-comments = My comments
profile-accountTab = Account
profile-preferencesTab = Preferences
accountSettings-manage-account = Manage your account