linking-section.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import { ElementType } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  4. import { useSSOContext, SSOSubscription } from '../context/sso-context'
  5. import { SSOLinkingWidget } from './linking/sso-widget'
  6. import getMeta from '../../../utils/meta'
  7. import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
  8. import OLNotification from '@/features/ui/components/ol/ol-notification'
  9. const availableIntegrationLinkingWidgets = importOverleafModules(
  10. 'integrationLinkingWidgets'
  11. ) as any[]
  12. const availableReferenceLinkingWidgets = importOverleafModules(
  13. 'referenceLinkingWidgets'
  14. ) as any[]
  15. const availableLangFeedbackLinkingWidgets = importOverleafModules(
  16. 'langFeedbackLinkingWidgets'
  17. ) as any[]
  18. function LinkingSection() {
  19. useBroadcastUser()
  20. const { t } = useTranslation()
  21. const { subscriptions } = useSSOContext()
  22. const ssoErrorMessage = getMeta('ol-ssoErrorMessage')
  23. const projectSyncSuccessMessage = getMeta('ol-projectSyncSuccessMessage')
  24. // hide linking widgets in CI
  25. const integrationLinkingWidgets = getMeta('ol-hideLinkingWidgets')
  26. ? []
  27. : availableIntegrationLinkingWidgets
  28. const referenceLinkingWidgets = getMeta('ol-hideLinkingWidgets')
  29. ? []
  30. : availableReferenceLinkingWidgets
  31. const langFeedbackLinkingWidgets = getMeta('ol-hideLinkingWidgets')
  32. ? []
  33. : availableLangFeedbackLinkingWidgets
  34. const oauth2ServerComponents = importOverleafModules('oauth2Server') as {
  35. import: { default: ElementType }
  36. path: string
  37. }[]
  38. const renderSyncSection =
  39. getMeta('ol-isSaas') || getMeta('ol-gitBridgeEnabled')
  40. const allIntegrationLinkingWidgets = integrationLinkingWidgets.concat(
  41. oauth2ServerComponents
  42. )
  43. const haslangFeedbackLinkingWidgets = langFeedbackLinkingWidgets.length
  44. const hasIntegrationLinkingSection =
  45. renderSyncSection && allIntegrationLinkingWidgets.length
  46. const hasReferencesLinkingSection = referenceLinkingWidgets.length
  47. // Filter out SSO providers that are not allowed to be linked by
  48. // managed users. Allow unlinking them if they are already linked.
  49. const hideGoogleSSO = getMeta('ol-cannot-link-google-sso')
  50. const hideOtherThirdPartySSO = getMeta('ol-cannot-link-other-third-party-sso')
  51. for (const providerId in subscriptions) {
  52. const isLinked = subscriptions[providerId].linked
  53. if (providerId === 'google') {
  54. if (hideGoogleSSO && !isLinked) {
  55. delete subscriptions[providerId]
  56. }
  57. } else {
  58. if (hideOtherThirdPartySSO && !isLinked) {
  59. delete subscriptions[providerId]
  60. }
  61. }
  62. }
  63. const hasSSOLinkingSection = Object.keys(subscriptions).length > 0
  64. if (
  65. !haslangFeedbackLinkingWidgets &&
  66. !hasIntegrationLinkingSection &&
  67. !hasReferencesLinkingSection &&
  68. !hasSSOLinkingSection
  69. ) {
  70. return null
  71. }
  72. return (
  73. <>
  74. <h3 id="integrations">{t('integrations')}</h3>
  75. <p className="small">{t('linked_accounts_explained')}</p>
  76. {haslangFeedbackLinkingWidgets ? (
  77. <>
  78. <h3 id="language-feedback" className="text-capitalize">
  79. {t('language_feedback')}
  80. </h3>
  81. <div className="settings-widgets-container">
  82. {langFeedbackLinkingWidgets.map(
  83. ({ import: { default: widget }, path }, widgetIndex) => (
  84. <ModuleLinkingWidget
  85. key={path}
  86. ModuleComponent={widget}
  87. isLast={widgetIndex === langFeedbackLinkingWidgets.length - 1}
  88. />
  89. )
  90. )}
  91. </div>
  92. </>
  93. ) : null}
  94. {hasIntegrationLinkingSection ? (
  95. <>
  96. <h3 id="project-sync" className="text-capitalize">
  97. {t('project_synchronisation')}
  98. </h3>
  99. {projectSyncSuccessMessage ? (
  100. <OLNotification
  101. type="success"
  102. content={projectSyncSuccessMessage}
  103. />
  104. ) : null}
  105. <div className="settings-widgets-container">
  106. {allIntegrationLinkingWidgets.map(
  107. ({ import: importObject, path }, widgetIndex) => (
  108. <ModuleLinkingWidget
  109. key={Object.keys(importObject)[0]}
  110. ModuleComponent={Object.values(importObject)[0]}
  111. isLast={
  112. widgetIndex === allIntegrationLinkingWidgets.length - 1
  113. }
  114. />
  115. )
  116. )}
  117. </div>
  118. </>
  119. ) : null}
  120. {hasReferencesLinkingSection ? (
  121. <>
  122. <h3 id="references" className="text-capitalize">
  123. {t('reference_managers')}
  124. </h3>
  125. <div className="settings-widgets-container">
  126. {referenceLinkingWidgets.map(
  127. ({ import: importObject, path }, widgetIndex) => (
  128. <ModuleLinkingWidget
  129. key={Object.keys(importObject)[0]}
  130. ModuleComponent={Object.values(importObject)[0]}
  131. isLast={widgetIndex === referenceLinkingWidgets.length - 1}
  132. />
  133. )
  134. )}
  135. </div>
  136. </>
  137. ) : null}
  138. {hasSSOLinkingSection ? (
  139. <>
  140. <h3 id="linked-accounts" className="text-capitalize">
  141. {t('linked_accounts')}
  142. </h3>
  143. {ssoErrorMessage ? (
  144. <OLNotification
  145. type="error"
  146. content={`${t('sso_link_error')}: ${ssoErrorMessage}`}
  147. />
  148. ) : null}
  149. <div className="settings-widgets-container">
  150. {Object.values(subscriptions).map(
  151. (subscription, subscriptionIndex) => (
  152. <SSOLinkingWidgetContainer
  153. key={subscription.providerId}
  154. subscription={subscription}
  155. isLast={
  156. subscriptionIndex === Object.keys(subscriptions).length - 1
  157. }
  158. />
  159. )
  160. )}
  161. </div>
  162. </>
  163. ) : null}
  164. {haslangFeedbackLinkingWidgets ||
  165. hasIntegrationLinkingSection ||
  166. hasReferencesLinkingSection ||
  167. hasSSOLinkingSection ? (
  168. <hr />
  169. ) : null}
  170. </>
  171. )
  172. }
  173. type LinkingWidgetProps = {
  174. ModuleComponent: any
  175. isLast: boolean
  176. }
  177. function ModuleLinkingWidget({ ModuleComponent, isLast }: LinkingWidgetProps) {
  178. return (
  179. <>
  180. <ModuleComponent />
  181. {isLast ? null : <hr />}
  182. </>
  183. )
  184. }
  185. type SSOLinkingWidgetContainerProps = {
  186. subscription: SSOSubscription
  187. isLast: boolean
  188. }
  189. function SSOLinkingWidgetContainer({
  190. subscription,
  191. isLast,
  192. }: SSOLinkingWidgetContainerProps) {
  193. const { t } = useTranslation()
  194. const { unlink } = useSSOContext()
  195. let description = ''
  196. switch (subscription.providerId) {
  197. case 'collabratec':
  198. description = t('linked_collabratec_description')
  199. break
  200. case 'google':
  201. description = `${t('login_with_service', {
  202. service: subscription.provider.name,
  203. })}.`
  204. break
  205. case 'orcid':
  206. description = t('oauth_orcid_description')
  207. break
  208. }
  209. return (
  210. <>
  211. <SSOLinkingWidget
  212. providerId={subscription.providerId}
  213. title={subscription.provider.name}
  214. description={description}
  215. helpPath={subscription.provider.descriptionOptions?.link}
  216. linked={subscription.linked}
  217. linkPath={subscription.provider.linkPath}
  218. onUnlink={() => unlink(subscription.providerId)}
  219. />
  220. {isLast ? null : <hr />}
  221. </>
  222. )
  223. }
  224. export default LinkingSection