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:
Domagoj Kriskovic 2024-11-11 17:24:51 +01:00 committed by Copybot
parent edb4e3d537
commit 8a90ffa3fb
6 changed files with 195 additions and 7 deletions

View file

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

View file

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

View file

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

View file

@ -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(() => {
acceptChanges(...changeIdsInSelection) const nChanges = numberOfChangesInSelection(ranges, state.selection.main)
}, [acceptChanges, changeIdsInSelection]) 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(() => { const rejectChangesHandler = useCallback(() => {
rejectChanges(...changeIdsInSelection) const nChanges = numberOfChangesInSelection(ranges, state.selection.main)
}, [rejectChanges, changeIdsInSelection]) 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 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}

View file

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

View file

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