project-access.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import { useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row'
  4. import MaterialIcon from '@/shared/components/material-icon'
  5. import OLButton from '@/shared/components/ol/ol-button'
  6. import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
  7. import {
  8. Dropdown,
  9. DropdownDivider,
  10. DropdownItem,
  11. DropdownMenu,
  12. DropdownToggle,
  13. } from '@/shared/components/dropdown/dropdown-menu'
  14. import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
  15. import LinkSharing from '@/features/share-project-modal/components/link-sharing'
  16. import { useEditorContext } from '@/shared/context/editor-context'
  17. import { useProjectContext } from '@/shared/context/project-context'
  18. import {
  19. SharingLinkData,
  20. SharingLinkPrivileges,
  21. setPublicAccessLevel,
  22. updateSharingLink,
  23. } from '../utils/api'
  24. import MemberPrivileges from '@/features/share-project-modal/components/member-privileges'
  25. import RemoveSharingLinksModal from '@/features/share-project-modal/components/remove-sharing-links-modal'
  26. import {
  27. ProjectAccessType,
  28. useShareProjectContext,
  29. } from '@/features/share-project-modal/components/share-project-modal'
  30. import { ExcludeStrict } from '@ol-types/utils'
  31. type ProjectAccessProps = {
  32. setIsInvitedPeopleScreen: React.Dispatch<React.SetStateAction<boolean>>
  33. invitedPeopleCount: number
  34. }
  35. export type PendingAccessType = ExcludeStrict<
  36. ProjectAccessType,
  37. 'legacyLinkSharing'
  38. >
  39. function ProjectAccess({
  40. setIsInvitedPeopleScreen,
  41. invitedPeopleCount,
  42. }: ProjectAccessProps) {
  43. const { t } = useTranslation()
  44. const [pendingAccess, setPendingAccess] = useState<PendingAccessType | null>(
  45. null
  46. )
  47. const { isProjectOwner } = useEditorContext()
  48. // TODO get the group name from backend
  49. const groupName = 'your group'
  50. const {
  51. monitorRequest,
  52. setSuccessActionMessage,
  53. projectAccess,
  54. setProjectAccess,
  55. sharingLinkData,
  56. setSharingLinkData,
  57. } = useShareProjectContext()
  58. const { projectId } = useProjectContext()
  59. const privileges = sharingLinkData?.privileges
  60. const handleAccessChange = (newAccess: PendingAccessType) => {
  61. setPendingAccess(null)
  62. let reqBody: Pick<SharingLinkData, 'privileges' | 'subscriptionId'>
  63. if (newAccess === 'onlyInvitedPeople') {
  64. reqBody = { privileges: false }
  65. } else if (
  66. newAccess === 'anyoneInXyzWithTheLink' &&
  67. sharingLinkData?.subscriptionId
  68. ) {
  69. reqBody = {
  70. privileges: privileges || 'readOnly',
  71. subscriptionId: sharingLinkData.subscriptionId,
  72. }
  73. } else if (newAccess === 'anyoneWithTheLink') {
  74. reqBody = { privileges: privileges || 'readOnly' }
  75. } else {
  76. return
  77. }
  78. monitorRequest(async () => {
  79. const data = await updateSharingLink(projectId, reqBody)
  80. if (projectAccess === 'legacyLinkSharing') {
  81. await setPublicAccessLevel(projectId, 'private')
  82. }
  83. return data
  84. }).then(data => {
  85. setSharingLinkData(newAccess === 'onlyInvitedPeople' ? null : data)
  86. setProjectAccess(newAccess)
  87. setSuccessActionMessage(t('access_updated'))
  88. })
  89. }
  90. const onAccessSelect = (eventKey: ProjectAccessType) => {
  91. if (
  92. projectAccess === 'legacyLinkSharing' &&
  93. eventKey !== 'legacyLinkSharing'
  94. ) {
  95. // Legacy link sharing: show confirmation first
  96. setPendingAccess(eventKey as PendingAccessType)
  97. } else {
  98. // Non-legacy: fire request directly
  99. handleAccessChange(eventKey as PendingAccessType)
  100. }
  101. }
  102. const onPrivilegesChange = (
  103. eventKey: ExcludeStrict<SharingLinkPrivileges, false>
  104. ) => {
  105. monitorRequest(() =>
  106. updateSharingLink(projectId, {
  107. privileges: eventKey,
  108. subscriptionId: sharingLinkData?.subscriptionId,
  109. })
  110. ).then(data => {
  111. setSharingLinkData(data)
  112. setSuccessActionMessage(t('access_updated'))
  113. })
  114. }
  115. const getProjectAccessDropdownToggleText = () => {
  116. switch (projectAccess) {
  117. case 'legacyLinkSharing':
  118. return t('via_sharing_links_legacy')
  119. case 'onlyInvitedPeople':
  120. return t('only_invited_people')
  121. case 'anyoneInXyzWithTheLink':
  122. return t('anyone_in_x_with_the_link', { groupName })
  123. case 'anyoneWithTheLink':
  124. return t('anyone_with_the_link')
  125. default:
  126. return ''
  127. }
  128. }
  129. return (
  130. <>
  131. <h3 className="h4 fw-normal mt-3 mb-2 pt-1">{t('project_access')}</h3>
  132. <ShareProjectModalRow>
  133. <div className="d-inline-flex align-items-center h5 m-0 gap-2">
  134. <MaterialIcon type="group" unfilled />
  135. <div className="px-2 fw-normal">
  136. {t('x_people_invited', { count: invitedPeopleCount })}
  137. </div>
  138. </div>
  139. <OLButton
  140. variant="ghost"
  141. trailingIcon="chevron_right"
  142. onClick={() => setIsInvitedPeopleScreen(true)}
  143. >
  144. {t('manage_access')}
  145. </OLButton>
  146. </ShareProjectModalRow>
  147. {projectAccess && (
  148. <ShareProjectModalRow>
  149. <div className="d-inline-flex align-items-center h5 m-0">
  150. {projectAccess === 'legacyLinkSharing' && (
  151. <MaterialIcon type="link" />
  152. )}
  153. {projectAccess === 'onlyInvitedPeople' && (
  154. <MaterialIcon type="lock" unfilled />
  155. )}
  156. {projectAccess === 'anyoneInXyzWithTheLink' && (
  157. <MaterialIcon type="domain" unfilled />
  158. )}
  159. {projectAccess === 'anyoneWithTheLink' && (
  160. <MaterialIcon type="globe" unfilled />
  161. )}
  162. <Dropdown onSelect={onAccessSelect}>
  163. <DropdownToggle
  164. variant="ghost"
  165. className="d-flex align-items-center gap-2 no-default-caret"
  166. >
  167. {getProjectAccessDropdownToggleText()}
  168. <MaterialIcon type="keyboard_arrow_down" />
  169. </DropdownToggle>
  170. <DropdownMenu>
  171. {projectAccess === 'legacyLinkSharing' && (
  172. <>
  173. <DropdownListItem className="d-flex align-items-center">
  174. <DropdownItem
  175. as="button"
  176. eventKey="legacyLinkSharing"
  177. leadingIcon={<MaterialIcon type="link" />}
  178. trailingIcon={
  179. projectAccess === 'legacyLinkSharing'
  180. ? 'check'
  181. : undefined
  182. }
  183. active={projectAccess === 'legacyLinkSharing'}
  184. >
  185. {t('via_sharing_links_legacy')}
  186. </DropdownItem>
  187. </DropdownListItem>
  188. <DropdownDivider />
  189. </>
  190. )}
  191. <DropdownListItem className="d-flex align-items-center">
  192. <DropdownItem
  193. as="button"
  194. eventKey="onlyInvitedPeople"
  195. leadingIcon={<MaterialIcon type="lock" unfilled />}
  196. trailingIcon={
  197. projectAccess === 'onlyInvitedPeople'
  198. ? 'check'
  199. : undefined
  200. }
  201. active={projectAccess === 'onlyInvitedPeople'}
  202. >
  203. {t('only_invited_people')}
  204. </DropdownItem>
  205. </DropdownListItem>
  206. {sharingLinkData?.subscriptionId && (
  207. <DropdownListItem className="d-flex align-items-center">
  208. <DropdownItem
  209. as="button"
  210. eventKey="anyoneInXyzWithTheLink"
  211. leadingIcon={<MaterialIcon type="domain" unfilled />}
  212. trailingIcon={
  213. projectAccess === 'anyoneInXyzWithTheLink'
  214. ? 'check'
  215. : undefined
  216. }
  217. active={projectAccess === 'anyoneInXyzWithTheLink'}
  218. >
  219. {t('anyone_in_x_with_the_link', { groupName })}
  220. </DropdownItem>
  221. </DropdownListItem>
  222. )}
  223. <DropdownListItem className="d-flex align-items-center gap-2">
  224. <DropdownItem
  225. as="button"
  226. eventKey="anyoneWithTheLink"
  227. leadingIcon={<MaterialIcon type="globe" unfilled />}
  228. trailingIcon={
  229. projectAccess === 'anyoneWithTheLink'
  230. ? 'check'
  231. : undefined
  232. }
  233. active={projectAccess === 'anyoneWithTheLink'}
  234. >
  235. {t('anyone_with_the_link')}
  236. </DropdownItem>
  237. </DropdownListItem>
  238. </DropdownMenu>
  239. </Dropdown>
  240. {pendingAccess && (
  241. <RemoveSharingLinksModal
  242. pendingAccess={pendingAccess}
  243. onCancel={() => setPendingAccess(null)}
  244. onConfirm={() => handleAccessChange(pendingAccess)}
  245. />
  246. )}
  247. </div>
  248. {projectAccess !== 'legacyLinkSharing' && privileges && (
  249. <Dropdown align="end" onSelect={onPrivilegesChange}>
  250. <DropdownToggle
  251. variant="ghost"
  252. className="d-flex align-items-center gap-2 no-default-caret"
  253. >
  254. <MemberPrivileges privileges={privileges} />
  255. <MaterialIcon type="keyboard_arrow_down" />
  256. </DropdownToggle>
  257. <DropdownMenu>
  258. <OLDropdownMenuItem
  259. as="button"
  260. eventKey="readAndWrite"
  261. leadingIcon={<MaterialIcon type="edit" unfilled />}
  262. active={privileges === 'readAndWrite'}
  263. trailingIcon={
  264. privileges === 'readAndWrite' ? 'check' : undefined
  265. }
  266. >
  267. {t('editor')}
  268. </OLDropdownMenuItem>
  269. <OLDropdownMenuItem
  270. as="button"
  271. eventKey="review"
  272. leadingIcon={<MaterialIcon type="mode_comment" unfilled />}
  273. active={privileges === 'review'}
  274. trailingIcon={privileges === 'review' ? 'check' : undefined}
  275. >
  276. {t('reviewer')}
  277. </OLDropdownMenuItem>
  278. <OLDropdownMenuItem
  279. as="button"
  280. eventKey="readOnly"
  281. leadingIcon={<MaterialIcon type="visibility" unfilled />}
  282. active={privileges === 'readOnly'}
  283. trailingIcon={privileges === 'readOnly' ? 'check' : undefined}
  284. >
  285. {t('viewer')}
  286. </OLDropdownMenuItem>
  287. </DropdownMenu>
  288. </Dropdown>
  289. )}
  290. </ShareProjectModalRow>
  291. )}
  292. {isProjectOwner && <LinkSharing />}
  293. </>
  294. )
  295. }
  296. export default ProjectAccess