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

Remove proptypes from ShareProjectModal

GitOrigin-RevId: b95fed5007f72e4a57a65b1d08d8fcc9579b3630
This commit is contained in:
David
2025-05-15 11:22:29 +01:00
committed by Copybot
parent 88a3b79f14
commit 63d3495ca3
12 changed files with 153 additions and 149 deletions

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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()

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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"

View File

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