mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Add confirm modal on accept/reject selected changes (#21540)
* Add showGenericConfirmModal in ModalsContext * Add confirm modal on accept/reject selected changes * plural in translations * change tooltip to include selected changes * add _plural to all translated languages * lowercase title/tooltip * count replacements as single change * use new translation key GitOrigin-RevId: afadbe1eeb2a290688b96f2b5388485f40c958d0
This commit is contained in:
parent
edb4e3d537
commit
8a90ffa3fb
6 changed files with 195 additions and 7 deletions
|
@ -37,6 +37,7 @@
|
||||||
"accept_change_error_title": "",
|
"accept_change_error_title": "",
|
||||||
"accept_invitation": "",
|
"accept_invitation": "",
|
||||||
"accept_or_reject_each_changes_individually": "",
|
"accept_or_reject_each_changes_individually": "",
|
||||||
|
"accept_selected_changes": "",
|
||||||
"accept_terms_and_conditions": "",
|
"accept_terms_and_conditions": "",
|
||||||
"accepted_invite": "",
|
"accepted_invite": "",
|
||||||
"accepting_invite_as": "",
|
"accepting_invite_as": "",
|
||||||
|
@ -258,11 +259,15 @@
|
||||||
"compromised_password": "",
|
"compromised_password": "",
|
||||||
"configure_sso": "",
|
"configure_sso": "",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm_accept_selected_changes": "",
|
||||||
|
"confirm_accept_selected_changes_plural": "",
|
||||||
"confirm_affiliation": "",
|
"confirm_affiliation": "",
|
||||||
"confirm_affiliation_to_relink_dropbox": "",
|
"confirm_affiliation_to_relink_dropbox": "",
|
||||||
"confirm_delete_user_type_email_address": "",
|
"confirm_delete_user_type_email_address": "",
|
||||||
"confirm_new_password": "",
|
"confirm_new_password": "",
|
||||||
"confirm_primary_email_change": "",
|
"confirm_primary_email_change": "",
|
||||||
|
"confirm_reject_selected_changes": "",
|
||||||
|
"confirm_reject_selected_changes_plural": "",
|
||||||
"confirm_remove_sso_config_enter_email": "",
|
"confirm_remove_sso_config_enter_email": "",
|
||||||
"confirm_your_email": "",
|
"confirm_your_email": "",
|
||||||
"confirming": "",
|
"confirming": "",
|
||||||
|
@ -1203,6 +1208,7 @@
|
||||||
"reject": "",
|
"reject": "",
|
||||||
"reject_all": "",
|
"reject_all": "",
|
||||||
"reject_change": "",
|
"reject_change": "",
|
||||||
|
"reject_selected_changes": "",
|
||||||
"relink_your_account": "",
|
"relink_your_account": "",
|
||||||
"reload_editor": "",
|
"reload_editor": "",
|
||||||
"remind_before_trial_ends": "",
|
"remind_before_trial_ends": "",
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import OLModal, {
|
||||||
|
OLModalBody,
|
||||||
|
OLModalFooter,
|
||||||
|
OLModalHeader,
|
||||||
|
OLModalTitle,
|
||||||
|
} from '@/features/ui/components/ol/ol-modal'
|
||||||
|
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||||
|
import { ButtonProps } from '@/features/ui/components/types/button-props'
|
||||||
|
|
||||||
|
export type GenericConfirmModalOwnProps = {
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
onConfirm: () => void
|
||||||
|
confirmLabel?: string
|
||||||
|
primaryVariant?: ButtonProps['variant']
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericConfirmModalProps = React.ComponentProps<typeof OLModal> &
|
||||||
|
GenericConfirmModalOwnProps
|
||||||
|
|
||||||
|
function GenericConfirmModal({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel,
|
||||||
|
primaryVariant = 'primary',
|
||||||
|
...modalProps
|
||||||
|
}: GenericConfirmModalProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const handleConfirmClick = modalProps.onConfirm
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OLModal {...modalProps}>
|
||||||
|
<OLModalHeader closeButton>
|
||||||
|
<OLModalTitle>{title}</OLModalTitle>
|
||||||
|
</OLModalHeader>
|
||||||
|
|
||||||
|
<OLModalBody className="modal-generic-confirm">{message}</OLModalBody>
|
||||||
|
|
||||||
|
<OLModalFooter>
|
||||||
|
<OLButton variant="secondary" onClick={() => modalProps.onHide()}>
|
||||||
|
{t('cancel')}
|
||||||
|
</OLButton>
|
||||||
|
<OLButton variant={primaryVariant} onClick={handleConfirmClick}>
|
||||||
|
{confirmLabel || t('ok')}
|
||||||
|
</OLButton>
|
||||||
|
</OLModalFooter>
|
||||||
|
</OLModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(GenericConfirmModal)
|
|
@ -12,9 +12,13 @@ import GenericMessageModal, {
|
||||||
import OutOfSyncModal, {
|
import OutOfSyncModal, {
|
||||||
OutOfSyncModalProps,
|
OutOfSyncModalProps,
|
||||||
} from '@/features/ide-react/components/modals/out-of-sync-modal'
|
} from '@/features/ide-react/components/modals/out-of-sync-modal'
|
||||||
|
import GenericConfirmModal, {
|
||||||
|
GenericConfirmModalOwnProps,
|
||||||
|
} from '../components/modals/generic-confirm-modal'
|
||||||
|
|
||||||
type ModalsContextValue = {
|
type ModalsContextValue = {
|
||||||
genericModalVisible: boolean
|
genericModalVisible: boolean
|
||||||
|
showGenericConfirmModal: (data: GenericConfirmModalOwnProps) => void
|
||||||
showGenericMessageModal: (
|
showGenericMessageModal: (
|
||||||
title: GenericMessageModalOwnProps['title'],
|
title: GenericMessageModalOwnProps['title'],
|
||||||
message: GenericMessageModalOwnProps['message']
|
message: GenericMessageModalOwnProps['message']
|
||||||
|
@ -28,8 +32,15 @@ const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
|
||||||
|
|
||||||
export const ModalsContextProvider: FC = ({ children }) => {
|
export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
const [showGenericModal, setShowGenericModal] = useState(false)
|
const [showGenericModal, setShowGenericModal] = useState(false)
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||||
const [genericMessageModalData, setGenericMessageModalData] =
|
const [genericMessageModalData, setGenericMessageModalData] =
|
||||||
useState<GenericMessageModalOwnProps>({ title: '', message: '' })
|
useState<GenericMessageModalOwnProps>({ title: '', message: '' })
|
||||||
|
const [genericConfirmModalData, setGenericConfirmModalData] =
|
||||||
|
useState<GenericConfirmModalOwnProps>({
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
onConfirm: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
const [shouldShowOutOfSyncModal, setShouldShowOutOfSyncModal] =
|
const [shouldShowOutOfSyncModal, setShouldShowOutOfSyncModal] =
|
||||||
useState(false)
|
useState(false)
|
||||||
|
@ -41,6 +52,15 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
setShowGenericModal(false)
|
setShowGenericModal(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleHideGenericConfirmModal = useCallback(() => {
|
||||||
|
setShowConfirmModal(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleConfirmGenericConfirmModal = useCallback(() => {
|
||||||
|
genericConfirmModalData.onConfirm()
|
||||||
|
setShowConfirmModal(false)
|
||||||
|
}, [genericConfirmModalData])
|
||||||
|
|
||||||
const showGenericMessageModal = useCallback(
|
const showGenericMessageModal = useCallback(
|
||||||
(
|
(
|
||||||
title: GenericMessageModalOwnProps['title'],
|
title: GenericMessageModalOwnProps['title'],
|
||||||
|
@ -52,6 +72,14 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const showGenericConfirmModal = useCallback(
|
||||||
|
(data: GenericConfirmModalOwnProps) => {
|
||||||
|
setGenericConfirmModalData(data)
|
||||||
|
setShowConfirmModal(true)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const handleHideOutOfSyncModal = useCallback(() => {
|
const handleHideOutOfSyncModal = useCallback(() => {
|
||||||
setShouldShowOutOfSyncModal(false)
|
setShouldShowOutOfSyncModal(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -64,10 +92,16 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
const value = useMemo<ModalsContextValue>(
|
const value = useMemo<ModalsContextValue>(
|
||||||
() => ({
|
() => ({
|
||||||
showGenericMessageModal,
|
showGenericMessageModal,
|
||||||
|
showGenericConfirmModal,
|
||||||
genericModalVisible: showGenericModal,
|
genericModalVisible: showGenericModal,
|
||||||
showOutOfSyncModal,
|
showOutOfSyncModal,
|
||||||
}),
|
}),
|
||||||
[showGenericMessageModal, showGenericModal, showOutOfSyncModal]
|
[
|
||||||
|
showGenericMessageModal,
|
||||||
|
showGenericConfirmModal,
|
||||||
|
showGenericModal,
|
||||||
|
showOutOfSyncModal,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -78,6 +112,12 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
||||||
onHide={handleHideGenericModal}
|
onHide={handleHideGenericModal}
|
||||||
{...genericMessageModalData}
|
{...genericMessageModalData}
|
||||||
/>
|
/>
|
||||||
|
<GenericConfirmModal
|
||||||
|
show={showConfirmModal}
|
||||||
|
onHide={handleHideGenericConfirmModal}
|
||||||
|
{...genericConfirmModalData}
|
||||||
|
onConfirm={handleConfirmGenericConfirmModal}
|
||||||
|
/>
|
||||||
<OutOfSyncModal
|
<OutOfSyncModal
|
||||||
{...outOfSyncModalData}
|
{...outOfSyncModalData}
|
||||||
show={shouldShowOutOfSyncModal}
|
show={shouldShowOutOfSyncModal}
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
import { isInsertOperation } from '@/utils/operations'
|
import { isInsertOperation } from '@/utils/operations'
|
||||||
import { isCursorNearViewportEdge } from '@/features/source-editor/utils/is-cursor-near-edge'
|
import { isCursorNearViewportEdge } from '@/features/source-editor/utils/is-cursor-near-edge'
|
||||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||||
|
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||||
|
import { numberOfChangesInSelection } from '../utils/changes-in-selection'
|
||||||
|
|
||||||
const ReviewTooltipMenu: FC = () => {
|
const ReviewTooltipMenu: FC = () => {
|
||||||
const state = useCodeMirrorStateContext()
|
const state = useCodeMirrorStateContext()
|
||||||
|
@ -72,6 +74,7 @@ const ReviewTooltipMenuContent: FC<{
|
||||||
const { setView } = useReviewPanelViewActionsContext()
|
const { setView } = useReviewPanelViewActionsContext()
|
||||||
const ranges = useRangesContext()
|
const ranges = useRangesContext()
|
||||||
const { acceptChanges, rejectChanges } = useRangesActionsContext()
|
const { acceptChanges, rejectChanges } = useRangesActionsContext()
|
||||||
|
const { showGenericConfirmModal } = useModalsContext()
|
||||||
|
|
||||||
const addComment = useCallback(() => {
|
const addComment = useCallback(() => {
|
||||||
setReviewPanelOpen(true)
|
setReviewPanelOpen(true)
|
||||||
|
@ -109,12 +112,42 @@ const ReviewTooltipMenuContent: FC<{
|
||||||
}, [ranges, state.selection.main])
|
}, [ranges, state.selection.main])
|
||||||
|
|
||||||
const acceptChangesHandler = useCallback(() => {
|
const acceptChangesHandler = useCallback(() => {
|
||||||
|
const nChanges = numberOfChangesInSelection(ranges, state.selection.main)
|
||||||
|
showGenericConfirmModal({
|
||||||
|
message: t('confirm_accept_selected_changes', { count: nChanges }),
|
||||||
|
title: t('accept_selected_changes'),
|
||||||
|
onConfirm: () => {
|
||||||
acceptChanges(...changeIdsInSelection)
|
acceptChanges(...changeIdsInSelection)
|
||||||
}, [acceptChanges, changeIdsInSelection])
|
},
|
||||||
|
primaryVariant: 'danger',
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
acceptChanges,
|
||||||
|
changeIdsInSelection,
|
||||||
|
ranges,
|
||||||
|
showGenericConfirmModal,
|
||||||
|
state.selection.main,
|
||||||
|
t,
|
||||||
|
])
|
||||||
|
|
||||||
const rejectChangesHandler = useCallback(() => {
|
const rejectChangesHandler = useCallback(() => {
|
||||||
|
const nChanges = numberOfChangesInSelection(ranges, state.selection.main)
|
||||||
|
showGenericConfirmModal({
|
||||||
|
message: t('confirm_reject_selected_changes', { count: nChanges }),
|
||||||
|
title: t('reject_selected_changes'),
|
||||||
|
onConfirm: () => {
|
||||||
rejectChanges(...changeIdsInSelection)
|
rejectChanges(...changeIdsInSelection)
|
||||||
}, [rejectChanges, changeIdsInSelection])
|
},
|
||||||
|
primaryVariant: 'danger',
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
showGenericConfirmModal,
|
||||||
|
t,
|
||||||
|
ranges,
|
||||||
|
state.selection.main,
|
||||||
|
rejectChanges,
|
||||||
|
changeIdsInSelection,
|
||||||
|
])
|
||||||
|
|
||||||
const showChangesButtons = changeIdsInSelection.length > 0
|
const showChangesButtons = changeIdsInSelection.length > 0
|
||||||
|
|
||||||
|
@ -130,7 +163,10 @@ const ReviewTooltipMenuContent: FC<{
|
||||||
{showChangesButtons && (
|
{showChangesButtons && (
|
||||||
<>
|
<>
|
||||||
<div className="review-tooltip-menu-divider" />
|
<div className="review-tooltip-menu-divider" />
|
||||||
<OLTooltip id="accept-all-changes" description={t('accept_all')}>
|
<OLTooltip
|
||||||
|
id="accept-all-changes"
|
||||||
|
description={t('accept_selected_changes')}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="review-tooltip-menu-button"
|
className="review-tooltip-menu-button"
|
||||||
onClick={acceptChangesHandler}
|
onClick={acceptChangesHandler}
|
||||||
|
@ -139,7 +175,10 @@ const ReviewTooltipMenuContent: FC<{
|
||||||
</button>
|
</button>
|
||||||
</OLTooltip>
|
</OLTooltip>
|
||||||
|
|
||||||
<OLTooltip id="reject-all-changes" description={t('reject_all')}>
|
<OLTooltip
|
||||||
|
id="reject-all-changes"
|
||||||
|
description={t('reject_selected_changes')}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="review-tooltip-menu-button"
|
className="review-tooltip-menu-button"
|
||||||
onClick={rejectChangesHandler}
|
onClick={rejectChangesHandler}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { SelectionRange } from '@codemirror/state'
|
||||||
|
import { Ranges } from '@/features/review-panel-new/context/ranges-context'
|
||||||
|
import { isDeleteChange, isInsertChange } from '@/utils/operations'
|
||||||
|
import { canAggregate } from './can-aggregate'
|
||||||
|
import { Change, EditOperation } from '../../../../../types/change'
|
||||||
|
|
||||||
|
export function numberOfChangesInSelection(
|
||||||
|
ranges: Ranges | undefined,
|
||||||
|
selection: SelectionRange
|
||||||
|
) {
|
||||||
|
if (!ranges) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
let precedingChange: Change<EditOperation> | null = null
|
||||||
|
|
||||||
|
for (const change of ranges.changes) {
|
||||||
|
if (
|
||||||
|
precedingChange &&
|
||||||
|
isInsertChange(precedingChange) &&
|
||||||
|
isDeleteChange(change) &&
|
||||||
|
canAggregate(change, precedingChange)
|
||||||
|
) {
|
||||||
|
// only count once for the aggregated change
|
||||||
|
continue
|
||||||
|
} else if (
|
||||||
|
isInsertChange(change) &&
|
||||||
|
change.op.p >= selection.from &&
|
||||||
|
change.op.p + change.op.i.length <= selection.to
|
||||||
|
) {
|
||||||
|
count++
|
||||||
|
} else if (
|
||||||
|
isDeleteChange(change) &&
|
||||||
|
selection.from <= change.op.p &&
|
||||||
|
change.op.p <= selection.to
|
||||||
|
) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
precedingChange = change
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
|
@ -41,6 +41,7 @@
|
||||||
"accept_change_error_title": "Accept Change Error",
|
"accept_change_error_title": "Accept Change Error",
|
||||||
"accept_invitation": "Accept invitation",
|
"accept_invitation": "Accept invitation",
|
||||||
"accept_or_reject_each_changes_individually": "Accept or reject each change individually",
|
"accept_or_reject_each_changes_individually": "Accept or reject each change individually",
|
||||||
|
"accept_selected_changes": "Accept selected changes",
|
||||||
"accept_terms_and_conditions": "Accept terms and conditions",
|
"accept_terms_and_conditions": "Accept terms and conditions",
|
||||||
"accepted_invite": "Accepted invite",
|
"accepted_invite": "Accepted invite",
|
||||||
"accepting_invite_as": "You are accepting this invite as",
|
"accepting_invite_as": "You are accepting this invite as",
|
||||||
|
@ -348,12 +349,16 @@
|
||||||
"configure_sso": "Configure SSO",
|
"configure_sso": "Configure SSO",
|
||||||
"configured": "Configured",
|
"configured": "Configured",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
"confirm_accept_selected_changes": "Are you sure you want to accept the selected change?",
|
||||||
|
"confirm_accept_selected_changes_plural": "Are you sure you want to accept the selected __count__ changes?",
|
||||||
"confirm_affiliation": "Confirm Affiliation",
|
"confirm_affiliation": "Confirm Affiliation",
|
||||||
"confirm_affiliation_to_relink_dropbox": "Please confirm you are still at the institution and on their license, or upgrade your account in order to relink your Dropbox account.",
|
"confirm_affiliation_to_relink_dropbox": "Please confirm you are still at the institution and on their license, or upgrade your account in order to relink your Dropbox account.",
|
||||||
"confirm_delete_user_type_email_address": "To confirm you want to delete __userName__ please type the email address associated with their account",
|
"confirm_delete_user_type_email_address": "To confirm you want to delete __userName__ please type the email address associated with their account",
|
||||||
"confirm_email": "Confirm Email",
|
"confirm_email": "Confirm Email",
|
||||||
"confirm_new_password": "Confirm New Password",
|
"confirm_new_password": "Confirm New Password",
|
||||||
"confirm_primary_email_change": "Confirm primary email change",
|
"confirm_primary_email_change": "Confirm primary email change",
|
||||||
|
"confirm_reject_selected_changes": "Are you sure you want to reject the selected change?",
|
||||||
|
"confirm_reject_selected_changes_plural": "Are you sure you want to reject the selected __count__ changes?",
|
||||||
"confirm_remove_sso_config_enter_email": "To confirm you want to remove your SSO configuration, enter your email address:",
|
"confirm_remove_sso_config_enter_email": "To confirm you want to remove your SSO configuration, enter your email address:",
|
||||||
"confirm_your_email": "Confirm your email address",
|
"confirm_your_email": "Confirm your email address",
|
||||||
"confirmation_link_broken": "Sorry, something is wrong with your confirmation link. Please try copy and pasting the link from the bottom of your confirmation email.",
|
"confirmation_link_broken": "Sorry, something is wrong with your confirmation link. Please try copy and pasting the link from the bottom of your confirmation email.",
|
||||||
|
@ -1712,6 +1717,7 @@
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
"reject_all": "Reject all",
|
"reject_all": "Reject all",
|
||||||
"reject_change": "Reject change",
|
"reject_change": "Reject change",
|
||||||
|
"reject_selected_changes": "Reject selected changes",
|
||||||
"related_tags": "Related Tags",
|
"related_tags": "Related Tags",
|
||||||
"relink_your_account": "Re-link your account",
|
"relink_your_account": "Re-link your account",
|
||||||
"reload_editor": "Reload editor",
|
"reload_editor": "Reload editor",
|
||||||
|
|
Loading…
Reference in a new issue