Merge pull request #7723 from overleaf/ta-settings-refactor

[SettingsPage] Refactor Linking Section

GitOrigin-RevId: 49aa27cdcb3669c59c9a9c46edd3249cee876dd0
This commit is contained in:
Timothée Alby 2022-04-25 13:04:44 +02:00 committed by Copybot
parent bc574ef9e9
commit 146a207fd1
16 changed files with 217 additions and 205 deletions

View file

@ -774,6 +774,7 @@ module.exports = {
sourceEditorExtensions: [], sourceEditorExtensions: [],
sourceEditorComponents: [], sourceEditorComponents: [],
integrationLinkingWidgets: [], integrationLinkingWidgets: [],
referenceLinkingWidgets: [],
}, },
moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'], moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],

View file

@ -69,9 +69,12 @@ function EmailsSection() {
} }
return ( return (
<UserEmailsProvider> <>
<EmailsSectionContent /> <UserEmailsProvider>
</UserEmailsProvider> <EmailsSectionContent />
</UserEmailsProvider>
<hr />
</>
) )
} }

View file

@ -1,46 +0,0 @@
import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
const integrationLinkingWidgets = importOverleafModules(
'integrationLinkingWidgets'
)
function IntegrationLinkingSection() {
const { t } = useTranslation()
return (
<>
<h3>{t('integrations')}</h3>
<p>{t('linked_accounts_explained')}</p>
<div className="settings-widgets-container">
{integrationLinkingWidgets.map(
({ import: importObject, path }, widgetIndex) => (
<IntegrationLinkingWidget
key={Object.keys(importObject)[0]}
ModuleComponent={Object.values(importObject)[0]}
isLast={widgetIndex === integrationLinkingWidgets.length - 1}
/>
)
)}
</div>
</>
)
}
type IntegrationLinkingWidgetProps = {
ModuleComponent: any
isLast: boolean
}
function IntegrationLinkingWidget({
ModuleComponent,
isLast,
}: IntegrationLinkingWidgetProps) {
return (
<>
<ModuleComponent />
{isLast ? null : <hr />}
</>
)
}
export default IntegrationLinkingSection

View file

@ -0,0 +1,135 @@
import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { useSSOContext, SSOSubscription } from '../context/sso-context'
import { SSOLinkingWidget } from './linking/sso-widget'
const integrationLinkingWidgets = importOverleafModules(
'integrationLinkingWidgets'
)
const referenceLinkingWidgets = importOverleafModules('referenceLinkingWidgets')
function LinkingSection() {
const { t } = useTranslation()
const { subscriptions } = useSSOContext()
const hasIntegrationLinkingSection = integrationLinkingWidgets.length
const hasReferencesLinkingSection = referenceLinkingWidgets.length
const hasSSOLinkingSection = Object.keys(subscriptions).length > 0
if (
!hasIntegrationLinkingSection &&
!hasReferencesLinkingSection &&
!hasSSOLinkingSection
) {
return null
}
return (
<>
<h3>{t('integrations')}</h3>
<p>{t('linked_accounts_explained')}</p>
{hasIntegrationLinkingSection ? (
<>
<h3 className="text-capitalize">{t('sync_dropbox_github')}</h3>
<div className="settings-widgets-container">
{integrationLinkingWidgets.map(
({ import: importObject, path }, widgetIndex) => (
<ModuleLinkingWidget
key={Object.keys(importObject)[0]}
ModuleComponent={Object.values(importObject)[0]}
isLast={widgetIndex === integrationLinkingWidgets.length - 1}
/>
)
)}
</div>
</>
) : null}
{hasReferencesLinkingSection ? (
<>
<h3 className="text-capitalize">{t('reference_sync')}</h3>
<div className="settings-widgets-container">
{referenceLinkingWidgets.map(
({ import: importObject, path }, widgetIndex) => (
<ModuleLinkingWidget
key={Object.keys(importObject)[0]}
ModuleComponent={Object.values(importObject)[0]}
isLast={widgetIndex === referenceLinkingWidgets.length - 1}
/>
)
)}
</div>
</>
) : null}
{hasSSOLinkingSection ? (
<>
<h3 className="text-capitalize">{t('linked_accounts')}</h3>
<div className="settings-widgets-container">
{Object.values(subscriptions).map(
(subscription, subscriptionIndex) => (
<SSOLinkingWidgetContainer
key={subscription.providerId}
subscription={subscription}
isLast={
subscriptionIndex === Object.keys(subscriptions).length - 1
}
/>
)
)}
</div>
</>
) : null}
{hasIntegrationLinkingSection ||
hasReferencesLinkingSection ||
hasSSOLinkingSection ? (
<hr />
) : null}
</>
)
}
type LinkingWidgetProps = {
ModuleComponent: any
isLast: boolean
}
function ModuleLinkingWidget({ ModuleComponent, isLast }: LinkingWidgetProps) {
return (
<>
<ModuleComponent />
{isLast ? null : <hr />}
</>
)
}
type SSOLinkingWidgetContainerProps = {
subscription: SSOSubscription
isLast: boolean
}
function SSOLinkingWidgetContainer({
subscription,
isLast,
}: SSOLinkingWidgetContainerProps) {
const { t } = useTranslation()
const { unlink } = useSSOContext()
return (
<>
<SSOLinkingWidget
providerId={subscription.providerId}
title={subscription.provider.name}
description={t(
subscription.provider.descriptionKey,
subscription.provider.descriptionOptions
)}
helpPath={subscription.provider.descriptionOptions?.link}
linked={subscription.linked}
linkPath={subscription.provider.linkPath}
onUnlink={() => unlink(subscription.providerId)}
/>
{isLast ? null : <hr />}
</>
)
}
export default LinkingSection

View file

@ -5,12 +5,12 @@ import getMeta from '../../../utils/meta'
import EmailsSection from './emails-section' import EmailsSection from './emails-section'
import AccountInfoSection from './account-info-section' import AccountInfoSection from './account-info-section'
import PasswordSection from './password-section' import PasswordSection from './password-section'
import IntegrationLinkingSection from './integration-linking-section' import LinkingSection from './linking-section'
import SSOLinkingSection from './sso-linking-section'
import MiscSection from './misc-section' import MiscSection from './misc-section'
import LeaveSection from './leave-section' import LeaveSection from './leave-section'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { UserProvider } from '../../../shared/context/user-context' import { UserProvider } from '../../../shared/context/user-context'
import { SSOProvider } from '../context/sso-context'
function SettingsPageRoot() { function SettingsPageRoot() {
const { t } = useTranslation() const { t } = useTranslation()
@ -34,7 +34,7 @@ function SettingsPageRoot() {
<div className="page-header"> <div className="page-header">
<h1>{t('account_settings')}</h1> <h1>{t('account_settings')}</h1>
</div> </div>
<div className="account-settings"> <div>
<EmailsSection /> <EmailsSection />
<div className="row"> <div className="row">
<div className="col-md-5"> <div className="col-md-5">
@ -45,10 +45,9 @@ function SettingsPageRoot() {
</div> </div>
</div> </div>
<hr /> <hr />
<IntegrationLinkingSection /> <SSOProvider>
<hr /> <LinkingSection />
<SSOLinkingSection /> </SSOProvider>
<hr />
<MiscSection /> <MiscSection />
<hr /> <hr />
<LeaveSection /> <LeaveSection />

View file

@ -1,68 +0,0 @@
import { useTranslation } from 'react-i18next'
import {
SSOProvider,
useSSOContext,
SSOSubscription,
} from '../context/sso-context'
import { SSOLinkingWidget } from './sso-linking/widget'
function SSOLinkingSection() {
const { t } = useTranslation()
return (
<SSOProvider>
<h3 className="text-capitalize">{t('linked_accounts')}</h3>
<p>{t('linked_accounts_explained')}</p>
<SSOLinkingWidgets />
</SSOProvider>
)
}
function SSOLinkingWidgets() {
const { subscriptions } = useSSOContext()
return (
<div className="settings-widgets-container">
{Object.values(subscriptions).map((subscription, subscriptionIndex) => (
<SSOLinkingWidgetContainer
key={subscription.providerId}
subscription={subscription}
isLast={subscriptionIndex === Object.keys(subscriptions).length - 1}
/>
))}
</div>
)
}
type SSOLinkingWidgetContainerProps = {
subscription: SSOSubscription
isLast: boolean
}
function SSOLinkingWidgetContainer({
subscription,
isLast,
}: SSOLinkingWidgetContainerProps) {
const { t } = useTranslation()
const { unlink } = useSSOContext()
return (
<>
<SSOLinkingWidget
providerId={subscription.providerId}
title={subscription.provider.name}
description={t(
subscription.provider.descriptionKey,
subscription.provider.descriptionOptions
)}
helpPath={subscription.provider.descriptionOptions?.link}
linked={subscription.linked}
linkPath={subscription.provider.linkPath}
onUnlink={() => unlink(subscription.providerId)}
/>
{isLast ? null : <hr />}
</>
)
}
export default SSOLinkingSection

View file

@ -1,21 +0,0 @@
const MOCK_DELAY = 1000
export function defaultSetupMocks(fetchMock) {
fetchMock.get(
'express:/user/tpds/queues',
{ tpdsToWeb: 0, webToTpds: 0 },
{ delay: MOCK_DELAY }
)
}
export function setDefaultMeta() {
window.metaAttributesCache.set('ol-user', {
features: { github: true, dropbox: true, mendeley: false, zotero: false },
refProviders: {
mendeley: true,
zotero: true,
},
})
window.metaAttributesCache.set('ol-github', { enabled: false })
window.metaAttributesCache.set('ol-dropbox', { registered: true })
}

View file

@ -1,10 +1,25 @@
const MOCK_DELAY = 1000 const MOCK_DELAY = 1000
export function defaultSetupMocks(fetchMock) { export function defaultSetupMocks(fetchMock) {
fetchMock.post('/user/oauth-unlink', 200, { delay: MOCK_DELAY }) fetchMock
.post('/user/oauth-unlink', 200, { delay: MOCK_DELAY })
.get(
'express:/user/tpds/queues',
{ tpdsToWeb: 0, webToTpds: 0 },
{ delay: MOCK_DELAY }
)
} }
export function setDefaultMeta() { export function setDefaultMeta() {
window.metaAttributesCache.set('ol-user', {
features: { github: true, dropbox: true, mendeley: false, zotero: false },
refProviders: {
mendeley: true,
zotero: true,
},
})
window.metaAttributesCache.set('ol-github', { enabled: false })
window.metaAttributesCache.set('ol-dropbox', { registered: true })
window.metaAttributesCache.set('ol-thirdPartyIds', { window.metaAttributesCache.set('ol-thirdPartyIds', {
collabratec: 'collabratec-id', collabratec: 'collabratec-id',
google: 'google-id', google: 'google-id',

View file

@ -1,23 +0,0 @@
import useFetchMock from '../hooks/use-fetch-mock'
import IntegrationLinkingSection from '../../js/features/settings/components/integration-linking-section'
import {
setDefaultMeta,
defaultSetupMocks,
} from './helpers/integration-linking'
import { UserProvider } from '../../js/shared/context/user-context'
export const Section = args => {
useFetchMock(defaultSetupMocks)
setDefaultMeta()
return (
<UserProvider>
<IntegrationLinkingSection {...args} />
</UserProvider>
)
}
export default {
title: 'Account Settings / Integration Linking / Section',
component: IntegrationLinkingSection,
}

View file

@ -0,0 +1,46 @@
import useFetchMock from '../hooks/use-fetch-mock'
import LinkingSection from '../../js/features/settings/components/linking-section'
import { setDefaultMeta, defaultSetupMocks } from './helpers/linking'
import { UserProvider } from '../../js/shared/context/user-context'
import { SSOProvider } from '../../js/features/settings/context/sso-context'
export const Section = args => {
useFetchMock(defaultSetupMocks)
setDefaultMeta()
return (
<UserProvider>
<SSOProvider>
<LinkingSection {...args} />
</SSOProvider>
</UserProvider>
)
}
export const SectionAllUnlinked = args => {
useFetchMock(defaultSetupMocks)
setDefaultMeta()
window.metaAttributesCache.set('ol-thirdPartyIds', {})
window.metaAttributesCache.set('ol-user', {
features: { github: true, dropbox: true, mendeley: true, zotero: true },
refProviders: {
mendeley: false,
zotero: false,
},
})
window.metaAttributesCache.set('ol-github', { enabled: false })
window.metaAttributesCache.set('ol-dropbox', { registered: false })
return (
<UserProvider>
<SSOProvider>
<LinkingSection {...args} />
</SSOProvider>
</UserProvider>
)
}
export default {
title: 'Account Settings / Linking',
component: LinkingSection,
}

View file

@ -17,13 +17,9 @@ import {
defaultSetupMocks as defaultSetupEmailsMocks, defaultSetupMocks as defaultSetupEmailsMocks,
} from './helpers/emails' } from './helpers/emails'
import { import {
setDefaultMeta as setDefaultIntegrationLinkingMeta, setDefaultMeta as setDefaultLinkingMeta,
defaultSetupMocks as defaultSetupIntegrationLinkingMocks, defaultSetupMocks as defaultSetupLinkingMocks,
} from './helpers/integration-linking' } from './helpers/linking'
import {
setDefaultMeta as setDefaultSSOMeta,
defaultSetupMocks as defaultSetupSSOMocks,
} from './helpers/sso-linking'
import { UserProvider } from '../../js/shared/context/user-context' import { UserProvider } from '../../js/shared/context/user-context'
export const Root = args => { export const Root = args => {
@ -31,14 +27,12 @@ export const Root = args => {
setDefaultAccountInfoMeta() setDefaultAccountInfoMeta()
setDefaultPasswordMeta() setDefaultPasswordMeta()
setDefaultEmailsMeta() setDefaultEmailsMeta()
setDefaultIntegrationLinkingMeta() setDefaultLinkingMeta()
setDefaultSSOMeta()
useFetchMock(defaultSetupLeaveMocks) useFetchMock(defaultSetupLeaveMocks)
useFetchMock(defaultSetupAccountInfoMocks) useFetchMock(defaultSetupAccountInfoMocks)
useFetchMock(defaultSetupPasswordMocks) useFetchMock(defaultSetupPasswordMocks)
useFetchMock(defaultSetupEmailsMocks) useFetchMock(defaultSetupEmailsMocks)
useFetchMock(defaultSetupIntegrationLinkingMocks) useFetchMock(defaultSetupLinkingMocks)
useFetchMock(defaultSetupSSOMocks)
return ( return (
<UserProvider> <UserProvider>

View file

@ -1,23 +0,0 @@
import useFetchMock from '../hooks/use-fetch-mock'
import SSOLinkingSection from '../../js/features/settings/components/sso-linking-section'
import { setDefaultMeta, defaultSetupMocks } from './helpers/sso-linking'
export const Section = args => {
useFetchMock(defaultSetupMocks)
setDefaultMeta()
return <SSOLinkingSection {...args} />
}
export const SectionAllUnlinked = args => {
useFetchMock(defaultSetupMocks)
setDefaultMeta()
window.metaAttributesCache.set('ol-thirdPartyIds', {})
return <SSOLinkingSection {...args} />
}
export default {
title: 'Account Settings / SSO Linking / Section',
component: SSOLinkingSection,
}

View file

@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { screen, fireEvent, render, waitFor } from '@testing-library/react' import { screen, fireEvent, render, waitFor } from '@testing-library/react'
import { IntegrationLinkingWidget } from '../../../../../../frontend/js/features/settings/components/integration-linking/widget' import { IntegrationLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/integration-widget'
describe('<IntegrationLinkingWidgetTest/>', function () { describe('<IntegrationLinkingWidgetTest/>', function () {
const defaultProps = { const defaultProps = {

View file

@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import { screen, fireEvent, render, waitFor } from '@testing-library/react' import { screen, fireEvent, render, waitFor } from '@testing-library/react'
import { SSOLinkingWidget } from '../../../../../../frontend/js/features/settings/components/sso-linking/widget' import { SSOLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/sso-widget'
describe('<SSOLinkingWidget />', function () { describe('<SSOLinkingWidget />', function () {
const defaultProps = { const defaultProps = {