link-sharing.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import { useCallback, useState, useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { Button, Col, Row } from 'react-bootstrap'
  4. import { Trans } from 'react-i18next'
  5. import Tooltip from '../../../shared/components/tooltip'
  6. import Icon from '../../../shared/components/icon'
  7. import { useShareProjectContext } from './share-project-modal'
  8. import { setProjectAccessLevel } from '../utils/api'
  9. import CopyLink from '../../../shared/components/copy-link'
  10. import { useProjectContext } from '../../../shared/context/project-context'
  11. import * as eventTracking from '../../../infrastructure/event-tracking'
  12. import { useUserContext } from '../../../shared/context/user-context'
  13. import { getJSON } from '../../../infrastructure/fetch-json'
  14. import useAbortController from '../../../shared/hooks/use-abort-controller'
  15. export default function LinkSharing({ canAddCollaborators }) {
  16. const [inflight, setInflight] = useState(false)
  17. const { monitorRequest } = useShareProjectContext()
  18. const { _id: projectId, publicAccessLevel } = useProjectContext()
  19. // set the access level of a project
  20. const setAccessLevel = useCallback(
  21. newPublicAccessLevel => {
  22. setInflight(true)
  23. monitorRequest(() =>
  24. setProjectAccessLevel(projectId, newPublicAccessLevel)
  25. )
  26. .then(() => {
  27. // NOTE: not calling `updateProject` here as it receives data via
  28. // project:publicAccessLevel:changed over the websocket connection
  29. // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
  30. })
  31. .finally(() => {
  32. setInflight(false)
  33. })
  34. },
  35. [monitorRequest, projectId]
  36. )
  37. switch (publicAccessLevel) {
  38. // Private (with token-access available)
  39. case 'private':
  40. return (
  41. <PrivateSharing setAccessLevel={setAccessLevel} inflight={inflight} />
  42. )
  43. // Token-based access
  44. case 'tokenBased':
  45. return (
  46. <TokenBasedSharing
  47. setAccessLevel={setAccessLevel}
  48. inflight={inflight}
  49. canAddCollaborators={canAddCollaborators}
  50. />
  51. )
  52. // Legacy public-access
  53. case 'readAndWrite':
  54. case 'readOnly':
  55. return (
  56. <LegacySharing
  57. setAccessLevel={setAccessLevel}
  58. accessLevel={publicAccessLevel}
  59. inflight={inflight}
  60. />
  61. )
  62. default:
  63. return null
  64. }
  65. }
  66. LinkSharing.propTypes = {
  67. canAddCollaborators: PropTypes.bool,
  68. }
  69. function PrivateSharing({ setAccessLevel, inflight }) {
  70. return (
  71. <Row className="public-access-level">
  72. <Col xs={12} className="text-center">
  73. <Trans i18nKey="link_sharing_is_off" />
  74. <span>&nbsp;&nbsp;</span>
  75. <Button
  76. type="button"
  77. bsStyle="link"
  78. className="btn-inline-link"
  79. onClick={() => {
  80. setAccessLevel('tokenBased')
  81. eventTracking.sendMB('link-sharing-click')
  82. }}
  83. disabled={inflight}
  84. >
  85. <Trans i18nKey="turn_on_link_sharing" />
  86. </Button>
  87. <span>&nbsp;&nbsp;</span>
  88. <LinkSharingInfo />
  89. </Col>
  90. </Row>
  91. )
  92. }
  93. PrivateSharing.propTypes = {
  94. setAccessLevel: PropTypes.func.isRequired,
  95. inflight: PropTypes.bool,
  96. }
  97. function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
  98. const { _id: projectId } = useProjectContext()
  99. const [tokens, setTokens] = useState(null)
  100. const { signal } = useAbortController()
  101. useEffect(() => {
  102. getJSON(`/project/${projectId}/tokens`, { signal })
  103. .then(data => setTokens(data))
  104. .catch(error => console.error(error))
  105. }, [projectId, signal])
  106. return (
  107. <Row className="public-access-level">
  108. <Col xs={12} className="text-center">
  109. <strong>
  110. <Trans i18nKey="link_sharing_is_on" />
  111. </strong>
  112. <span>&nbsp;&nbsp;</span>
  113. <Button
  114. bsStyle="link"
  115. className="btn-inline-link"
  116. onClick={() => setAccessLevel('private')}
  117. disabled={inflight}
  118. >
  119. <Trans i18nKey="turn_off_link_sharing" />
  120. </Button>
  121. <span>&nbsp;&nbsp;</span>
  122. <LinkSharingInfo />
  123. </Col>
  124. <Col xs={12} className="access-token-display-area">
  125. <div className="access-token-wrapper">
  126. <strong>
  127. <Trans i18nKey="anyone_with_link_can_edit" />
  128. </strong>
  129. <AccessToken
  130. token={tokens?.readAndWrite}
  131. path="/"
  132. tooltipId="tooltip-copy-link-rw"
  133. />
  134. </div>
  135. <div className="access-token-wrapper">
  136. <strong>
  137. <Trans i18nKey="anyone_with_link_can_view" />
  138. </strong>
  139. <AccessToken
  140. token={tokens?.readOnly}
  141. path="/read/"
  142. tooltipId="tooltip-copy-link-ro"
  143. />
  144. </div>
  145. </Col>
  146. </Row>
  147. )
  148. }
  149. TokenBasedSharing.propTypes = {
  150. setAccessLevel: PropTypes.func.isRequired,
  151. inflight: PropTypes.bool,
  152. canAddCollaborators: PropTypes.bool,
  153. }
  154. function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
  155. return (
  156. <Row className="public-access-level">
  157. <Col xs={12} className="text-center">
  158. <strong>
  159. {accessLevel === 'readAndWrite' && (
  160. <Trans i18nKey="this_project_is_public" />
  161. )}
  162. {accessLevel === 'readOnly' && (
  163. <Trans i18nKey="this_project_is_public_read_only" />
  164. )}
  165. </strong>
  166. <span>&nbsp;&nbsp;</span>
  167. <Button
  168. type="button"
  169. bsStyle="link"
  170. className="btn-inline-link"
  171. onClick={() => setAccessLevel('private')}
  172. disabled={inflight}
  173. >
  174. <Trans i18nKey="make_private" />
  175. </Button>
  176. <span>&nbsp;&nbsp;</span>
  177. <LinkSharingInfo />
  178. </Col>
  179. </Row>
  180. )
  181. }
  182. LegacySharing.propTypes = {
  183. accessLevel: PropTypes.string.isRequired,
  184. setAccessLevel: PropTypes.func.isRequired,
  185. inflight: PropTypes.bool,
  186. }
  187. export function ReadOnlyTokenLink() {
  188. const { _id: projectId } = useProjectContext()
  189. const [tokens, setTokens] = useState(null)
  190. const { signal } = useAbortController()
  191. useEffect(() => {
  192. getJSON(`/project/${projectId}/tokens`, { signal })
  193. .then(data => setTokens(data))
  194. .catch(error => console.error(error))
  195. }, [projectId, signal])
  196. return (
  197. <Row className="public-access-level">
  198. <Col xs={12} className="access-token-display-area">
  199. <div className="access-token-wrapper">
  200. <strong>
  201. <Trans i18nKey="anyone_with_link_can_view" />
  202. </strong>
  203. <AccessToken
  204. token={tokens?.readOnly}
  205. path="/read/"
  206. tooltipId="tooltip-copy-link-ro"
  207. />
  208. </div>
  209. </Col>
  210. </Row>
  211. )
  212. }
  213. function AccessToken({ token, path, tooltipId }) {
  214. const { isAdmin } = useUserContext()
  215. if (!token) {
  216. return (
  217. <pre className="access-token">
  218. <span>
  219. <Trans i18nKey="loading" />…
  220. </span>
  221. </pre>
  222. )
  223. }
  224. let origin = window.location.origin
  225. if (isAdmin) {
  226. origin = window.ExposedSettings.siteUrl
  227. }
  228. const link = `${origin}${path}${token}`
  229. return (
  230. <pre className="access-token">
  231. <span>{link}</span>
  232. <CopyLink link={link} tooltipId={tooltipId} />
  233. </pre>
  234. )
  235. }
  236. AccessToken.propTypes = {
  237. token: PropTypes.string,
  238. tooltipId: PropTypes.string.isRequired,
  239. path: PropTypes.string.isRequired,
  240. }
  241. function LinkSharingInfo() {
  242. return (
  243. <Tooltip
  244. id="link-sharing-info"
  245. description={<Trans i18nKey="learn_more_about_link_sharing" />}
  246. >
  247. <a
  248. href="/learn/how-to/What_is_Link_Sharing%3F"
  249. target="_blank"
  250. rel="noopener"
  251. >
  252. <Icon type="question-circle" />
  253. </a>
  254. </Tooltip>
  255. )
  256. }