diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.tsx similarity index 91% rename from services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx rename to services/web/frontend/js/features/share-project-modal/components/add-collaborators.tsx index b6d8491596..8606fb11fa 100644 --- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx +++ b/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({ 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) + } + }} /> ) } - -AddCollaborators.propTypes = { - readOnly: PropTypes.bool, -} diff --git a/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx b/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx index 46b0e21443..6d806968b1 100644 --- a/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx +++ b/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({ ) } -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 diff --git a/services/web/frontend/js/features/share-project-modal/components/invite.jsx b/services/web/frontend/js/features/share-project-modal/components/invite.tsx similarity index 86% rename from services/web/frontend/js/features/share-project-modal/components/invite.jsx rename to services/web/frontend/js/features/share-project-modal/components/invite.tsx index 3ca6f60d66..e9d761e4ee 100644 --- a/services/web/frontend/js/features/share-project-modal/components/invite.jsx +++ b/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 ( @@ -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 }) { ) } - -RevokeInvite.propTypes = { - invite: PropTypes.object.isRequired, -} diff --git a/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx b/services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx similarity index 87% rename from services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx rename to services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx index 4e9e60c28c..d235bd248b 100644 --- a/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx +++ b/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? void + inflight: boolean + projectId: string + setShowLinks: (show: boolean) => void +}) { const { t } = useTranslation() return ( @@ -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(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(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() diff --git a/services/web/frontend/js/features/share-project-modal/components/member-privileges.jsx b/services/web/frontend/js/features/share-project-modal/components/member-privileges.tsx similarity index 59% rename from services/web/frontend/js/features/share-project-modal/components/member-privileges.jsx rename to services/web/frontend/js/features/share-project-modal/components/member-privileges.tsx index bae354858a..c2b7bf98ef 100644 --- a/services/web/frontend/js/features/share-project-modal/components/member-privileges.jsx +++ b/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, -} diff --git a/services/web/frontend/js/features/share-project-modal/components/owner-info.jsx b/services/web/frontend/js/features/share-project-modal/components/owner-info.tsx similarity index 100% rename from services/web/frontend/js/features/share-project-modal/components/owner-info.jsx rename to services/web/frontend/js/features/share-project-modal/components/owner-info.tsx diff --git a/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/select-collaborators.tsx similarity index 86% rename from services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx rename to services/web/frontend/js/features/share-project-modal/components/select-collaborators.tsx index b65d4b3f0a..464c5b5368 100644 --- a/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx +++ b/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 }) { const { t } = useTranslation() const { @@ -58,12 +69,14 @@ export default function SelectCollaborators({ }) }, [unselectedOptions, inputValue]) - const inputRef = useRef(null) + const inputRef = useRef(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({ ) } -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 (
  • 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({ ) } - -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, -} diff --git a/services/web/frontend/js/features/share-project-modal/components/send-invites.jsx b/services/web/frontend/js/features/share-project-modal/components/send-invites.tsx similarity index 80% rename from services/web/frontend/js/features/share-project-modal/components/send-invites.jsx rename to services/web/frontend/js/features/share-project-modal/components/send-invites.tsx index f27d42f404..da704d039f 100644 --- a/services/web/frontend/js/features/share-project-modal/components/send-invites.jsx +++ b/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 ( @@ -30,10 +34,3 @@ export default function SendInvites({ ) } - -SendInvites.propTypes = { - canAddCollaborators: PropTypes.bool, - hasExceededCollaboratorLimit: PropTypes.bool, - haveAnyEditorsBeenDowngraded: PropTypes.bool, - somePendingEditorsResolved: PropTypes.bool, -} diff --git a/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx b/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.tsx similarity index 91% rename from services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx rename to services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.tsx index fec3e941ea..02100d2a2a 100644 --- a/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.jsx +++ b/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 }) { ) } -TransferOwnershipModal.propTypes = { - member: PropTypes.object.isRequired, - cancel: PropTypes.func.isRequired, -} diff --git a/services/web/frontend/js/features/share-project-modal/components/view-member.jsx b/services/web/frontend/js/features/share-project-modal/components/view-member.tsx similarity index 68% rename from services/web/frontend/js/features/share-project-modal/components/view-member.jsx rename to services/web/frontend/js/features/share-project-modal/components/view-member.tsx index 3faec91612..d4cf2e9333 100644 --- a/services/web/frontend/js/features/share-project-modal/components/view-member.jsx +++ b/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 ( @@ -19,11 +23,3 @@ export default function ViewMember({ member }) { ) } - -ViewMember.propTypes = { - member: PropTypes.shape({ - _id: PropTypes.string.isRequired, - email: PropTypes.string.isRequired, - privileges: PropTypes.string.isRequired, - }).isRequired, -} diff --git a/services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.js b/services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.ts similarity index 85% rename from services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.js rename to services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.ts index f23af1fbd3..35f9f9db9f 100644 --- a/services/web/frontend/js/features/share-project-modal/hooks/use-user-contacts.js +++ b/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(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 { const [emailPrefix] = contact.email.split('@') // the name is not just the default "email prefix as first name" diff --git a/services/web/frontend/js/features/share-project-modal/utils/types.ts b/services/web/frontend/js/features/share-project-modal/utils/types.ts new file mode 100644 index 0000000000..9636f14fb5 --- /dev/null +++ b/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' +}