diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 457c09d5fc..a4c8b151fe 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -965,7 +965,10 @@ "invite_more_members": "", "invite_not_accepted": "", "invite_resend_limit_hit": "", + "invite_resent": "", + "invite_revoked": "", "invite_users": "", + "invited_people": "", "invited_to_group": "", "invited_to_group_have_individual_subcription": "", "inviting": "", @@ -1576,6 +1579,7 @@ "resend": "", "resend_confirmation_code": "", "resend_group_invite": "", + "resend_invite": "", "resend_link_sso": "", "resend_managed_user_invite": "", "resending_confirmation_code": "", diff --git a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 index ae0890e9eb..5ca6ba1da4 100644 Binary files a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 and b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 differ diff --git a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs index e503509ee9..2ea68a635b 100644 --- a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs +++ b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs @@ -45,6 +45,7 @@ export default /** @type {const} */ ([ 'lightbulb_2', 'lock', 'lock_open', + 'mail', 'mode_comment', 'more_vert', 'neurology', @@ -52,6 +53,8 @@ export default /** @type {const} */ ([ 'notifications', 'open_in_new', 'password', + 'person', + 'person_edit', 'picture_as_pdf', 'push_pin', 'rate_review', @@ -75,6 +78,7 @@ export default /** @type {const} */ ([ 'upload', 'visibility', 'visibility_off', + 'warning', 'web_asset', 'whatshot', 'wifi_off', 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 586e7384ad..07fb6ed6ad 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 @@ -15,6 +15,15 @@ import OLCol from '@/shared/components/ol/ol-col' import MaterialIcon from '@/shared/components/material-icon' import { useUserContext } from '@/shared/context/user-context' import { upgradePlan } from '@/main/account-upgrade' +import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row' +import { + Dropdown, + DropdownMenu, + DropdownToggle, +} from '@/shared/components/dropdown/dropdown-menu' +import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item' +import { useFeatureFlag } from '@/shared/context/split-test-context' +import classnames from 'classnames' type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded' @@ -38,6 +47,7 @@ export default function EditMember({ canAddCollaborators, isReviewerOnFreeProject, }: EditMemberProps) { + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') const [privileges, setPrivileges] = useState( member.privileges ) @@ -51,8 +61,8 @@ export default function EditMember({ setPrivileges(member.privileges) }, [member.privileges]) - const { monitorRequest } = useShareProjectContext() - const { projectId, project, updateProject } = useProjectContext() + const { monitorRequest, setSuccessActionMessage } = useShareProjectContext() + const { projectId, project, updateProject, features } = useProjectContext() const { members, invites } = project || {} const user = useUserContext() @@ -113,10 +123,14 @@ export default function EditMember({ ).then(() => { updateProject({ members: - members?.map(item => - item._id === member._id ? { ...item, newPrivileges } : item + members?.map( + (item): ProjectMember => + item._id === member._id + ? { ...item, privileges: newPrivileges } + : item ) || [], }) + setSuccessActionMessage(t('access_updated')) }) } } @@ -136,7 +150,195 @@ export default function EditMember({ const confirmRemoval = privileges !== member.privileges && privilegeChangePending - return ( + const getDropdownLabel = () => { + if (hasBeenDowngraded) { + return t('select_access_level') + } + switch (privileges) { + case 'owner': + return t('make_owner') + case 'readAndWrite': + return t('editor') + case 'review': + return t('reviewer') + case 'readOnly': + return t('viewer') + case 'removeAccess': + return t('remove_access') + case 'downgraded': + return t('select_access_level') + default: + return t('remove_access') + } + } + + const getPrivilegeSubtitle = (privilege: PermissionsOption) => { + if (!['readAndWrite', 'review'].includes(privilege)) { + return + } + + if ( + (hasBeenDowngraded && !confirmRemoval) || + (!canAddCollaborators && !['readAndWrite', 'review'].includes(privileges)) + ) { + return t('limited_to_n_collaborators_per_project', { + count: features.collaborators, + }) + } + } + + const isPrivilegeDisabled = (privilege: PermissionsOption) => { + return ( + !canAddCollaborators && + ['readAndWrite', 'review'].includes(privilege) && + ((hasBeenDowngraded && !confirmRemoval) || + !['readAndWrite', 'review'].includes(privileges)) + ) + } + + return isSharingUpdatesEnabled ? ( + +
+ +
+ {member.email} + {member.pendingEditor && ( +
{t('view_only_downgraded')}
+ )} + {member.pendingReviewer && ( +
+ {t('view_only_reviewer_downgraded')} +
+ )} + {shouldWarnMember() && ( +
+ {t('will_lose_edit_access_on_date', { + date: linkSharingEnforcementDate, + })} +
+ )} + {isReviewerOnFreeProject && ( +
+ upgradePlan('track-changes')} + />, + ]} + /> +
+ )} +
+
+
+ { + if (eventKey) { + handlePrivilegeChange(eventKey) + } + }} + > +
+ {hasBeenDowngraded && !confirmRemoval && ( + + )} + + {getDropdownLabel()} + + +
+ + } + active={privileges === 'owner'} + trailingIcon={privileges === 'owner' ? 'check' : undefined} + > + {t('make_owner')} + + } + active={privileges === 'readAndWrite'} + trailingIcon={privileges === 'readAndWrite' ? 'check' : undefined} + disabled={isPrivilegeDisabled('readAndWrite')} + description={getPrivilegeSubtitle('readAndWrite')} + > + {t('editor')} + + {features.trackChangesVisible && ( + } + active={privileges === 'review'} + trailingIcon={privileges === 'review' ? 'check' : undefined} + disabled={isPrivilegeDisabled('review')} + description={getPrivilegeSubtitle('review')} + > + {t('reviewer')} + + )} + } + active={privileges === 'readOnly'} + trailingIcon={privileges === 'readOnly' ? 'check' : undefined} + > + {t('viewer')} + + } + active={privileges === 'removeAccess'} + trailingIcon={privileges === 'removeAccess' ? 'check' : undefined} + > + {t('remove_access')} + + +
+ {confirmRemoval && ( +
{ + e.preventDefault() + if (privilegeChangePending) { + commitPrivilegeChange(privileges) + } + }} + > + setPrivileges(member.privileges)} + /> + + )} +
+
+ ) : (
{ @@ -322,12 +524,17 @@ type ChangePrivilegesActionsProps = { function ChangePrivilegesActions({ handleReset, }: ChangePrivilegesActionsProps) { + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') const { t } = useTranslation() return ( -
+
+
+ +
{invite.email}
+ + {t('pending_invite')} + +
+ {isProjectOwner ? ( + {}}> + + + + + + + + + + ) : ( +
+
+ +
+
+ )} + + ) : (
{invite.email}
@@ -45,8 +87,10 @@ export default function Invite({ } function ResendInvite({ invite }: { invite: ProjectMember }) { + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') const { t } = useTranslation() - const { monitorRequest, setError, inFlight } = useShareProjectContext() + const { monitorRequest, setError, inFlight, setSuccessActionMessage } = + useShareProjectContext() const { projectId } = useProjectContext() // const buttonRef = useRef(null) @@ -54,6 +98,7 @@ function ResendInvite({ invite }: { invite: ProjectMember }) { const handleClick = useCallback( () => monitorRequest(() => resendInvite(projectId, invite)) + .then(() => setSuccessActionMessage(t('invite_resent'))) .catch(error => { if (error?.response?.status === 404) { setError('invite_expired') @@ -71,10 +116,19 @@ function ResendInvite({ invite }: { invite: ProjectMember }) { ;(document.activeElement as HTMLElement).blur() } }), - [invite, monitorRequest, projectId, setError] + [invite, monitorRequest, projectId, setError, setSuccessActionMessage, t] ) - return ( + return isSharingUpdatesEnabled ? ( + } + onClick={handleClick} + disabled={inFlight} + > + {t('resend_invite')} + + ) : ( } + variant="danger" + onClick={handleClick} + > + {t('revoke_invite')} + + ) : ( +

{t('invited_people')}

+ + {sortedMembers.map(member => + isProjectOwner ? ( + + ) : ( + + ) + )} + + {(invites || []).map(invite => ( + + ))} + + ) } export default InvitedPeople diff --git a/services/web/frontend/js/features/share-project-modal/components/owner-info.tsx b/services/web/frontend/js/features/share-project-modal/components/owner-info.tsx index d074ab669d..ec20c1083a 100644 --- a/services/web/frontend/js/features/share-project-modal/components/owner-info.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/owner-info.tsx @@ -2,13 +2,26 @@ import { useProjectContext } from '@/shared/context/project-context' import { useTranslation } from 'react-i18next' import OLRow from '@/shared/components/ol/ol-row' import OLCol from '@/shared/components/ol/ol-col' +import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row' import MaterialIcon from '@/shared/components/material-icon' +import { useFeatureFlag } from '@/shared/context/split-test-context' export default function OwnerInfo() { + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') const { t } = useTranslation() const { project } = useProjectContext() - return ( + return isSharingUpdatesEnabled ? ( + +
+ +
{project?.owner.email}
+
+
+
{t('owner')}
+
+
+ ) : (
diff --git a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx index 9a70aa1f22..bccf27b191 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx @@ -134,7 +134,13 @@ export default function ShareModalBody({ /> )} {isInvitedPeopleScreen || !isProjectOwner ? ( - + ) : ( any) => { setError(undefined) + setSuccessActionMessage(undefined) setInFlight(true) const promise = request() diff --git a/services/web/frontend/js/features/share-project-modal/components/view-member.tsx b/services/web/frontend/js/features/share-project-modal/components/view-member.tsx index 8e8bcee012..77e6cb40fa 100644 --- a/services/web/frontend/js/features/share-project-modal/components/view-member.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/view-member.tsx @@ -1,11 +1,27 @@ import MemberPrivileges from './member-privileges' import OLRow from '@/shared/components/ol/ol-row' import OLCol from '@/shared/components/ol/ol-col' +import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row' import MaterialIcon from '@/shared/components/material-icon' import { ProjectMember } from '@/shared/context/types/project-metadata' +import { useFeatureFlag } from '@/shared/context/split-test-context' export default function ViewMember({ member }: { member: ProjectMember }) { - return ( + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') + + return isSharingUpdatesEnabled ? ( + +
+ +
{member.email}
+
+
+
+ +
+
+
+ ) : (
diff --git a/services/web/locales/en.json b/services/web/locales/en.json index f03b253775..ec8285af7e 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1261,7 +1261,10 @@ "invite_not_valid": "This is not a valid project invite", "invite_not_valid_description": "The invite may have expired. Please contact the project owner", "invite_resend_limit_hit": "The invite resend limit hit", + "invite_resent": "Invite resent.", + "invite_revoked": "Invite revoked.", "invite_users": "Invite users", + "invited_people": "Invited people", "invited_to_group": "<0>__inviterName__ has invited you to join a group subscription on __appName__", "invited_to_group_have_individual_subcription": "__inviterName__ has invited you to join a group __appName__ subscription. If you join this group, you may not need your individual subscription. Would you like to cancel it?", "invited_to_group_login": "To accept this invitation you need to log in as __emailAddress__.", @@ -2094,6 +2097,7 @@ "resend": "Resend", "resend_confirmation_code": "Resend confirmation code", "resend_group_invite": "Resend group invite", + "resend_invite": "Resend invite", "resend_link_sso": "Resend SSO invite", "resend_managed_user_invite": "Resend managed user invite", "resending_confirmation_code": "Resending confirmation code",