Bläddra i källkod

Merge pull request #25550 from overleaf/dp-share-modal-proptypes

Remove proptypes from ShareProjectModal

GitOrigin-RevId: b95fed5007f72e4a57a65b1d08d8fcc9579b3630
David 1 år sedan
förälder
incheckning
966cea3d8b

+ 12 - 13
services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx → services/web/frontend/js/features/share-project-modal/components/add-collaborators.tsx

@@ -2,20 +2,19 @@ import { useEffect, useState, useMemo, useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useMultipleSelection } from 'downshift'
 import { useShareProjectContext } from './share-project-modal'
-import SelectCollaborators from './select-collaborators'
+import SelectCollaborators, { ContactItem } from './select-collaborators'
 import { resendInvite, sendInvite } from '../utils/api'
 import { useUserContacts } from '../hooks/use-user-contacts'
 import useIsMounted from '@/shared/hooks/use-is-mounted'
 import { useProjectContext } from '@/shared/context/project-context'
 import { sendMB } from '@/infrastructure/event-tracking'
 import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
-import PropTypes from 'prop-types'
 import OLForm from '@/features/ui/components/ol/ol-form'
 import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
 import { Select } from '@/shared/components/select'
 import OLButton from '@/features/ui/components/ol/ol-button'
 
-export default function AddCollaborators({ readOnly }) {
+export default function AddCollaborators({ readOnly }: { readOnly?: boolean }) {
   const [privileges, setPrivileges] = useState('readAndWrite')
 
   const isMounted = useIsMounted()
@@ -43,7 +42,7 @@ export default function AddCollaborators({ readOnly }) {
     )
   }, [contacts, currentMemberEmails])
 
-  const multipleSelectionProps = useMultipleSelection({
+  const multipleSelectionProps = useMultipleSelection<ContactItem>({
     initialActiveIndex: 0,
     initialSelectedItems: [],
   })
@@ -129,7 +128,7 @@ export default function AddCollaborators({ readOnly }) {
               ? previousViewersAmount + 1
               : previousViewersAmount,
         })
-      } catch (error) {
+      } catch (error: any) {
         setInFlight(false)
         setError(
           error.data?.errorReason ||
@@ -213,13 +212,17 @@ export default function AddCollaborators({ readOnly }) {
             dataTestId="add-collaborator-select"
             items={privilegeOptions}
             itemToKey={item => item.key}
-            itemToString={item => item.label}
-            itemToSubtitle={item => item.description || ''}
-            itemToDisabled={item => readOnly && item.key !== 'readOnly'}
+            itemToString={item => item?.label || ''}
+            itemToSubtitle={item => item?.description || ''}
+            itemToDisabled={item => !!(readOnly && item?.key !== 'readOnly')}
             selected={privilegeOptions.find(
               option => option.key === privileges
             )}
-            onSelectedItemChanged={item => setPrivileges(item.key)}
+            onSelectedItemChanged={item => {
+              if (item) {
+                setPrivileges(item.key)
+              }
+            }}
           />
           <ClickableElementEnhancer
             as={OLButton}
@@ -233,7 +236,3 @@ export default function AddCollaborators({ readOnly }) {
     </OLForm>
   )
 }
-
-AddCollaborators.propTypes = {
-  readOnly: PropTypes.bool,
-}

+ 0 - 10
services/web/frontend/js/features/share-project-modal/components/edit-member.tsx

@@ -1,5 +1,4 @@
 import { useState, useEffect, useMemo } from 'react'
-import PropTypes from 'prop-types'
 import { Trans, useTranslation } from 'react-i18next'
 import { useShareProjectContext } from './share-project-modal'
 import TransferOwnershipModal from './transfer-ownership-modal'
@@ -227,15 +226,6 @@ export default function EditMember({
     </form>
   )
 }
-EditMember.propTypes = {
-  member: PropTypes.shape({
-    _id: PropTypes.string.isRequired,
-    email: PropTypes.string.isRequired,
-    privileges: PropTypes.string.isRequired,
-  }),
-  hasExceededCollaboratorLimit: PropTypes.bool.isRequired,
-  canAddCollaborators: PropTypes.bool.isRequired,
-}
 
 type SelectPrivilegeProps = {
   value: string

+ 14 - 19
services/web/frontend/js/features/share-project-modal/components/invite.jsx → services/web/frontend/js/features/share-project-modal/components/invite.tsx

@@ -1,5 +1,4 @@
 import { useCallback } from 'react'
-import PropTypes from 'prop-types'
 import { useShareProjectContext } from './share-project-modal'
 import { useTranslation } from 'react-i18next'
 import MemberPrivileges from './member-privileges'
@@ -11,8 +10,15 @@ import OLCol from '@/features/ui/components/ol/ol-col'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLButton from '@/features/ui/components/ol/ol-button'
 import MaterialIcon from '@/shared/components/material-icon'
+import { ProjectContextMember } from '@/shared/context/types/project-context'
 
-export default function Invite({ invite, isProjectOwner }) {
+export default function Invite({
+  invite,
+  isProjectOwner,
+}: {
+  invite: ProjectContextMember
+  isProjectOwner: boolean
+}) {
   const { t } = useTranslation()
   return (
     <OLRow className="project-invite">
@@ -38,12 +44,7 @@ export default function Invite({ invite, isProjectOwner }) {
   )
 }
 
-Invite.propTypes = {
-  invite: PropTypes.object.isRequired,
-  isProjectOwner: PropTypes.bool.isRequired,
-}
-
-function ResendInvite({ invite }) {
+function ResendInvite({ invite }: { invite: ProjectContextMember }) {
   const { t } = useTranslation()
   const { monitorRequest, setError, inFlight } = useShareProjectContext()
   const { _id: projectId } = useProjectContext()
@@ -66,7 +67,9 @@ function ResendInvite({ invite }) {
           // if (buttonRef.current) {
           //   buttonRef.current.blur()
           // }
-          document.activeElement.blur()
+          if (document.activeElement) {
+            ;(document.activeElement as HTMLElement).blur()
+          }
         }),
     [invite, monitorRequest, projectId, setError]
   )
@@ -84,16 +87,12 @@ function ResendInvite({ invite }) {
   )
 }
 
-ResendInvite.propTypes = {
-  invite: PropTypes.object.isRequired,
-}
-
-function RevokeInvite({ invite }) {
+function RevokeInvite({ invite }: { invite: ProjectContextMember }) {
   const { t } = useTranslation()
   const { updateProject, monitorRequest } = useShareProjectContext()
   const { _id: projectId, invites, members } = useProjectContext()
 
-  function handleClick(event) {
+  function handleClick(event: React.MouseEvent) {
     event.preventDefault()
 
     monitorRequest(() => revokeInvite(projectId, invite)).then(() => {
@@ -126,7 +125,3 @@ function RevokeInvite({ invite }) {
     </OLTooltip>
   )
 }
-
-RevokeInvite.propTypes = {
-  invite: PropTypes.object.isRequired,
-}

+ 50 - 34
services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx → services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx

@@ -1,5 +1,4 @@
 import { useCallback, useState, useEffect } from 'react'
-import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 import { useShareProjectContext } from './share-project-modal'
 import { setProjectAccessLevel } from '../utils/api'
@@ -18,6 +17,16 @@ import OLButton from '@/features/ui/components/ol/ol-button'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import MaterialIcon from '@/shared/components/material-icon'
 
+type Tokens = {
+  readAndWrite: string
+  readAndWriteHashPrefix: string
+  readAndWritePrefix: string
+  readOnly: string
+  readOnlyHashPrefix: string
+}
+
+type AccessLevel = 'private' | 'tokenBased' | 'readAndWrite' | 'readOnly'
+
 export default function LinkSharing() {
   const [inflight, setInflight] = useState(false)
   const [showLinks, setShowLinks] = useState(true)
@@ -28,7 +37,7 @@ export default function LinkSharing() {
 
   // set the access level of a project
   const setAccessLevel = useCallback(
-    newPublicAccessLevel => {
+    (newPublicAccessLevel: string) => {
       setInflight(true)
       sendMB('link-sharing-click-off', {
         project_id: projectId,
@@ -75,6 +84,7 @@ export default function LinkSharing() {
     case 'readAndWrite':
     case 'readOnly':
       return (
+        // TODO: do we even need this anymore?
         <LegacySharing
           setAccessLevel={setAccessLevel}
           accessLevel={publicAccessLevel}
@@ -87,7 +97,17 @@ export default function LinkSharing() {
   }
 }
 
-function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
+function PrivateSharing({
+  setAccessLevel,
+  inflight,
+  projectId,
+  setShowLinks,
+}: {
+  setAccessLevel: (level: AccessLevel) => void
+  inflight: boolean
+  projectId: string
+  setShowLinks: (show: boolean) => void
+}) {
   const { t } = useTranslation()
   return (
     <OLRow className="public-access-level">
@@ -113,23 +133,21 @@ function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
   )
 }
 
-PrivateSharing.propTypes = {
-  setAccessLevel: PropTypes.func.isRequired,
-  inflight: PropTypes.bool,
-  projectId: PropTypes.string,
-  setShowLinks: PropTypes.func.isRequired,
-}
-
 function TokenBasedSharing({
   setAccessLevel,
   inflight,
   setShowLinks,
   showLinks,
+}: {
+  setAccessLevel: (level: AccessLevel) => void
+  inflight: boolean
+  setShowLinks: (show: boolean) => void
+  showLinks: boolean
 }) {
   const { t } = useTranslation()
   const { _id: projectId } = useProjectContext()
 
-  const [tokens, setTokens] = useState(null)
+  const [tokens, setTokens] = useState<Tokens | null>(null)
 
   const { signal } = useAbortController()
 
@@ -190,14 +208,15 @@ function TokenBasedSharing({
   )
 }
 
-TokenBasedSharing.propTypes = {
-  setAccessLevel: PropTypes.func.isRequired,
-  inflight: PropTypes.bool,
-  setShowLinks: PropTypes.func.isRequired,
-  showLinks: PropTypes.bool,
-}
-
-function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
+function LegacySharing({
+  accessLevel,
+  setAccessLevel,
+  inflight,
+}: {
+  accessLevel: AccessLevel
+  setAccessLevel: (level: AccessLevel) => void
+  inflight: boolean
+}) {
   const { t } = useTranslation()
 
   return (
@@ -223,17 +242,11 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
   )
 }
 
-LegacySharing.propTypes = {
-  accessLevel: PropTypes.string.isRequired,
-  setAccessLevel: PropTypes.func.isRequired,
-  inflight: PropTypes.bool,
-}
-
 export function ReadOnlyTokenLink() {
   const { t } = useTranslation()
   const { _id: projectId } = useProjectContext()
 
-  const [tokens, setTokens] = useState(null)
+  const [tokens, setTokens] = useState<Tokens | null>(null)
 
   const { signal } = useAbortController()
 
@@ -260,7 +273,17 @@ export function ReadOnlyTokenLink() {
   )
 }
 
-function AccessToken({ token, tokenHashPrefix, path, tooltipId }) {
+function AccessToken({
+  token,
+  tokenHashPrefix,
+  path,
+  tooltipId,
+}: {
+  token?: string
+  tokenHashPrefix?: string
+  path: string
+  tooltipId: string
+}) {
   const { t } = useTranslation()
   const { isAdmin } = useUserContext()
 
@@ -288,13 +311,6 @@ function AccessToken({ token, tokenHashPrefix, path, tooltipId }) {
   )
 }
 
-AccessToken.propTypes = {
-  token: PropTypes.string,
-  tokenHashPrefix: PropTypes.string,
-  tooltipId: PropTypes.string.isRequired,
-  path: PropTypes.string.isRequired,
-}
-
 function LinkSharingInfo() {
   const { t } = useTranslation()
 

+ 6 - 5
services/web/frontend/js/features/share-project-modal/components/member-privileges.jsx → services/web/frontend/js/features/share-project-modal/components/member-privileges.tsx

@@ -1,7 +1,11 @@
-import PropTypes from 'prop-types'
+import { ProjectContextMember } from '@/shared/context/types/project-context'
 import { useTranslation } from 'react-i18next'
 
-export default function MemberPrivileges({ privileges }) {
+export default function MemberPrivileges({
+  privileges,
+}: {
+  privileges: ProjectContextMember['privileges']
+}) {
   const { t } = useTranslation()
 
   switch (privileges) {
@@ -18,6 +22,3 @@ export default function MemberPrivileges({ privileges }) {
       return null
   }
 }
-MemberPrivileges.propTypes = {
-  privileges: PropTypes.string.isRequired,
-}

+ 0 - 0
services/web/frontend/js/features/share-project-modal/components/owner-info.jsx → services/web/frontend/js/features/share-project-modal/components/owner-info.tsx


+ 40 - 42
services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx → services/web/frontend/js/features/share-project-modal/components/select-collaborators.tsx

@@ -1,14 +1,20 @@
 import { useEffect, useMemo, useState, useRef, useCallback } from 'react'
-import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 import { matchSorter } from 'match-sorter'
-import { useCombobox } from 'downshift'
+import { useCombobox, UseMultipleSelectionReturnValue } from 'downshift'
 import classnames from 'classnames'
 
 import MaterialIcon from '@/shared/components/material-icon'
 import Tag from '@/features/ui/components/bootstrap-5/tag'
 import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 import { Spinner } from 'react-bootstrap'
+import { Contact } from '../utils/types'
+
+export type ContactItem = {
+  email: string
+  display: string
+  type: string
+}
 
 // Unicode characters in these Unicode groups:
 //  "General Punctuation — Spaces"
@@ -21,6 +27,11 @@ export default function SelectCollaborators({
   options,
   placeholder,
   multipleSelectionProps,
+}: {
+  loading: boolean
+  options: Contact[]
+  placeholder: string
+  multipleSelectionProps: UseMultipleSelectionReturnValue<ContactItem>
 }) {
   const { t } = useTranslation()
   const {
@@ -58,12 +69,14 @@ export default function SelectCollaborators({
     })
   }, [unselectedOptions, inputValue])
 
-  const inputRef = useRef(null)
+  const inputRef = useRef<HTMLInputElement>(null)
 
   const focusInput = useCallback(() => {
     if (inputRef.current) {
       window.setTimeout(() => {
-        inputRef.current.focus()
+        if (inputRef.current) {
+          inputRef.current.focus()
+        }
       }, 10)
     }
   }, [inputRef])
@@ -80,7 +93,7 @@ export default function SelectCollaborators({
     return true
   }, [inputValue, selectedItems])
 
-  function stateReducer(state, actionAndChanges) {
+  function stateReducer(_: unknown, actionAndChanges: any) {
     const { type, changes } = actionAndChanges
     // force selected item to be null so that adding, removing, then re-adding the same collaborator is recognised as a selection change
     if (type === useCombobox.stateChangeTypes.InputChange) {
@@ -101,7 +114,7 @@ export default function SelectCollaborators({
     inputValue,
     defaultHighlightedIndex: 0,
     items: filteredOptions,
-    itemToString: item => item && item.name,
+    itemToString: item => (item && item.name) || '',
     stateReducer,
     onStateChange: ({ inputValue, type, selectedItem }) => {
       switch (type) {
@@ -119,7 +132,7 @@ export default function SelectCollaborators({
   })
 
   const addNewItem = useCallback(
-    (_email, focus = true) => {
+    (_email: string, focus = true) => {
       const email = _email.replace(matchAllSpaces, '')
 
       if (
@@ -200,7 +213,7 @@ export default function SelectCollaborators({
                   addNewItem(inputValue, false)
                 },
                 onChange: e => {
-                  setInputValue(e.target.value)
+                  setInputValue((e.target as HTMLInputElement).value)
                 },
                 onClick: () => focusInput,
                 onKeyDown: event => {
@@ -230,7 +243,7 @@ export default function SelectCollaborators({
                   const data =
                     // modern browsers
                     event.clipboardData?.getData('text/plain') ??
-                    // IE11
+                    // @ts-ignore IE11
                     window.clipboardData?.getData('text')
 
                   if (data) {
@@ -276,20 +289,18 @@ export default function SelectCollaborators({
     </div>
   )
 }
-SelectCollaborators.propTypes = {
-  loading: PropTypes.bool.isRequired,
-  options: PropTypes.array.isRequired,
-  placeholder: PropTypes.string,
-  multipleSelectionProps: PropTypes.shape({
-    getSelectedItemProps: PropTypes.func.isRequired,
-    getDropdownProps: PropTypes.func.isRequired,
-    addSelectedItem: PropTypes.func.isRequired,
-    removeSelectedItem: PropTypes.func.isRequired,
-    selectedItems: PropTypes.array.isRequired,
-  }).isRequired,
-}
 
-function Option({ selected, item, getItemProps, index }) {
+function Option({
+  selected,
+  item,
+  getItemProps,
+  index,
+}: {
+  selected: boolean
+  item: Contact
+  getItemProps: (any: any) => any
+  index: number
+}) {
   return (
     <li {...getItemProps({ item, index })}>
       <DropdownItem
@@ -306,24 +317,21 @@ function Option({ selected, item, getItemProps, index }) {
   )
 }
 
-Option.propTypes = {
-  selected: PropTypes.bool.isRequired,
-  item: PropTypes.shape({
-    display: PropTypes.string.isRequired,
-  }),
-  index: PropTypes.number.isRequired,
-  getItemProps: PropTypes.func.isRequired,
-}
-
 function SelectedItem({
   removeSelectedItem,
   selectedItem,
   focusInput,
   getSelectedItemProps,
   index,
+}: {
+  removeSelectedItem: (item: ContactItem) => void
+  selectedItem: ContactItem
+  focusInput: () => void
+  getSelectedItemProps: (any: any) => any
+  index: number
 }) {
   const handleClick = useCallback(
-    event => {
+    (event: React.MouseEvent) => {
       event.preventDefault()
       event.stopPropagation()
       removeSelectedItem(selectedItem)
@@ -344,13 +352,3 @@ function SelectedItem({
     </Tag>
   )
 }
-
-SelectedItem.propTypes = {
-  focusInput: PropTypes.func.isRequired,
-  removeSelectedItem: PropTypes.func.isRequired,
-  selectedItem: PropTypes.shape({
-    display: PropTypes.string.isRequired,
-  }),
-  getSelectedItemProps: PropTypes.func.isRequired,
-  index: PropTypes.number.isRequired,
-}

+ 5 - 8
services/web/frontend/js/features/share-project-modal/components/send-invites.jsx → services/web/frontend/js/features/share-project-modal/components/send-invites.tsx

@@ -2,7 +2,6 @@ import AddCollaborators from './add-collaborators'
 import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
 import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade'
 import AccessLevelsChanged from './access-levels-changed'
-import PropTypes from 'prop-types'
 import OLRow from '@/features/ui/components/ol/ol-row'
 
 export default function SendInvites({
@@ -10,6 +9,11 @@ export default function SendInvites({
   hasExceededCollaboratorLimit,
   haveAnyEditorsBeenDowngraded,
   somePendingEditorsResolved,
+}: {
+  canAddCollaborators: boolean
+  hasExceededCollaboratorLimit: boolean
+  haveAnyEditorsBeenDowngraded: boolean
+  somePendingEditorsResolved: boolean
 }) {
   return (
     <OLRow className="invite-controls">
@@ -30,10 +34,3 @@ export default function SendInvites({
     </OLRow>
   )
 }
-
-SendInvites.propTypes = {
-  canAddCollaborators: PropTypes.bool,
-  hasExceededCollaboratorLimit: PropTypes.bool,
-  haveAnyEditorsBeenDowngraded: PropTypes.bool,
-  somePendingEditorsResolved: PropTypes.bool,
-}

+ 8 - 6
services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx → services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.tsx

@@ -1,6 +1,5 @@
 import { useState } from 'react'
 import { Trans, useTranslation } from 'react-i18next'
-import PropTypes from 'prop-types'
 import { transferProjectOwnership } from '../utils/api'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useLocation } from '@/shared/hooks/use-location'
@@ -13,8 +12,15 @@ import OLModal, {
 import OLNotification from '@/features/ui/components/ol/ol-notification'
 import OLButton from '@/features/ui/components/ol/ol-button'
 import { Spinner } from 'react-bootstrap'
+import { ProjectContextMember } from '@/shared/context/types/project-context'
 
-export default function TransferOwnershipModal({ member, cancel }) {
+export default function TransferOwnershipModal({
+  member,
+  cancel,
+}: {
+  member: ProjectContextMember
+  cancel: () => void
+}) {
   const { t } = useTranslation()
 
   const [inflight, setInflight] = useState(false)
@@ -82,7 +88,3 @@ export default function TransferOwnershipModal({ member, cancel }) {
     </OLModal>
   )
 }
-TransferOwnershipModal.propTypes = {
-  member: PropTypes.object.isRequired,
-  cancel: PropTypes.func.isRequired,
-}

+ 6 - 10
services/web/frontend/js/features/share-project-modal/components/view-member.jsx → services/web/frontend/js/features/share-project-modal/components/view-member.tsx

@@ -1,10 +1,14 @@
-import PropTypes from 'prop-types'
 import MemberPrivileges from './member-privileges'
 import OLRow from '@/features/ui/components/ol/ol-row'
 import OLCol from '@/features/ui/components/ol/ol-col'
 import MaterialIcon from '@/shared/components/material-icon'
+import { ProjectContextMember } from '@/shared/context/types/project-context'
 
-export default function ViewMember({ member }) {
+export default function ViewMember({
+  member,
+}: {
+  member: ProjectContextMember
+}) {
   return (
     <OLRow className="project-member">
       <OLCol xs={8}>
@@ -19,11 +23,3 @@ export default function ViewMember({ member }) {
     </OLRow>
   )
 }
-
-ViewMember.propTypes = {
-  member: PropTypes.shape({
-    _id: PropTypes.string.isRequired,
-    email: PropTypes.string.isRequired,
-    privileges: PropTypes.string.isRequired,
-  }).isRequired,
-}

+ 3 - 2
services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.js → services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.ts

@@ -1,10 +1,11 @@
 import { useEffect, useState } from 'react'
 import { getJSON } from '../../../infrastructure/fetch-json'
 import useAbortController from '../../../shared/hooks/use-abort-controller'
+import { Contact } from '../utils/types'
 
 export function useUserContacts() {
   const [loading, setLoading] = useState(true)
-  const [data, setData] = useState(null)
+  const [data, setData] = useState<Contact[] | null>(null)
   const [error, setError] = useState(false)
 
   const { signal } = useAbortController()
@@ -21,7 +22,7 @@ export function useUserContacts() {
   return { loading, data, error }
 }
 
-function buildContact(contact) {
+function buildContact(contact: Omit<Contact, 'name' | 'display'>): Contact {
   const [emailPrefix] = contact.email.split('@')
 
   // the name is not just the default "email prefix as first name"

+ 9 - 0
services/web/frontend/js/features/share-project-modal/utils/types.ts

@@ -0,0 +1,9 @@
+export type Contact = {
+  id: string
+  display: string
+  email: string
+  first_name: string
+  last_name: string
+  name: string
+  type: 'user'
+}