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_invitation": "",
|
||||
"accept_or_reject_each_changes_individually": "",
|
||||
"accept_selected_changes": "",
|
||||
"accept_terms_and_conditions": "",
|
||||
"accepted_invite": "",
|
||||
"accepting_invite_as": "",
|
||||
|
@ -258,11 +259,15 @@
|
|||
"compromised_password": "",
|
||||
"configure_sso": "",
|
||||
"confirm": "",
|
||||
"confirm_accept_selected_changes": "",
|
||||
"confirm_accept_selected_changes_plural": "",
|
||||
"confirm_affiliation": "",
|
||||
"confirm_affiliation_to_relink_dropbox": "",
|
||||
"confirm_delete_user_type_email_address": "",
|
||||
"confirm_new_password": "",
|
||||
"confirm_primary_email_change": "",
|
||||
"confirm_reject_selected_changes": "",
|
||||
"confirm_reject_selected_changes_plural": "",
|
||||
"confirm_remove_sso_config_enter_email": "",
|
||||
"confirm_your_email": "",
|
||||
"confirming": "",
|
||||
|
@ -1203,6 +1208,7 @@
|
|||
"reject": "",
|
||||
"reject_all": "",
|
||||
"reject_change": "",
|
||||
"reject_selected_changes": "",
|
||||
"relink_your_account": "",
|
||||
"reload_editor": "",
|
||||
"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, {
|
||||
OutOfSyncModalProps,
|
||||
} from '@/features/ide-react/components/modals/out-of-sync-modal'
|
||||
import GenericConfirmModal, {
|
||||
GenericConfirmModalOwnProps,
|
||||
} from '../components/modals/generic-confirm-modal'
|
||||
|
||||
type ModalsContextValue = {
|
||||
genericModalVisible: boolean
|
||||
showGenericConfirmModal: (data: GenericConfirmModalOwnProps) => void
|
||||
showGenericMessageModal: (
|
||||
title: GenericMessageModalOwnProps['title'],
|
||||
message: GenericMessageModalOwnProps['message']
|
||||
|
@ -28,8 +32,15 @@ const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
|
|||
|
||||
export const ModalsContextProvider: FC = ({ children }) => {
|
||||
const [showGenericModal, setShowGenericModal] = useState(false)
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
const [genericMessageModalData, setGenericMessageModalData] =
|
||||
useState<GenericMessageModalOwnProps>({ title: '', message: '' })
|
||||
const [genericConfirmModalData, setGenericConfirmModalData] =
|
||||
useState<GenericConfirmModalOwnProps>({
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
})
|
||||
|
||||
const [shouldShowOutOfSyncModal, setShouldShowOutOfSyncModal] =
|
||||
useState(false)
|
||||
|
@ -41,6 +52,15 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
|||
setShowGenericModal(false)
|
||||
}, [])
|
||||
|
||||
const handleHideGenericConfirmModal = useCallback(() => {
|
||||
setShowConfirmModal(false)
|
||||
}, [])
|
||||
|
||||
const handleConfirmGenericConfirmModal = useCallback(() => {
|
||||
genericConfirmModalData.onConfirm()
|
||||
setShowConfirmModal(false)
|
||||
}, [genericConfirmModalData])
|
||||
|
||||
const showGenericMessageModal = useCallback(
|
||||
(
|
||||
title: GenericMessageModalOwnProps['title'],
|
||||
|
@ -52,6 +72,14 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
|||
[]
|
||||
)
|
||||
|
||||
const showGenericConfirmModal = useCallback(
|
||||
(data: GenericConfirmModalOwnProps) => {
|
||||
setGenericConfirmModalData(data)
|
||||
setShowConfirmModal(true)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleHideOutOfSyncModal = useCallback(() => {
|
||||
setShouldShowOutOfSyncModal(false)
|
||||
}, [])
|
||||
|
@ -64,10 +92,16 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
|||
const value = useMemo<ModalsContextValue>(
|
||||
() => ({
|
||||
showGenericMessageModal,
|
||||
showGenericConfirmModal,
|
||||
genericModalVisible: showGenericModal,
|
||||
showOutOfSyncModal,
|
||||
}),
|
||||
[showGenericMessageModal, showGenericModal, showOutOfSyncModal]
|
||||
[
|
||||
showGenericMessageModal,
|
||||
showGenericConfirmModal,
|
||||
showGenericModal,
|
||||
showOutOfSyncModal,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -78,6 +112,12 @@ export const ModalsContextProvider: FC = ({ children }) => {
|
|||
onHide={handleHideGenericModal}
|
||||
{...genericMessageModalData}
|
||||
/>
|
||||
<GenericConfirmModal
|
||||
show={showConfirmModal}
|
||||
onHide={handleHideGenericConfirmModal}
|
||||
{...genericConfirmModalData}
|
||||
onConfirm={handleConfirmGenericConfirmModal}
|
||||
/>
|
||||
<OutOfSyncModal
|
||||
{...outOfSyncModalData}
|
||||
show={shouldShowOutOfSyncModal}
|
||||
|
|
|
@ -30,6 +30,8 @@ import {
|
|||
import { isInsertOperation } from '@/utils/operations'
|
||||
import { isCursorNearViewportEdge } from '@/features/source-editor/utils/is-cursor-near-edge'
|
||||
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 state = useCodeMirrorStateContext()
|
||||
|
@ -72,6 +74,7 @@ const ReviewTooltipMenuContent: FC<{
|
|||
const { setView } = useReviewPanelViewActionsContext()
|
||||
const ranges = useRangesContext()
|
||||
const { acceptChanges, rejectChanges } = useRangesActionsContext()
|
||||
const { showGenericConfirmModal } = useModalsContext()
|
||||
|
||||
const addComment = useCallback(() => {
|
||||
setReviewPanelOpen(true)
|
||||
|
@ -109,12 +112,42 @@ const ReviewTooltipMenuContent: FC<{
|
|||
}, [ranges, state.selection.main])
|
||||
|
||||
const acceptChangesHandler = useCallback(() => {
|
||||
acceptChanges(...changeIdsInSelection)
|
||||
}, [acceptChanges, changeIdsInSelection])
|
||||
const nChanges = numberOfChangesInSelection(ranges, state.selection.main)
|
||||
showGenericConfirmModal({
|
||||
message: t('confirm_accept_selected_changes', { count: nChanges }),
|
||||
title: t('accept_selected_changes'),
|
||||
onConfirm: () => {
|
||||
acceptChanges(...changeIdsInSelection)
|
||||
},
|
||||
primaryVariant: 'danger',
|
||||
})
|
||||
}, [
|
||||
acceptChanges,
|
||||
changeIdsInSelection,
|
||||
ranges,
|
||||
showGenericConfirmModal,
|
||||
state.selection.main,
|
||||
t,
|
||||
])
|
||||
|
||||
const rejectChangesHandler = useCallback(() => {
|
||||
rejectChanges(...changeIdsInSelection)
|
||||
}, [rejectChanges, changeIdsInSelection])
|
||||
const nChanges = numberOfChangesInSelection(ranges, state.selection.main)
|
||||
showGenericConfirmModal({
|
||||
message: t('confirm_reject_selected_changes', { count: nChanges }),
|
||||
title: t('reject_selected_changes'),
|
||||
onConfirm: () => {
|
||||
rejectChanges(...changeIdsInSelection)
|
||||
},
|
||||
primaryVariant: 'danger',
|
||||
})
|
||||
}, [
|
||||
showGenericConfirmModal,
|
||||
t,
|
||||
ranges,
|
||||
state.selection.main,
|
||||
rejectChanges,
|
||||
changeIdsInSelection,
|
||||
])
|
||||
|
||||
const showChangesButtons = changeIdsInSelection.length > 0
|
||||
|
||||
|
@ -130,7 +163,10 @@ const ReviewTooltipMenuContent: FC<{
|
|||
{showChangesButtons && (
|
||||
<>
|
||||
<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
|
||||
className="review-tooltip-menu-button"
|
||||
onClick={acceptChangesHandler}
|
||||
|
@ -139,7 +175,10 @@ const ReviewTooltipMenuContent: FC<{
|
|||
</button>
|
||||
</OLTooltip>
|
||||
|
||||
<OLTooltip id="reject-all-changes" description={t('reject_all')}>
|
||||
<OLTooltip
|
||||
id="reject-all-changes"
|
||||
description={t('reject_selected_changes')}
|
||||
>
|
||||
<button
|
||||
className="review-tooltip-menu-button"
|
||||
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_invitation": "Accept invitation",
|
||||
"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",
|
||||
"accepted_invite": "Accepted invite",
|
||||
"accepting_invite_as": "You are accepting this invite as",
|
||||
|
@ -348,12 +349,16 @@
|
|||
"configure_sso": "Configure SSO",
|
||||
"configured": "Configured",
|
||||
"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_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_email": "Confirm Email",
|
||||
"confirm_new_password": "Confirm New Password",
|
||||
"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_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.",
|
||||
|
@ -1712,6 +1717,7 @@
|
|||
"reject": "Reject",
|
||||
"reject_all": "Reject all",
|
||||
"reject_change": "Reject change",
|
||||
"reject_selected_changes": "Reject selected changes",
|
||||
"related_tags": "Related Tags",
|
||||
"relink_your_account": "Re-link your account",
|
||||
"reload_editor": "Reload editor",
|
||||
|
|
Loading…
Reference in a new issue